Publications Subscriptions Observe - meteor

I am working on a project where I want to show markers on a map.
These markers should be published from server with a viewport-constraint. That means that just markers are published which are inside the current users viewport.
The publication looks something like this:
//server
Meteor.publish('posts', function(bottom_left_x, bottom_left_y, upper_right_x, upper_right_y, limit) {
return Posts.find({locs: {$geoWithin: {$box:
[[bottom_left_x, bottom_left_y],
[upper_right_x, upper_right_y]]}}},
{sort: {submitted: -1}, limit: limit});
});
I always call this function via subscription when my map_center changes:
//client
google.maps.event.addListener(map, 'idle', function(event) {
var bounds = map.getBounds();
var ne = bounds.getNorthEast();
var sw = bounds.getSouthWest();
postsHandle= Meteor.subscribe('posts', sw.lat(), sw.lng(), ne.lat(), ne.lng(), 10);
});
Till now everything works fine.
Further i created a observefunction on posts, that renders a marker when "added" is called and remove when "removed" is called. Observe is very good to render new markers and to destroy the old ones
//client
Posts.find().observeChanges({
added: function(post) {
// when 'added' callback fires, add HTML element
var marker = new google.maps.Marker({
position: new google.maps.LatLng(post.locs.lat, post.locs.lng),
postId: post._id,
map: map,
});
},removed .... and so on
That problem is that the observe-Callback is triggered on the whole Posts-Collection. But i just want to show markers which are inside the users viewport. Thats why i normally have to do something like this:
//client
Posts.find({locs: {$geoWithin: {$box:
[[bottom_left_x, bottom_left_y],
[upper_right_x, upper_right_y]]}}},
{sort: {submitted: -1}, limit: limit}).observeChanges({
But thats not possible. GeoWithin is not supported inside minimongo and it is not possible to call oberserve with a collection that has a limit.
Has anyone an idea how to accomplish this?
Maybe there is a way to push the posts i get from subcription directly to the map without using a query on minimongo?

The solution is so easy !
Meteor.autosubscribe( function () {
Meteor.subscribe( 'chat', { room: Session.get( 'currentRoom' ) } );
} );
If you want to limit your subscription to the maps viewport with changing viewport-bounds, than you have to use autosubscribe. It seems that autosubscribe takes care of changing subscription-arguments :)
Meteor.autosubscribe( function () {
var a = Session.get('bounds');
if(a)
Meteor.subscribe( 'posts', a.swlat, a.swlng, a.nelat, a.nelng, 5 );
} );

Related

Meteor Iron-Router: Wait for Subscription sequential

I have the following route defined in my iron-router:
this.route("/example/:id", {
name: "example",
template: "example",
action: function () {
this.wait(Meteor.subscribe('sub1', this.params.id));
this.wait(Meteor.subscribe('sub2', <<data of sub1 needed here>>));
if (this.ready()) {
this.render();
} else {
this.render('Loading');
}
}
});
I want to wait for sub1 and sub2 before rendering my actual template. The problem is that I need a piece of data which is part of the result of sub1 for the sub2 subscription.
How can I wait sequential for subscriptions? So that I can split the wait in two steps and wait for my first subscription to be finished. Then start the second subscription and then set this.ready() to render the template?
A workaround that I thought of was to use Reactive-Vars for the subscriptions and dont use .wait and .ready which is provided by iron-router. But I would like to use a more convenient solution provided by iron-router or Meteor itself. Do you know a better solution for this?
Thanks for your answers!
Publish Composite Package:
If the second subscription is reactively dependent on certain fields from the first dataset -- and if there will be a many-to-many "join" association, it might be worth looking into reywood:publish-composite package:
It provides a clean and easy way to manage associated subscriptions for collections with hierarchical relations.
Publication:
Meteor.publishComposite('compositeSub', function(id) {
return {
find: function() {
// return all documents from FirstCollection filtered by 'this.params.id' passed to subscription
return FirstCollection.find({ _id: id });
},
children: [
find: function(item) {
// return data from second collection filtered by using reference of each item's _id from results of first subscription
// you can also use any other field from 'item' as reference here, as per your database relations
return SecondCollection.find({ itemId: item._id });
}
]
}
});
Subscription:
Then you can just subscribe in the router using:
Meteor.subscribe('compositeSub', this.params.id);
Router hooks:
As a suggestion, hooks in iron-router are really useful, as they take care of a lot of things for you. So why not use the waitOn hook that manages this.wait and loading states neatly?
this.route('/example/:id', {
name: "example",
template: "example",
// this template will be rendered until the subscriptions are ready
loadingTemplate: 'loading',
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('compositeSub', this.params.id);
// FYI, this can also return an array of subscriptions
},
action: function () {
this.render();
}
});
You can use the configure option to add a template for loading event:
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading'
});
Note regarding the comment in question:
If both subscriptions only depend on the same id parameter passed to it, you can use the following, as mentioned by #abj27 in the comment above -- however, this does not seem to be the case, going by your example:
Publication:
Meteor.publish("subOneAndTwo", function (exampleId) {
check(exampleId, String);
return [
FirstCollection.find({ _id: exampleId });
SecondCollection.find({ firstId: exampleId })
];
});
Subscription:
Meteor.subscribe('subOneAndTwo', this.params.id);
So just check what you need and use a solution accordingly.
https://github.com/kadirahq/subs-manager
With this package, you can assign a subscription to a variable. Then, you can check that variable's ready state. I just got this working... after years of trying to understand.
Here is my code snippet that works, but oddly I had to wrap it in a 1ms timeout to work...
```
Router.route('/activity/:activityId', function (params) {
var params = this.params;
setTimeout(function(){
var thePage = window.location.href.split("/");;
window.thePage = thePage[4];
dbSubscriptionActivites.clear();
window.thisDbSubscriptionActivites = "";
window.thisDbSubscriptionActivites = dbSubscriptionActivites.subscribe("activityByPage", window.thePage);
Tracker.autorun(function() {
if(window.thisDbSubscriptionActivites.ready()) {
dbSubscriptionComments.clear();
window.thisDbSubscriptionComments = "";
window.thisDbSubscriptionComments = dbSubscriptionComments.subscribe('fetchComments', "activity", Activities.findOne({})._id);
BlazeLayout.render("activity");
$('body').removeClass("shards-app-promo-page--1");
}
});
},1); // Must wait for DOM?
});
```
Examine: window.thisDbSubscriptionActivites = dbSubscriptionActivites.subscribe("activityByPage", window.thePage);
I'm setting as a window variable, but you could do a const mySub = ...
Then, you check that in the autorun function later.
You can see there is where I am doing subscriptions.
I suppose I really should move the BlazeLayout render in to another .ready() check for the comments.

Meteor: Collection loads when I navigate to a page, but not when URL is directly inputted

I'm a Meteor newb and would appreciate any help here.
I'm creating a flashcard app where you can create decks of cards. It saves the progress that one has made through a deck of cards.
It appears that when I navigate into a deck of cards by clicking on the name of a deck (a link), everything works fine. But when I directly paste the URI in, the Deck collection fails to load.
I believe that this is because of routes.js, in the lib folder, is the first to load so there is no data. I tried using the waitOn function in Router.configure, but it's still not working.
Thanks in advance for your help!
Router.route ('/:_id/:wordIndex', {
template: 'wordPage',
data: function () {
var index = this.params.wordIndex;
var id = this.params._id;
console.log(Decks);
var word = Decks.findOne ( id,
{ fields: { 'wordIds': 1 } }
);
}
}
);
I tried using the waitOn function within the router already as well:
Router.configure ( {
layoutTemplate: 'layout',
waitOn: function () { return [
Meteor.subscribe ('words'),
Meteor.subscribe ('decks')
]}
})
EDIT
Ok, it seems like I found a solution by getting the data client side instead of at the route. But I dunno. There's gotta be a better way? This seems excessive to me.
Template.wordItem.helpers({
wordObj: function () {
var controller = Iron.controller();
var index = controller.params.wordIndex;
var id = controller.params._id;
var wordsObject = Decks.findOne ( id,
{ fields: { 'wordIds': 1 } }
);
var wordId = wordsObject.wordIds[index];
return Words.findOne ( wordId );
},
Here is a sample repository I've created which is very similar to what you are trying to do.
http://meteorpad.com/pad/xKS38qEZAieEPDyLs/Leaderboard
It seems to work. I suspect your problem is elsewhere.

google maps infoWindow click event re-renders map-canvas in Meteor

Hey Im trying to use google maps within my MeteorJS project to have google maps display on a map all customers, and then to display an infoWindow when you click on one of the markers.
problem is anytime you click on the marker it re-renders the map from scratch, i know this has to do with the the reactivity of the Session variable being set when the infoWindow is being clicked.
is there any way avoid the map being re-rendered when the session variable is changing?
thanks.
below is the JS and template im using in my project.
<template name="customers_map">
{{#constant}}
<div id="mapWrapper">
<div id="map-canvas"></div>
</div>
{{/constant}}
</template>
the code for making the google maps and markers.
Template.customers_map.rendered = function() {
$("#map-canvas").height("400px");
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(p) {
Session.set("myLat", p.coords.latitude);
Session.set("myLng", p.coords.longitude);
});
}
Deps.autorun(function(){
var mapOptions = {
center: new google.maps.LatLng(Session.get("myLat"), Session.get("myLng")),
zoom: 15,
zoomControl: true,
zoomControlOptions: {style: google.maps.ZoomControlStyle.SMALL},
streetViewControl: false,
mapTypeControl: false,
scaleControl: true,
mapTypeId: google.maps.MapTypeId.SMALL
}
var map = new google.maps.Map(document.getElementById("map-canvas"), mapOptions);
var infowindow = new google.maps.InfoWindow({
content: Template.customers_infoWindow()
});
Customers.find().forEach(function(customer) {
if (customer.loc != null) {
var geo = customer.geoLocation();
var marker = new google.maps.Marker({
position: new google.maps.LatLng(geo.lat, geo.lng),
title: customer.name(),
icon:'http://maps.google.com/mapfiles/ms/icons/green-dot.png'
});
marker.setMap(map);
google.maps.event.addListener(marker, 'click', function() {
Session.set("customerId", customer._id);
infowindow.open(map,marker);
});
} else {
console.log(customer.name() + " has no geoLocation");
};
});
});
};
the infoWindow template
<template name="customers_infoWindow">
<h1>{{record.name}}</h1>
</template>
and the js for the infoWindow template
Template.customers_infoWindow.record = function() {
return Customers.findOne({_id: Session.get("customerId")});
}
If you create a global googlemaps object, you can access its properties from anywhere. This article has a nice example of doing this.
The overall gist is:
Create a googlemaps class with an initialize method. At the end of the initialize method, set a session variable for your map's existence. ( Session.set('map', true);)
Call create a googlemap object by calling the googlemap init method from within Template.customers_map.rendered.
It's a bit difficult to be sure without having a running version in front of me, but I think this is essentially because you have all your code in one big Deps.autorun block. Clicking one of the markers is changing the Session variable customerId, which will cause customers_infoWindow to re-render (as it's clearly a dependency), but I'm sure this is the intended behaviour.
However, since you're declaring var infoWindow in your Deps.autorun block to have an instance of that template as one of its properties, I think that changing customers_infoWindow will actually invalidate the entire Deps.autorun calculation, which means the whole block will be executed again, including the var map = new google.maps.Map(...) line, which will essentially re-render the map (even though it doesn't re-render that actual div element that contains it).
So, I would suggest splitting your code into separate Deps.autorun blocks, and making sure that anything in the same block should be re-run at the same time - clearly, this means that the Google Maps initialisation code and the infoWindow handler should be in separate blocks.
To reiterate, I think that's what's going on, but you'll have to try it and let me know...

Simplemodal Google Map displays once but not on 2nd Click

I've done well by my standards! I have pretty much zero knowledge of JS other than the basics of Functions etc. Ive used these pages to pull together a working script that loads Google Maps into a Modal using the SimpleModal framework. To my relief I got it working but it has one final bug that I cannot shift. The Modal loads on the first click of the HREF but if I close the modal and then try to reopen it it loads the modal with parts of the map missing. The missing map issue was a problem i thought I had already solved. My JS is
var map;
var src = 'https://sites.google.com/site/bristol2monaco/kml/route2.kml';
function initialize() {
var myLatlng = new google.maps.LatLng(51.337890,-0.813049);
map = new google.maps.Map(document.getElementById("basic-modal-content"), {
center: myLatlng,
zoom: 7,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
loadKmlLayer(src, map);
}
function loadKmlLayer(src, map) {
var kmlLayer = new google.maps.KmlLayer(src, {
suppressInfoWindows: true,
clickable: false,
preserveViewport: true,
map: map
});
}
initialize();
and the js file that registers the 'click' contains:
jQuery(function ($) {
// Load dialog on page load
//$('#basic-modal-content').modal();
// Load dialog on click
$('#table .newbasic').click(function (e) {
$('#basic-modal-content').modal();
var center = map.getCenter();
google.maps.event.trigger(map, "resize");
map.setCenter(center);
return false;
});
});
As i thought i had already solved the missing map bug (using solutions posted here) with the (map, resize) line above none of the solutions on here help. Do i have to reinitialise the map or something. Grateful for advice.
When you call the modal to open use the onOpen Function described by Eric Martin. With using his onOpen function you will be able to use the callback feature and thusly use the google map event-listener to listen for the resize event. Once the resize event has been heard, you can reinitialize your google map thusly removing the gray areas
$("#table .newbasic").modal({
onOpen: function (dialog) {
google.maps.event.addListenerOnce(map, 'resize', function() {
//Alert TESTING IF RESIZE is heard(remove after test)
alert("heard resize onOpen");
initialize();
map.setCenter(center);
});
google.maps.event.trigger(map, "resize");
}
});

jQuery Mobile + jQuery UI google maps + Fusion Markers

this has been baking my noodle for months, I'll be honest, I am a designer not a programmer so this type of scripting is a bit harder than the average jquery/javascript that I'm used too.
I can't find any basic documentation on how to implement it, apart from this but it's not massively intuitive - http://jquery-ui-map.googlecode.com/svn/trunk/demos/jquery-mobile-example.html
I've tried making a JSfiddle, but I can't even get it to work (now working thanks to Tsar)
I've built a jQuery mobile app and I'm desperate to get the geo-location functionality working with fusion table markers (from my fusion table) and to be allowed to click on the fusion table markers to reveal a info window. This info window will be constructed in fusion tables.
I need the geo-location to center the map on the devices current location - if the geo-location is not available or denied by device user, then the map needs to be centered on these co-ordinates 52.450939, -1.721002.
What would be the next level is to have the the jQuery mobile UI pop-up window but this is not critical, just the standard bubble on the map is fine.
A updated JSFiddle... http://jsfiddle.net/twGHC/30/
My fusion table number is: 1260763
Default location is: (only if Geo location is not available) 52.450939, -1.721002
Zoom level: 13
Any advice would be awesome, please feel free to JSfiddle it. Thanks in advance.
Here's a working solution, which detects user's location, drops a marker on it and plots the map with your Fusion Markers. As per Google Maps v3 API documentation:
$(function() {
var position = new google.maps.LatLng(52.450939, -1.721002);
getCurrentPosition = function(callback) {
// Try W3C Geolocation (Preferred)
if(navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
function(pos) {
position = new google.maps.LatLng(pos.coords.latitude, pos.coords.longitude);
callback(position);
}, callback(position));
} // Try Google Gears Geolocation
else if (google.gears) {
var geo = google.gears.factory.create('beta.geolocation');
geo.getCurrentPosition(
function(pos) {
position = new google.maps.LatLng(pos.latitude,pos.longitude);
callback(position);
}, callback(position));
} // Browser doesn't support Geolocation
else {
// Drop the user off in Coventry =)
callback(position);
}
};
// call the above function
getCurrentPosition(InitMap);
});
function InitMap(pos) {
map = new google.maps.Map(document.getElementById('map_canvas'), {
center: pos,
zoom: 14,
mapTypeId: 'roadmap'
});
var marker = new google.maps.Marker({
position: pos,
animation: google.maps.Animation.DROP,
map: map,
title: "You are here, mate!"
});
var layer = new google.maps.FusionTablesLayer({
query: {
select: 'Geocodable address',
from: '1260763'
},
});
layer.setMap(map);
};
When user denies tracking of his location, exception is caught in getCurrentPosition, however, 2nd optional parameter in this function is an exception handler, so what I did is simply passed in callback(position) so that default location sets on the map. If you don't want to do it, move out the map initializer code from InitMap into separate function and call it instead, when exceptions are caught, to display a blank map.
See it in action here: http://jsfiddle.net/twGHC/36/
P.S. In case your next question is "how to add a balloon pop-up on marker click", here's what you need to do.
Here is how to do it with jquery-ui-map:
http://jsfiddle.net/rweCf/1/
http://jsfiddle.net/rweCf/1/embedded/result/
If you just want to change within a certain radius of the client position this is how you would do it
http://jsfiddle.net/Ywknf/1/ (client location is the initial point so everyone can see the results)
Here is the code if the url isnt working or the trunk code changed
$(function() {
$('#map_canvas').gmap({'center': '52.450939, -1.721002', 'zoom': 10, 'disableDefaultUI': true, 'mapTypeId': 'terrain'}).bind('init', function(event, map) {
$('#map_canvas').gmap('getCurrentPosition', function(results, status) {
if ( status === 'OK' ) {
var position = new google.maps.LatLng(results.coords.latitude, results.coords.longitude);
var marker = $('#map_canvas').gmap('get', 'markers > client' );
if ( !marker ) {
$('#map_canvas').gmap('addMarker', { 'id': 'client', 'position': position, 'bounds': true });
} else {
marker.setPosition(position);
map.panTo(position);
}
} else if ( status === 'NOT_SUPPORTED' ) {
$('#map_canvas').gmap('addMarker', { 'id': 'client', 'position': $('#map_canvas').gmap('get', 'map').getCenter(), 'bounds': true });
}
});
$('#map_canvas').gmap('loadFusion', { 'query': { 'select': 'Geocodable address', 'from': 1260763 } } );
});
});

Resources