I have a requirement to define some zones within a particular area using Polygons in Google Maps with the drawing library.
I pretty much have the framework in place but i have noticed that sometimes the order of my polygons is not as i'd expect.
I use a zIndex property of 999999999 for my first polygon and decrease this value as additional polygons are drawn with the idea being that the first zone is the smallest with other zones ranging out from this - and with this setup the smaller polygons should still be selectable.
My problem is that this works sometime but then draing some of the larger further out zones it overlays on top of my other polygons despite having a lower zIndex value
Can anyone see where i'm going wrong or if there is another way to do this because zIndex doesn't always sees to apply.
Heres a sample sahpe initialisation
google.maps.event.addListener(drawingManager, 'overlaycomplete', function(e) {
zVal = zVal-1000000;
if (e.type != google.maps.drawing.OverlayType.MARKER) {
// Switch back to non-drawing mode after drawing a shape.
drawingManager.setDrawingMode(null);
// Add an event listener that selects the newly-drawn shape when the user
// mouses down on it.
var newShape = e.overlay;
//Check to see if the shape has an id if not alert user to pick a zone
newShape.type = e.type;
newShape.id=curID;
newShape.zIndex=zVal;
$('#'+curID).attr("disabled", true);
google.maps.event.addListener(newShape, 'click', function() {
if(this.id =="" || this.id==null){
alert('no id');
}
setSelection(newShape);
});
setSelection(newShape);
clearSelection();
}
});
Heres a pic of the smaller zone being zone 1 and in this scenario its unselectable despite having the higher zIndex
The problem is how you set the zIndex:
newShape.zIndex=zVal;
Polygons are not HTMLElements, the zIndex is not a CSS-property, it's just a property to tell the API in which order the Polygons should be rendered.
The Polygons will be rendered as objects in a <canvas/>, CSS may not be used for these objects, it simply matters when the objects will be drawn.
To be able to draw the Polygon in the correct order the API needs to know when the zIndex-property of a Polygon changes(then the API must re-calculate the order of all Polygons and redraw them).
But when you set the zIndex-property in the way you do it, the API will not realize the change.
Solution:
use the setter-method of MVCObject to set the zIndex:
newShape.set('zIndex',zVal);
the API now automatically will be notified about the property-change and set the order of the Polygons
Related
This question is directed to Leaflet users (and those who use the Leaflet.draw plugin)...
I'm using Leaflet and would like to allow my user to draw 1--and only 1--single polygon over any area of the map. I would also like to limit the size of that polygon in some way (such as limiting the length of the side for a square or the area covered it covers--preferably specified in degrees so that the set size limits would translate regardless of the zoom level).
My end goal is simply to extract the coordinates of the 4 square vertices or the coordinates covered by the polygon area.
That said, I found the Leaflet.Draw plugin. It is fantastic, however, I need to limit its functionality to my requirements (only 1 polygon drawn at a time and, in particular, the size cannot be drawn too large). Is this possible to do? If so, how?
Regardless of if it is or is not possible, is there a better way to go about doing this?
Can I propose another solution to this issue?
I would limit the number of polygons to one by doing the following:
map.on('draw:created', function (e) {
var layer = e.layer;
if(drawnItems && drawnItems.getLayers().length!==0){
drawnItems.clearLayers();
}
drawnItems.addLayer(layer);
});
I am listening to the draw:created event and determine if there is already a marker. If there is, I remove that marker and place my new one in the desired location. Therefore, one less click for user as they no longer need to delete the previous and one marker rule is always enforced.
If you wanted to allow more than one marker you could do a FIFO delete of the oldest layer.
If you do not want to automatically delete a layer, you could either prompt the user or ignore the request.
That said, I found the Leaflet.Draw plugin. It is fantastic, however, I need to limit its functionality to my requirements (only 1 polygon drawn at a time and, in particular, the size cannot be drawn too large). Is this possible to do? If so, how?
I think you'll need to code it yourself.
I see two possibities:
hacking the draw plugin (writing your own code inside the plugin)
extending the L.Draw.Polygon class from the draw plugin (see the docs about OOP in Leaflet) to create a costum one
1 is faster, 2 is cleaner. You'll have to choose depending on the size of your project.
I did it without hacking the Leaflet Draw source.
After the controls are added to the map, I place a hidden div inside the controls. Then when a polygon is created I display that div. I used CSS to absolute position it over the controls so the buttons are then "disabled" and CSS to make the buttons look faded. If the polygon is deleted then I hide that div.
Not the best solution, but I works without having to edit the source.
After drawControl is added, I add the hidden div:
$('.leaflet-draw-section:first').append('<div class="leaflet-draw-inner-toolbar" title="Polygon already added"></div>');
Here's the JS to toggle them:
map.on('draw:created', function (e) {
var type = e.layerType,
layer = e.layer;
// keep the polygon on the map
drawnItems.addLayer(layer);
// disable the create polygon tools
$('.leaflet-draw-inner-toolbar').show();
});
map.on('draw:deleted', function(e) {
// enable the create polygon tools
$('.leaflet-draw-inner-toolbar').hide();
});
Here's the CSS:
.leaflet-draw-inner-toolbar {
background: none repeat scroll 0 0 rgba(255, 255, 255, 0.6);
bottom: 0;
display: none;
left: 0;
position: absolute;
right: 0;
top: 0;
}
I am trying to set up OpenLayers to not display the vector layer just before a zoom starts and make it reappear after a zoom ends. I have the zoom ends part already established like this:
map = new OpenLayers.Map('map_element', { eventListeners: { "zoomend": mapEvent}});
function mapEvent(event) {
if(event.type == "zoomend") {
hide_vector_layer();
}
}
But I don't see any kind of event listener for the start of a zoom in the documentation. There is a "movestart" which covers moving, panning, and zoom. Unfortunately, I can't use the "movestart" one, because I don't want the layer to disappear during a pan. You would think there would be a "zoomstart", as there is a "zoomend".
The reason I am trying to do this, is because I don't like how the vector layer zooms at a different rate when using Google Maps as a base layer. It looks wrong, looks like all the features are inaccurate, even though they land in the right place after the zoom is complete.
Any suggestions?
Here is a easy to add the 'BeforeZoom' event to the OpenLayers . Just add the code below to where you created your map object.
map.zoomToProxy = map.zoomTo;
map.zoomTo = function (zoom,xy){
//Your Before Zoom Actions
//If you want zoom to go through call
map.zoomToProxy(zoom,xy);
//else do nothing and map wont zoom
};
How this works:
For any kind of zooming activity, OpenLayers API ultimately calls the function called zoomTo. So before overriding it, we copy that function to a new function called 'zoomToProxy'. The we override it and add our conditional zoom logic. If we want the zoom to happen we just call new proxy function :)
For this purpose you should override moveTo and moveByPx methods of OpenLayers.Map for eliminate movestart event triggering for any actions except zooming.
I had the same problem that OP had, and I tried to solve it with drnextgis's solution. But unfortunately it didn't completely work:: the zoomChanged property in OpenLayers.Map.moveTo evaluates to true not only when the zoom level has changed, but also when the map has been resized.
My map was 100% of the user's browser window, so if they resized the window, the event would be triggered. This was undesirable for me, as I only wanted to trigger the event if the zoom level had actually changed. My solution was to create an new event, called "zoomstart", which I inserted at the top of OpenLayers.Map.moveTo. Here's the code:
var getZoom = this.getZoom();
if ( !!getZoom && !!zoom && this.isValidZoomLevel(zoom) && getZoom != zoom )
this.events.triggerEvent("zoomstart", zoom);
This code will pass the new zoom level to an event listener that is registered to zoomstart, and in my case I determine the map's restrictedExtent and do other stuff based upon the new zoom level.
Peace be with ye.
"movestart" handles "zoomstart". To detect if the zoomstart, try:
map.events.register("movestart",map, function(e) {
if(e.zoomChanged)
{
//zoom start code here
}
});
Solution of "Shaunak" is worked very well for me.
I want to restrict zooming below 11 so edited his code as
if (zoom > 11) {
map.zoomToProxy(zoom, xy);
}
I'm using Polymaps.js library for making maps.
It would be very useful to be able to observe zoom level changes on layers. I'm trying to keep a layer of points scaled by values in meters, so I need to recalculate the radii whenever the zoom level changes.
I'm using the on "move" function, which I think is triggered on zooms, too. There may be on on "zoom" function, too. I think that there is.
For example:
// whenever the map moves...
map.on("move", function() {
// get the current zoom
var z = map.zoom();
// show/hide parcels
parcels.visible(z >= 16);
});
Ok, here is my problem, I'll put a picture to illustrate it easier.
I need the user to draw some polygons, representing the coverage area.
The polygon needs to have fixed number of points (vertex) because it goes into a processing algorithm later, and it would be really slow if a polygon can contain a lot of points.
Anyway, in my example lets stick to hexagons (6 points).
The user need to be able to drag the polygon around and modify it, but not change the number of points.
I tried setting the editable: true option for the polygon, it works fine, but it gives me the situation shown on the picture. It creates a handle for every point, and another handle (semi-transparent) in the middle between each points. Now, if the user moves that semi-transparent point, it will add another point (vertex) to the polygon, and add additional two handles in the middle of newly created lines. That gives us a 7 point polygon.
The best option would be to remove those semi-transparent handles, so the user can only drag polygon points, and it that way he can't affect the total number of points.
Can I achieve this using google maps editable option?
Another way to achieve what you want is to forego the built-in edit-ability of the polygon and implement it yourself. This is more powerful and flexible.
First, don't make the polygon editable. Next, make a Marker for each corner of the polygon. Finally, make each marker draggable and an event listener on it's "drag" event to update the polygon.
Making the markers and adding the event listener:
for (var i=0; i<coordinates.length; i++){
marker_options.position = coordinates[i];
var point = new google.maps.Marker(marker_options);
google.maps.event.addListener(point, "drag", update_polygon_closure(polygon, i));
}
Where update_polygon_closure is defined as:
function update_polygon_closure(polygon, i){
return function(event){
polygon.getPath().setAt(i, event.latLng);
}
}
Full code is in a jsfiddle here: https://jsfiddle.net/3L140cg3/16/
Since no one seems to have a better solution, I'm marking my workaround as accepted, in case someone stumbles upon the same problem. Not very pretty, but gets the job done
The only solution I found so far is to hide the handles manually after the polygon has been drawn. The problem here is that the handles don't have any CSS class or id, so I have to hide all divs with opacity 0.5 (opacity of the handles). It works, but it is pretty risky, considering that something else might have the same opacity and doesn't need to be hidden.
// variables
var map, path, color;
polygon = new google.maps.Polygon({
paths: path,
strokeColor: color,
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: color,
fillOpacity: 0.10,
editable: true,
});
polygon.setMap(map);
setTimeout(function(){ map.find('div[style*=opacity: 0.5]').hide(); }, 50);
As a slight improvement to #zolakt's answer, you can both hide the midpoint divs on the polygon and add a mousedown event listener to track when a midpoint is clicked to prevent dragging and changing the polygon:
// hide the midpoints (note that users can still click where the hidden midpoint
// divs are and drag to edit the polygon
$('#multi_markers div[style*="opacity: 0.5"]').hide();
// get the paths for the current polygon
var octopusPaths = HotelLib.octopusPolygon.getPaths();
// track when a polygon midpoint is clicked on
google.maps.event.addListener(HotelLib.octopusPolygon, 'mousedown', function(mdEvent) {
// if a midpoint is clicked on, mdEvent.edge will have an integer value
if(mdEvent.edge || (mdEvent.edge == 0)){
// immediately reset the polygon to its former paths
// effectively disabling the drag to edit functionality
HotelLib.octopusPolygon.setPaths(octopusPaths);
// hide the midpoints again since re-setting the polygon paths
// will show the midpoints
$('#multi_markers div[style*="opacity: 0.5"]').hide();
}
});
I just created an alternative solution to this without having to fiddle around with setTimeout or the polyline creation. This is also a somewhat global solution, so you can basically drop it in any established program that uses Google Maps.
We'll use MutationObserver to observe when those midpoint nodes appear on the DOM and then instantly hide them. They should start appearing when something is set as editable.
Basically just put this anywhere after the map is initialized:
var editMidpointNodeObserver = new MutationObserver(function(list, observer)
{
if($('#mapwrapper div[style*="opacity: 0.5"]').parent('div[style*="cursor: pointer"]').length > 0)
{
$('#mapwrapper div[style*="opacity: 0.5"]').parent('div[style*="cursor: pointer"]').remove();
}
});
editMidpointNodeObserver.observe($('#mapwrapper')[0], { childList: true, subtree: true });
Change the #mapwrapper to whatever the id of your Google Maps wrapper element is. I am using jQuery here, so therefore the $('#mapwrapper')[0] to convert jQuery object to a native DOM object. Should work without jQuery as well, I am assuming you know how to convert this to vanilla js.
We also just straight up remove the nodes, so no need to worry about user being able to click invisible ones by accident or otherwise.
MutationObserver should be supported in all browsers: https://caniuse.com/mutationobserver
I have this map, which I show some red markers over and whenever a location is chosen from a list the current marker is painted blue and the map centers around it.
I achieve this by having 2 layers - one for the red markers which is drawn at the beginning and one which is redrawn whenever a point is chosen from the list.
I would like to define that the red marker layer will always appear above the blue marker layer. Effectively hiding the "current marker" indication. (The reason for this is complicated)
This link is to a page that works the way I don't want. The blue layer is on top of the red layer.
I tried to reverse the order by defining the graphicZIndex property for both the vector and in the layers.addFeature function.
I'm obviously doing something wrong and maybe someone can point me to what it is.
The way I define the z-axis:
currentPointLayer = new OpenLayers.Layer.Vector("Selected Point Layer", {
style : {
externalGraphic : 'marker-blue.png',
graphicHeight : 15,
graphicWidth : 15,
graphicZIndex : 1
},
rendererOptions: { zIndexing: true }
});
Again, I want to hide the blue marker behind the red markers layer.
You can either change the order of your layers as ilia choly stated.
Or, if you want to use zIndexing, you have to put all features into one layer, because zIndexing is only done within a single layer.
Have a look at this simple example about styling, that also uses zIndexing. It randomly creates some points in the map. If you zoom out, chances are good that two circles overlap and if you hoover over one, it will be highlighted and put on top.
So you want highlight a marker with different color whenever a point is selected? Managing it with 2 layers is really an overkill. You should be able to define a vector layer with style like this:
var currentPointLayer = new OpenLayers.Layer.Vector("Selected Point Layer", {
styleMap: new OpenLayers.StyleMap({
externalGraphic : '${markerUrl}',
pointRadius: 20
})});
map.addLayer(currentPointLayer);
Then you have to set attribute 'markerUrl' of every feature(i.e. feature.attributes.markerUrl) to 'marker-red.png' - that would be initial state of all features.
Then whenever feature is selected you change markerUrl attribute of selected feature to 'marker-blue.png' and(important) call
currentPointLayer.redraw();
Obviously you'll also have to set previously selected feature to 'marker-red.png' when new feature is selected.
in your init_map() function you're adding the red marker layer before the blue ones. Try switching them.
preparePointsOnMap(map, points); //red marker
if (!map.getCenter()) {
map_set_center(lon, lat, mZoom); //blue marker
}