Hi everyone,
WARNING: long email.
tl;tr version:
I ported the Markerclusterer-Plus library (http://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclustererplus/examples/advanced_example.html) for google maps to Polymer.
The source can be found here: https://github.com/timeu/google-map-markerclusterer
The component page + demo can be found here: http://timeu.github.io/google-map-markerclusterer/components/google-map-markerclusterer/
Background:
I wanted to play around with polymer and web-components for some time now but there was always other work I had to do.
Recently I had to work on a visualization to display quantitive values on a pie chart on a google-map (output of a clustering analysis).
http://jsfiddle.net/timeu/kxMRQ/embedded/result/
The problem was, that we had data for 1000 markers and displaying that much pie charts was really slow.
So after some research I stumbled over Markerclusterer-Plus and after some tweaking it worked fine (Demo:http://jsfiddle.net/timeu/gr6qJ/embedded/result/)
However we wanted to replace the default cluster icons with custom ones (cluster icon should be a pie chart with an average of all the embedded pie charts). Markerclusterer-Plus didn’t provide a way to specify custom cluster icons so I was planning to extend the library and I thought instead of just extending the library I could also port it to Polymer.
This is my first polymer-element and although I tried to stick the official Polymer docs and best practices as closely as possible the code might not be perfect.
In the course of developing this Polymer element I ran into some questions and issues. Maybe some Polymer Devs could provide some feedback and maybe comment if I chose the right approaches:
Basic structure:
I decided to not just write a simple wrapper for the markerclusterer-plus js library. Instead I tried to map the markerclusterer-plus classes to corresponding polymer-elements.
The google-map-markerclusterer consists of 5 polymer-elements:
<google-map-markerclsuterer>: This is the main element that the user should use. It depends on the google-apis and google-map elements. <google-map-clustericon>: This is an internal polymer-element that handles some of the clustering functionality. <google-map-defaulticon>: This is an internal polymer-element that displays the default cluster icon<google-map-overlayview-marker>: This is an element that allows to use any overlay as a marker. <google-map-overlayview>: This is a polymer-element that wraps an OnverlayView of google maps (similar to the <google-map-marker>). <google-map-markerclusterer>, <google-map-clustericon> and <google-map-overlayview-marker> inherit from it. The original Markerclusterer-Plus library only supported simples markers but the polymer version should also support custom markers that are implemented via an OverlayView.
Here are some things I couldn’t get my head around:
1. To embed or not to embed the element inside of <google-map> ?
<google-map-markerclusterer> depends on the <google-map> element and specifically on the map attribute.
Currently the <google-map> element defines a content insertion point for markers so that you can write
<google-map><google-map-marker></google-map>
I could now extend google-map and change the template to allow to embed my <google-map-markerclusterer> but I wanted to avoid using inheritence too much. AFAIK best practice is to use composition instead of inheritence.
So I decided to put the <google-map-markerclusterer> as sibling to a <google-map>. Inside a parent polymer-element I could use data-binding to bind the map attribute (like I did in the demo):
<polymer-element name="my-app">
<template>
<google-map map="{{map}}" />
<google-map-markerclusterer map="{{map}}" />
<template>
</polymer-element>
2.) Best practice when dealing with multiple elements that depend on a third party library ?
google-map-markerclusterer as well as google-map depend on the google-api. Therefore I put the <google-api> element into the template of <google-map-markerclusterer> to make sure that google-api is loaded. I also have to wait for the googleApiLoaded event to be fired to initialize the OverlayView.
However google-map-markercluster can pratically only be used together with the <google-map> element. So does it really make sense to add the dependency to google-api to google-map-markerclusterer? I could use the approach to send events to sibling elements (described here: http://www.polymer-project.org/articles/communication.html#events) but that puts the burdon on the client of my element to make sure that the event is propogated to the google-map-markerclusterer.
Furthermore the base class of google-map-markerclusterer - google-map-overlayview - also depends on the the google-map-api to be loaded. Should I put the gogole-api element in both classes or is it enough to put it only intot he google-map-overlayview and use event bubling to handle the event in google-map-markerclusterer ?
In general if some internal elements depend on the external 3rd party library should there templates contain it or not ?
3.) How to programatically add child elements to an element?
Currently the google-map-markerclusterer defines a published markers attribute that has a Changewatcher attached (markersChanged()) which generates the clusters and adds them to the map. I create an array of markers or overlayview elements and then set the markers attribute.
However that doesn’t seem completely right to me because a marker is not a domain model but a custom/polymer-element. For example <google-map> uses a content insertion point for gogole-map-markers and a MutationObserver to handle the changes to the LightDOM.
Should I take the same approach? My feeling says yes, but with google-map-markerclusterer I don’t add one or two markers but potentially thousands. I could make a loop over my marker elements array and add them one by one to the LightDOM. Will my MutationObserver callback be executed on each iteration? That would kill performance (I could probably use this.async to delay the costly clustering function call).
Another alternative might be to use the repeat-binding together with a content insertion point inside of the template to add multiple markers (is that actually possible)?
What is common best practice for dynamically adding polymer-elements to other polymer-elements ? Should I allow both cases (attribute and LightDOM) or only one ? How to deal with adding multiple elements at once ?
4.) How to deal with configurable child elements ?
The default behavior of google-map-markerclusterer is to use the google-map-defaulticon element to display the cluster icon.
However I wanted to give the user the possibility to change this to a custom icon.
So I decided to add an insertion point and a default icon to the template.
<polymer-element name="google-map-markerclusterer">
<template>
<google-maps-api></google-maps-api>
<google-map-clustericon id="defaultclustericon"><google-map-defaulticon defaultStyles="{{styles}}" /></google-map-clustericon>
<content id="clustericontemplate" select="google-map-clustericon"></content>
<!-- markers is not used currently -->
<content id="markers"></content>
</template>
</polymer-element>
And in the attached function I do following:
attached: function() {
var clusterIconTemplateContent = this.$.clustericontemplate.getDistributedNodes();
if (clusterIconTemplateContent.length > 0) {
this.clusterIconTemplate_ = clusterIconTemplateContent[0];
}
else {
this.clusterIconTemplate_ = this.$.defaultclustericon;
}
},
This way if I only define the google-map-markerclusterer element it uses the defaultclustericon element in template but alternatively I can do something like this:
<google-map-markerclusterer>
<google-map-clustericon>
<my-custom-clustericon></my-custom-clustericon>
</google-map-clustericon>
</google-map-markerclusterer>
Is that the right approach? And what happens if have two insertion points (i.e. markers and a clustericontemplate) ?
5.) Issues with microtasks
The google-map-markerclusterers listens to the idle event of google-maps which is called whenever the map becomes idle after zooming or panning.
When the even is fired the google-map-markerclusterer will re-run the clustering, remove existing clusters and add the new clusters. To remove a marker or overlayview from a gogole-map, setMap(null) has to be called on the overlayview/marker.
Initially I used the changeWatcher (mapChanged) to handle this.
However I ran into a weird problem that if I use the mousewheel to quickly zoom in and out it can happen that the clusters are not properly removed.
This is probably due to the fact that the markers are removed from the array before the changeWatcher had time to call setMap(null). I tried to use this.async and Platform.flush() but nothing helped.
I ended up creating setters (setMap()) on my elements that directly set the map on the overlay/marker instead of using the changeWatchers.
Are there any best practices to cope with that case ?
Some additional remarks:
Data-binding in styles (Polyfill):
Data-binding inside of styles using the Polyfills doesn’t work (I think this is a known issue). Probably this won’t be fixed.
So far I have to define style changes twice:
:host {
position:absolute;
left:-{{iconOffset[1]}};
top:-{{iconOffset[0]}};
}
and then in the code to get to work with the polyfills.
this.style.left = "-"+this.iconOffset[1];
this.style.top = "-"+this.iconOffset[0];
There was also a weird issue that Chrome somehow didn’t properly display one element that was styled, although the styles were correct. When I manually unchecked one of the styles of that element in Chrome Dev Tools and then checked it again, the element was displayed correctly. As a workaround I added just set a constant style programtaically (this.$.text.style.position="absolute";) in my code.
Not sure if this is a bug
Binding and insertion points:
http://stackoverflow.com/questions/23522037/data-binding-between-nested-polymer-elements
Because I chose the approach to extract a configurable polymer-element (clustericon) from the insertion point and add it to google-map-markerclusterer I can’t relay on data-binding to pass in some settings (gridSize,etc).
As a workaround I set the attributes programatically (element.gridSize = this.gridSize).
Is there a better way ?
document.importNode(element,true) doesn’t copy instance variables ?
Could it be that document.importNode() doesn’t copy the instance variables ?
For example in the default setup the google-map-defaulticon is displayed with a standard style (defind as a static variables).
However the user can change pass a different style array to the google-map-markerclsuterer. When the user changes the style array it gets propogated to the <google-map-defaulticon> (placeholder) that is included in the template of <google-map-markerclusterer>. But when I then use document.importNode to clone this element the styles variable is null and the default style is applied again.
The current workaround is to use a static variable to solve this.
In general I must say I really enjoyed working with Polymer and I can see the huge potential (great work on that note!).
I hope I could convey the issues, I had clearly enough.
I also plan to write a blog-post on my experience porting markerclusterer-plus to polymer.
cheers
Uemit
3.) How to programatically add child elements to an element?
Currently the google-map-markerclusterer defines a published markers attribute that has a Changewatcher attached (
markersChanged()) which generates the clusters and adds them to the map. I create an array of markers or overlayview elements and then set the markers attribute.However that doesn’t seem completely right to me because a marker is not a domain model but a custom/polymer-element. For example
<google-map>uses a content insertion point for gogole-map-markers and a MutationObserver to handle the changes to the LightDOM.
Should I take the same approach? My feeling says yes, but with google-map-markerclusterer I don’t add one or two markers but potentially thousands. I could make a loop over my marker elements array and add them one by one to the LightDOM. Will my MutationObserver callback be executed on each iteration? That would kill performance (I could probably use this.async to delay the costly clustering function call).Another alternative might be to use the
repeat-bindingtogether with a content insertion point inside of the template to add multiple markers (is that actually possible)?
5.) Issues with microtasks
The google-map-markerclusterers listens to the idle event of google-maps which is called whenever the map becomes idle after zooming or panning.
When the even is fired the google-map-markerclusterer will re-run the clustering, remove existing clusters and add the new clusters. To remove a marker or overlayview from a gogole-map,setMap(null)has to be called on the overlayview/marker.
Initially I used the changeWatcher (mapChanged) to handle this.
However I ran into a weird problem that if I use the mousewheel to quickly zoom in and out it can happen that the clusters are not properly removed.
This is probably due to the fact that the markers are removed from the array before the changeWatcher had time to callsetMap(null). I tried to usethis.asyncandPlatform.flush()but nothing helped.
I ended up creating setters (setMap()) on my elements that directly set the map on the overlay/marker instead of using the changeWatchers.Are there any best practices to cope with that case ?
Some additional remarks:
Data-binding in styles (Polyfill):
Data-binding inside of styles using the Polyfills doesn’t work (I think this is a known issue). Probably this won’t be fixed.
So far I have to define style changes twice::host { position:absolute; left:-{{iconOffset[1]}}; top:-{{iconOffset[0]}}; }and then in the code to get to work with the polyfills.
this.style.left = "-"+this.iconOffset[1]; this.style.top = "-"+this.iconOffset[0];There was also a weird issue that Chrome somehow didn’t properly display one element that was styled, although the styles were correct. When I manually unchecked one of the styles of that element in Chrome Dev Tools and then checked it again, the element was displayed correctly. As a workaround I added just set a constant style programtaically (
this.$.text.style.position="absolute";) in my code.
Not sure if this is a bug
Binding and insertion points:
http://stackoverflow.com/questions/23522037/data-binding-between-nested-polymer-elements
Because I chose the approach to extract a configurable polymer-element (clustericon) from the insertion point and add it to google-map-markerclusterer I can’t relay on data-binding to pass in some settings (gridSize,etc).
As a workaround I set the attributes programatically (element.gridSize = this.gridSize).
Is there a better way ?document.importNode(element,true) doesn’t copy instance variables ?
Could it be that
document.importNode()doesn’t copy the instance variables ?For example in the default setup the google-map-defaulticon is displayed with a standard style (defind as a static variables).
However the user can change pass a different style array to the google-map-markerclsuterer. When the user changes the style array it gets propogated to the<google-map-defaulticon>(placeholder) that is included in the template of<google-map-markerclusterer>. But when I then usedocument.importNodeto clone this element the styles variable is null and the default style is applied again.
The current workaround is to use a static variable to solve this.In general I must say I really enjoyed working with Polymer and I can see the huge potential (great work on that note!).
I hope I could convey the issues, I had clearly enough.
I also plan to write a blog-post on my experience porting markerclusterer-plus to polymer.
Follow Polymer on Google+: plus.google.com/107187849809354688692
cheers
Uemit
---
You received this message because you are subscribed to the Google Groups "Polymer" group.
To unsubscribe from this group and stop receiving emails from it, send an email to polymer-dev...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/polymer-dev/64c5fa95-90c1-43c1-af15-49323d29e6ae%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.