I've a layout template with a left sidebar where I show information of Location passed entities as an array.
Also in this template, I show a object Map with all of this locations.
I want to do click on a Location of my sidebar and then on the same template show another object Map replacing the previous with information of this Location. Keeping the sidebar with the information and not doing new queries on the database.
How to achieve this?
Ajax? Conditional layout?
I read this article but I don't understand how to solved my problem: http://twig.sensiolabs.org/doc/recipes.html#overriding-a-template-that-also-extends-itself
PD: I'm using Twig template and Symfony2
You could have a separate template for printing object map and, as you guessed, this would have to be done using AJAX. You would pass the data you want to show on map (not id as you don't want to query database again) and the controller would return formatted HTML.
However, this seems to me a bit overkill. I would always consider doing JS (with optional framework) in order to swap the content of sidebar with Map object.
It really depends on which map API do you use. If it could be controlled via JS I would look no further. It it could not, well then, AJAX is your natural choice....
UPDATE:
OK, what you should do is create initial Map object that will be modified later on:
var theMap = null;
function initializeMap(){
var mapOptions = {
center: new google.maps.LatLng(some_latitude, some_longitude),
zoom: 8, // goes from 0 to 18
mapTypeId: google.maps.MapTypeId.ROADMAP
};
theMap = new google.maps.Map(document.getElementById("map_canvas"),mapOptions);
// you must have some element with ID = 'map_canvas'
}
some_latitude and some_longitude are fairly unimportant as you will most likely set new coordinates in a few moments.
Now assuming (but not crucial at all) that you use some of the JS frameworks (I prefer jQuery) you could bind click event to those location links:
var onlyMarker = null;
$('.location').click(function(){
var $t = $(this);
var newLatLang = new google.maps.LatLng($t.attr('data-lat') ,$t.attr('data-lng'));
if ( onlyMarker == null ) {
onlyMarker = new google.maps.Marker({
position: newLatLang
map: theMap,
title: $t.attr('title')
});
}else{
onlyMarker.setPosition(newLatLang);
}
});
Now, relying on HTML5's 'data-*' attibutes is not good idea in particular as if you use any other version lower you will most likely end-up with invalid markup. The workaround is to for link (<a>) to carry id/key to LatLng object, for example:
// initially generated from `AJAX` or in `Twig` loop
var allLatlangs = [
new google.maps.LatLngf(some_latitude, some_longitude),
new google.maps.LatLngf(some_latitude, some_longitude),
new google.maps.LatLngf(some_latitude, some_longitude),
];
$('.location').click(function(){
var id = $(this).attr('id');
var newLatLang = allLatLang(id);
//....
// everything else is the same
// ....
});
Don't forget to include Maps API with proper API key:
https://maps.googleapis.com/maps/api/js?key=API_KEY_HERE&sensor=true
In order to obtain valid API key follow this link: API KEY HOW-TO
This link basically covers key steps that I have described here so study it and it should all come together nicely.
Also, if you're unsure how to retrieve some things from Maps you should consult reference:
REFERENCE which has every method call described pretty good
Remember not to execute any of this code until everything is being loaded.
Hope this helped a bit :)
Related
In my openlayers 5 (based on a angular 6 app), I am implementing a functionality where you search for something, query the db, the db brings back some geoJSON and I render this geoJSON data in a ol vector layer.
There are two different ways to search, so there are two different forms that bring back geoJSOn to the same ol vector.
Of course, before rendering the data, I have to clear out the layer.
This is my code
ngOnInit() {//initialize some params and the ol map
//bring results-as you type - pure angular
this.results = this.myForm.valueChanges.pipe(
switchMap( formdata => this.mapcmsService.searchName(formdata.name, formdata.cepDrop))
);//pipe
this.tilesource = new OlXYZ({
url:'http://tile.stamen.com/terrain/{z}/{x}/{y}.jpg'
});
this.tilelayer = new OlTileLayer({
source: this.tilesource
});
this.vectorsource = new VectorSource({});
this.vectorlayer = new VectorLayer({
source: this.vectorsource
});
this.view = new OlView({
center: OlProj.fromLonLat([6.661594, 50.433237]),
zoom: 2,
});
this.olmap = new OlMap({
target: 'map',
layers: [this.tilelayer,this.vectorlayer],
view: this.view,
projection: 'EPSG:3857'
});
const selectClick = new Select({
condition: click,
layers:[this.vectorlayer]
});
this.olmap.addInteraction(selectClick);
selectClick.on(
'select', ()=>{
const values = selectClick.getFeatures().item(0).values_;
this.getDetails(values.id);
}
);
} //closes ngOnInit
Outside the ngOnInit, after the initialization,there are the two different functions that bring geoJSON to the same ol vector layer. They basically do the same thing.
searchById(id){
this.map_loading = true;
this.myService.getById(id).subscribe((data) =>{
this.vectorsource.refresh();
this.vectorsource.clear();
const fff = (new GeoJSON()).readFeatures(data.data);
this.vectorsource.addFeatures(fff);
this.map_loading = false;
})
}//searchById
and
searchCategories(){
this.map_loading = true;
this.myService.categoriesSearch(this.categoriesForm.value).subscribe((data) =>{
this.vectorsource.refresh();
this.vectorsource.clear();
const fff = (new GeoJSON()).readFeatures(data.data);
this.vectorsource.addFeatures(fff);
this.map_loading = false;
})
}//searchCategories
The problem is that the ol vector source is not always cleared before new features are added. I search for something, features are rendered. I search again , and sometimes, the old features remain on the map, along with the new ones.
I did a silly move to add refresh with clean and nothing is fixed. This is not standard, eg every other search. This randomly happen and I dont have a clue how to debug it. Please advice
Thanks
Is there an unique id for each feature?
I had the same problem that features were loaded constantly. I used the bbox-strategy and every time I moved the map, it loaded all the features in the extent, even if they were already there.
I had to set an unique id in the data for my features, so OpenLayers can refer to the existing ones if you load new ones. This randomness maybe comes through the generated ids for the features, that are sometimes equal to the new ones and sometimes not.
Dont know if that faces your problem, it just flew into my brain while I read that.
How can we re-create template while switching routes?
For example, i have subscriber template. It detects when user scrolls down to a display and subscribes to more data. It takes several parameters.
Example:
amazing_page.html
{{#each}}
{{amazing_topic}}
{{/each}}
{{>subscriber name='topics' count=5}}
subscriber.js
//rough sample code
Template.subscriber.onCreated(function() {
var self = this;
var type = Template.currentData().name;
var count = Template.currentData().count;
var user = Template.currentData().user;
var skipCount = 0;
self.autorun(function(c){
self.subscribe(type, skipCount, user);
var block = true;
$(window).scroll(function() {
if (($(window).scrollTop() + $(window).height()) >= ($(document).height()) && block) {
block = false;
skipCount = skipCount + count;
console.log(type);
console.log(skipCount);
self.subscribe(type, skipCount, user, {
onReady: function() {
block = true;
},
onStop: function() {
console.log('stopped');
}
});
}
});
})
});
I use this template with different parameters in different routes.
The problem is if user switches some routes, and scrolls down in one page, all subscribers he gets in another pages will actualy work in this page. More, they will store increased values for them variables, and will do all included logic.
I found a bad decision when we use Route.getName (for example) comparing and name parameter of subscriber. It is not a best option. Can someone help me to find a good practice for that?:)
Simple Example:
We have 3 different routes:
1)News
2)Videos
3)Topics
These routes templates have included special subscriber-templates. And subscribtion works fine on scroll.
Ok, now let's visit all of them: News, Videos, Topics.
Good, now scroll down and... I have three instance of subscriber template what will subscribe on them own publications, because they not destroyed when we switch routes.
And, as a result - when user scrolling Topics page, he will call subscribtion for News and Videos too, and he will take data from these collections too;)
And - this is a problem:)
UPD:
Looks like we find a decision. If i use Template.instance (autorun/subscribe) it will start working expected, except some strange cases:)
First of all, when i go in another route in next iteration (scroll down) it returns me data from old, destroyed template + error. Next time (next iteration) it will start to subscribe to a correct data. Hmm...it looks like i have mistake in autorun section...or not?
Attached print screen from console
this
It sounds like you have multiple subscriptions to the same collection and that therefore the list of documents shown in various contexts can change in unexpected ways. Meteor manages multiple subscriptions on the same collection by synchronizing the union of the selected documents.
The simplest way to manage each of your views is to make sure that the data context for a particular view uses a .find() with the query you need. This will typically be the same query that your publication is using.
A different but less efficient approach is to .stop() the subscription when you leave a view.
When using Google Places API, what is the best way of hiding the map itself ?
We only need the search auto-complete, and our users don't need to see the map itself.
https://developers.google.com/maps/documentation/javascript/places?authuser=2#place_search_requests
Got the answer from https://developers.google.com/maps/documentation/javascript/places-autocomplete?authuser=2#video
Basically the Autocomplete constructor has a constructor overload
= new google.maps.places.Autocomplete( htmlInput, autoCompleteOptions )
var autocomplete = new google.maps.places.Autocomplete(input, { bounds: defaultBounds });
And this is not violating the TOS at all, reading the https://developers.google.com/maps/documentation/javascript/places-autocomplete?authuser=2#video above.
I'm in the process of learning meteor. I followed the tutorial to create microscope. If some one submits a post meteor will re render the template for all users. This could be very annoying if there are hundreds of posts then the user will come back to the top of the page and loose track of where he was. I want to implement something similar to what facebook has. When a new post is submitted template isn't rendered rather, a button or link will appear. Clicking it will cause the template to re-render and show the new posts.
I was thinking of using observeChanges on the collection to detect any changes and it does stop the page from showing new posts but only way to show them is to reload the page.
Meteor.publish('posts', function(options) {
var self = this, postHandle = null;
var initializing = true;
postHandle = Posts.find({}, options).observeChanges({
added: function(id, post) {
if (initializing){
self.added('posts', id, post);
}
},
changed: function(id, fields) {
self.changed('posts', id, fields);
}
});
self.ready();
initializing = false;
self.onStop(function() { postHandle.stop(); });
});
Is this the right path to take? If yes, how do I alert the user of new posts? Else, what would be a better way to implement this?
Thank you
This is a tricky question but also valuable as it pertains to a design pattern that is applicable in many instances. One of the key aspects is wanting to know that there is new data but not wanting to show it (yet) to the user. We can also assume that when the user does want to see the data, they probably don't want to wait for it to be loaded into the client (just like Facebook). This means that the client still needs to cache the data as it arrives, just not display it immediately.
Therefore, you probably don't want to restrict the data displayed in the publication - because this won't send the data to the client. Rather, you want to send all the (relevant) data to the client and cache it there until it is ready.
The easiest way involves having a timestamp in your data to work from. You can then couple this with a Reactive Variable to only add new documents to your displayed set when that Reactive Variable changes. Something like this (code will probably be in different files):
// Within the template where you want to show your data
Template.myTemplate.onCreated(function() {
var self = this;
var options = null; // Define non-time options
// Subscribe to the data so everything is loaded into the client
// Include relevant options to limit data but exclude timestamps
self.subscribe("posts", options);
// Create and initialise a reactive variable with the current date
self.loadedTime = new ReactiveVar(new Date());
// Create a reactive variable to see when new data is available
// Create an autorun for whenever the subscription changes ready() state
// Ignore the first run as ready() should be false
// Subsequent false values indicate new data is arriving
self.newData = new ReactiveVar(false);
self.autorun(function(computation) {
if(!computation.firstRun) {
if(!self.subscriptionsReady()) {
self.newData.set(true);
}
}
});
});
// Fetch the relevant data from that subscribed (cached) within the client
// Assume this will be within the template helper
// Use the value (get()) of the Reactive Variable
Template.myTemplate.helpers({
displayedPosts = function() {
return Posts.find({timestamp: {$lt: Template.instance().loadedTime.get()}});
},
// Second helper to determine whether or not new data is available
// Can be used in the template to notify the user
newData = function() {
return Template.instance().newData.get();
});
// Update the Reactive Variable to the current time
// Assume this takes place within the template helper
// Assume you have button (or similar) with a "reload" class
Template.myTemplate.events({
'click .reLoad' = function(event, template) {
template.loadedTime.set(new Date());
}
});
I think this is the simplest pattern to cover all of the points you raise. It gets more complicated if you don't have a timestamp, you have multiple subscriptions (then need to use the subscription handles) etc. Hope this helps!
As Duncan said in his answer, ReactiveVar is the way to go. I've actually implemented a simple facebook feed page with meteor where I display the public posts from a certain page. I use infinite scroll to keep adding posts to the bottom of the page and store them in a ReactiveVar. Check the sources on github here and the live demo here. Hope it helps!
I am a big fan of gmaps4rails gem and it has been working well and very flexible.
However, I cannot seem to clear and update the Cluster marker easily and I cannot find similar solutions on Stackoverflow that is using this gem.
In my javascript, I am using the following methods to filter out visible markers. But Gmaps.map.clearClusterer doesn't seem to respond to click event. I am able to show and hide markers properly but not able to clear cluster markers nor update the cluster markers with new number. What should I do differently?
var VisibleMarker=function(){
var filtered=_.filter(Gmaps.map.markers,function(marker){
return marker.building_type == SELECT;
}); //end of _.filter
return filtered
}//end of VisibleMarker
var applyFilters=function(){
var VisibleArray=VisibleMarker()
_.each(Gmaps.map.markers,function(marker){
Gmaps.map.hideMarker(marker)
})
Gmaps.map.clearClusterer()
_.each(VisibleArray,function(marker){
Gmaps.map.showMarker(marker)
})
Gmaps.map.createClusterer(VisibleArray)
};//end of applyFilters
UPDATE:
How should I properly call the addMarkers() method in the MarkerClusterer Class?
http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/docs/reference.html
I used this line:
Gmaps.map.addMarkers(VisibleArray)
But it calls this function instead:
Gmaps4Rails.prototype.addMarkers = function(new_markers) {
this.markers = this.markers.concat(new_markers);
this.create_markers();
return this.adjustMapToBounds();
};