Leaflet.draw mapping: How to initiate the draw function without toolbar? - javascript

For anyone experienced with leaflet or leaflet.draw plugin:
I want to initiate drawing a polygon without using the toolbar from leaflet.draw. I've managed to find the property that allows editing without using the toolbar (layer.editing.enable();) by searching online (it's not in the main documentation). I can't seem to find how to begin drawing a polygon without the toolbar button. Any help would be much appreciated!
Thank you :)

This simple code works for me:
new L.Draw.Polyline(map, drawControl.options.polyline).enable();
Just put it into the onclick handler of your custom button (or wherever you want).
The variables map and drawControl are references to your leaflet map and draw control.
Diving into the source code (leaflet.draw-src.js) you can find the functions to draw the other elements and to edit or delete them.
new L.Draw.Polygon(map, drawControl.options.polygon).enable()
new L.Draw.Rectangle(map, drawControl.options.rectangle).enable()
new L.Draw.Circle(map, drawControl.options.circle).enable()
new L.Draw.Marker(map, drawControl.options.marker).enable()
new L.EditToolbar.Edit(map, {
featureGroup: drawControl.options.featureGroup,
selectedPathOptions: drawControl.options.edit.selectedPathOptions
})
new L.EditToolbar.Delete(map, {
featureGroup: drawControl.options.featureGroup
})
I hope this will be useful for you too.
EDIT:
The L.EditToolbar.Edit and L.EditToolbar.Delete classes expose the following useful methods:
enable(): to start edit/delete mode
disable(): to return to standard mode
save(): to save changes (it fires draw:edited / draw:deleted events)
revertLayers(): to undo changes

I think it's worth mentioning Jacob Toyes answer to this problem. You're always drawing with handlers in leaflet.draw - not directly with layers. If you want to edit a layer, you use the handler saved in a layers editing field like that: layer.editing.enable();. And if you want to create a new layer, you first create a new handler:
// Define you draw handler somewhere where you click handler can access it. N.B. pass any draw options into the handler
var polygonDrawer = new L.Draw.Polyline(map);
// Assumming you have a Leaflet map accessible
map.on('draw:created', function (e) {
var type = e.layerType,
layer = e.layer;
// Do whatever you want with the layer.
// e.type will be the type of layer that has been draw (polyline, marker, polygon, rectangle, circle)
// E.g. add it to the map
layer.addTo(map);
});
// Click handler for you button to start drawing polygons
$('#draw_poly').click(function() {
polygonDrawer.enable();
});
By now there actually is an example on the leaflet.draw github page: https://github.com/Leaflet/Leaflet.draw/blob/develop/docs/examples/edithandlers.html
Nevertheless I think handlers aren't well documented there yet.
Like stated above, L.EditToolbar.Edit and L.EditToolbar.Delete expose interesting methods and events like editstart and editstop. What's not mentioned is that these two classes themselves are derived from L.Handler.

So I've figured this out for circles, but it should be the same for polygons. It's actually really simple. Hopefully the following code answers your question, but if not let me know and I can post more to a gist or something.
// Creates the circle on the map for the given latLng and Radius
// If the createdWithAddress flag is true, the circle will not update
// it's address according to its position.
createCircle: function(latLng, radius, createdWithAddress) {
if (!this.circle) {
var self = this,
centerIcon,
centerMarker;
centerIcon = new L.Icon({
iconUrl: '/assets/location_pin_24px.png',
iconSize: [24, 24],
iconAnchor: [12, 24],
shadowUrl: '/assets/marker-shadow.png',
shadowSize: [20, 20],
shadowAnchor:[6, 20]
})
// Setup the options for the circle -> Override icons, immediately editable
options = {
stroke: true,
color: '#333333',
opacity: 1.0,
weight: 4,
fillColor: '#FFFFFF',
moveIcon: centerIcon,
resizeIcon: new L.Icon({
iconUrl: '/assets/radius_handle_18px.png',
iconSize: [12, 12],
iconAnchor: [0,0]
})
}
if (someConfigVarYouDontNeedToKnow) {
options.editable = false
centerMarker = new L.Marker(latLng, { icon:centerIcon })
} else {
options.editable = true
}
// Create our location circle
// NOTE: I believe I had to modify Leaflet or Leaflet.draw to allow for passing in
// options, but you can make it editable with circle.editing.enable()
this.circle = L.circle([latLng.lat, latLng.lng], radius, options)
// Add event handlers to update the location
this.circle.on('add', function() {
if (!createdWithAddress) {
self.reverseGeocode(this.getLatLng())
}
self.updateCircleLocation(this.getLatLng(), this.getRadius())
self.updateMapView()
})
this.circle.on('edit', function() {
if (self.convertLatLngToString(this.getLatLng()) !== self.getLocationLatLng()) {
self.reverseGeocode(this.getLatLng())
}
self.updateCircleLocation(this.getLatLng(), this.getRadius())
self.updateMapView()
})
this.map.addLayer(this.circle)
if (centerMarker) {
centerMarker.addTo(this.map)
this.circle.redraw()
centerMarker.update()
}
}
},
Sorry a lot of that is just noise, but it should give you an idea of how to go about this. You can control editing like you said with editing.enable()/.disable().
Make sure to comment with any questions. Good luck man.

While BaCH's solution is propably the best, I would like to add a one-liner solution which is actually more future-proof (than digging into undocumented Leaflet Draw methods) and the simplest.
document.querySelector('.leaflet-draw-draw-polygon').click();
That's it.
You just take andvantage of the existance of the toolbar but actually, you do not use it. You can initiate drawing programmatically in any way. And you can also hide the toolbar using CSS.
In case you want to initiate drawing of a different shape, use one of the following classes:
.leaflet-draw-draw-polyline
.leaflet-draw-draw-rectangle
.leaflet-draw-draw-circle
.leaflet-draw-draw-marker
.leaflet-draw-draw-circlemarker

Related

Leaflet Markercluster - write persistent data to each marker

I have a map implemented via leaflet and there is a button to send the bounds of the map to a server which in turn responds to me with an array of elements. Each element has a latitude, a longitude and a unique id.
What I get from the server looks roughly like this:
[{
"poi_marker": "redMarker.png",
"poi_id": 412, //this is my unique ID
"poi_pos": {
"lat": 17.53243,
"lng": 14.52353
}
},...]
So far my code looked like this:
function createAndAddMarker(prefill) {
let marker = createMarker(prefill.poi_pos, prefill.poi_marker);
map.addLayer(marker);
marker._icon.dataset.uniqueId = prefill.poi_id;
return marker;
}
function createMarker(coords, iconUrl) {
let redMarker = L.icon({
iconUrl: iconUrl,
iconSize: [20, 32], // size of the icon
iconAnchor: [10, 32], // point of the icon which will correspond to marker's location
});
let createdMarker = new L.marker(coords, {
draggable: true,
zIndexOffset: 101,
icon: redMarker
});
return createdMarker;
}
and that worked fine! Now that I tried to implement leaflet.markercluster the first function looks pretty similar:
function createAndAddMarker(prefill) {
let marker = createMarker(prefill.poi_pos, prefill.poi_marker);
clusterLayer.addLayer(marker); //clusterLayer = layer that got created & added a when map did
marker._icon.dataset.uniqueId = prefill.poi_id;
return marker;
}
but there is already 2 problems with it.
Not all the markers get actually drawn to the map because some are already clustered. For these items that don't get drawn, the line marker._icon.dataset.uniqueId = prefill.poi_id will throw an error as it can not find "marker._icon" because the marker doesn't seem to have an icon as long as it's not drawn.
The markers that get drawn get the uniqueId written onto them, but if I zoom out and make them cluster up too, the original marker+icon gets removed from the DOM and readded when I zoom in again. Unfortunately, it get's readded without my unique-id, so I have no idea how to persistenly link the marker with the unique Id.
I need to get corresponding unique-ID to a marker, every time I click on it, but have no idea how to achieve this.
Your input is very much appreciated!
If something is unclear pls let me know and I will attempt to clarify as much as possible.
Thanx in advance!
As you have very well spotted, the Leaflet Marker icon may be removed from DOM / recreated from scratch when Leaflet.markercluster plugin needs to hide the Marker (e.g. if it is clustered or far away from the view port).
Hence attaching data to that icon is troublesome.
But you have several other possibilities to attach your data to the Marker instead, as described in Leaflet: Including metadata with CircleMarkers:
directly to the Marker object
within the options when creating the Marker
to the Marker Feature properties
You do not necessarily need to use a DOM object's dataset to attach your JavaScript data.

mapbox gl js: use intermediate rendering step as mask for custom layer

my objective is to have a mapbox map with a custom layer overlaid on it. that custom layer should be masked by an other mapbox map.
usecase:
i have some default map. on top of that i want to render some special heatmap. that heatmap has to only be shown above roads.
current approach:
add layers to mapbox that produce a map where all roads are white and background is black
add a custom layer that copies the content of the framebuffer to a texture
add layers to render some default map
add custom layer that draws the special heatmap using the above texture as a mask.
problem:
the order in which the layers are rendered seems to not match the order in which they are added to mapbox. this example has a custom layer that outputs the color of the center pixel in the framebuffer at the moment it is called:
var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 16,
center: [8.30468, 47.05232],
style: {
"version":8,
"name":"test",
"sources": {
"openmaptiles": {
"type": "vector",
"url": "https://api.maptiler.com/tiles/v3/tiles.json?key=X0HhMcrAjHfR6MvFLSSn"
}
},
"layers":[]}
});
var getPixelLayer = {
id: 'highlight',
type: 'custom',
onAdd: function (map, gl) { },
render: function (gl, matrix) {
var pixels = new Uint8Array(4);
gl.readPixels(Math.round(gl.drawingBufferWidth/2), Math.round(gl.drawingBufferHeight/2), 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
document.getElementById("color").innerHTML = pixels[0]+","+pixels[1]+","+pixels[2];
}
};
map.on('load', function() {
//expected output: "255,255,255" everywhere
//actual output: "0,0,0" everywhere
/*
map.addLayer({"id":"bkg1","paint":{"background-color":"rgb(255, 255, 255)"},"type":"background"});
map.addLayer(getPixelLayer);
map.addLayer({"id":"bkg2","paint":{"background-color":"rgb(0, 0, 0)"},"type":"background"});
*/
//expected output: "0,0,0" everywhere
//actual output: "177,177,177" above buildings
// "0,0,0" above streets
// "0,0,0" above background
map.addLayer({"id":"bkg1","paint":{"background-color":"rgb(0, 0, 0)"},"type":"background"});
map.addLayer(getPixelLayer);
map.addLayer({"id":"roads","layout":{"line-cap":"round","line-join":"round","visibility":"visible"},"paint":{"line-color":"#fff","line-offset":0,"line-width":{"base":1.4,"stops":[[8,1.2],[16,12]]}},"source":"openmaptiles","source-layer":"transportation","type":"line"});
map.addLayer({"id":"building","paint":{"fill-color":"rgb(177, 177, 177)"},"source":"openmaptiles","source-layer":"building","type":"fill"});
});
https://jsfiddle.net/t31cj5v0/
as can be seen the rendering order seems to be background->buildings->custom_layer->streets instead of background->custom_layer->streets->buildings. The layer orderings appears correct on screen at the end, but doing what i want is not possible like this.
does anyone know what causes this behaviour and how it could be avoided?
other implementations i tried:
i also tried to render the black-white street map to a different mapbox object with synced movement. this works, but since each mapbox has its own webgl context i cant use the resulting black-white texture in the other context.
featurerequest (?)
it would be really nice if some "mask" layer could be implemented to accomplish this objective natively. but right now i'm just searching for some hack to do this. thanks in advance.
mapbox layers are drawn in 3 rendering passes. "offscreen", "opaque" and "translucent". "offscreen" is not important as the subsequent passes always draw over it. the problem appears since first all layers classifiead as "opaque" are drawn, then all layers classified as "translucent" (inside the render passes layer order is respected). because of this the rendering order does not match the declared order.
custom layers are drawn in the "translucent" pass, but because of the above it can happen that later layers are already rendered (if "opaque"). usually this is not a problem as mapbox uses a stencil buffer to track which layer goes under/over which one even if they are not drawn in order.
to make the render order match add this line after creating the mapbox instance. it will disable the "opaque" rendering pass and force every layer that was "opaque" to be "translucent":
map.painter.opaquePassEnabledForLayer = function() { return false; }

Check if a point is inside polygon in OpenLayers 3

When I draw a polygon in an OpenLayers map, I want to know if the marker is inside the polygon or not. I searched in the OpenLayers API, but didn't find a solution.
And you can see my full code in this link.
I have the impression that I have to modify this function:
function addInteraction() {
var value = typeSelect.value;
if (value !== 'None') {
draw = new ol.interaction.Draw({
source: vectorSource,
type: /** #type {ol.geom.GeometryType} */ (typeSelect.value)
});
map.addInteraction(draw);
draw.on('drawend',function(e){
//Here
});
}
}
How can I do this?
You have a method 'intersectsCoordinate' for the ol.geom.Geometry.
So the code for that will look like:
var polygonGeometry = e.feature.getGeometry();
var coords = iconFeature.getGeometry().getCoordinates();
polygonGeometry.intersectsCoordinate(coords)
You can use the JSTS library, which implements simple geometry processing such as intersects, difference, etc. It contains an OL3 parser that allows the conversion of geometry from OL3 to JSTS and vice-versa.
See an example in OL3. Basically, you would use a process that checks if the geometry of your marker is within your polygon or not and do what you want from there.

OpenLayers DrawFeature control with Point destroys double click to zoom

I have some simple code that I copied from one of the openlayers examples for drawing several different types of geometries on the map. The problem is, whenever the "point" geometry is selected, I lose the ability to double-click to zoom in. The only difference between the examples and my code is I'm registering the handlers to use MOD_SHIFT, because i want to retain the ability to pan/zoom. Here is a snipit of code:
point: new OpenLayers.Control.DrawFeature(this.geometryFilterLayer,
OpenLayers.Handler.Point,
{
'done': console.info("drew point")
},
{
keyMask: OpenLayers.Handler.MOD_SHIFT
}
),
polygon: new OpenLayers.Control.DrawFeature(this.geometryFilterLayer,
OpenLayers.Handler.Polygon,
{
'done': console.info("drew polygon")
},
{
keyMask: OpenLayers.Handler.MOD_SHIFT
}
),
The funny thing about the above code is, the 'done' event only gets fired when the control/handler is created, and the keyMask doesn't work at all -- I have to loop through this object and manually set the keyMask each time, but that's not the real problem at hand.
I've tried every way I can think of to register a dblclick event, but no matter what, I can't get it to zoom in when I double click. It works fine on all the other geometries (bbox, point/radius, and polygon).
Can anybody give me some advice?
I never solved this issue, but ended up doing away with using MOD_XXX altogether. Each different draw control had too much built-in functionality for what happens when you hold shift, ctrl, alt, etc. I ended up using custom Buttons and a toolbar, that way the user can explicitly select the drawing control themselves.
this.toolbar = new OpenLayers.Control.Panel({
displayClass: 'olControlEditingToolbar'
});
map.addControl(this.toolbar);
var navButton = new OpenLayers.Control.Button({
displayClass: "olControlNavigation",
title: "Navigation",
trigger: lang.hitch(this, function(data){
this.toggleDrawControl("navigation");
navButton.activate();
pointButton.deactivate();
bboxButton.deactivate();
pointRadiusButton.deactivate();
polygonButton.deactivate();
})
});
...
this.toolbar.addControls([navButton, pointButton, bboxButton, pointRadiusButton, polygonButton]);
and my function to toggle draw controls (can be called externally, so that's why I re-call the activate and deactivate functions:
toggleDrawControl: function(geometryType){
this.currentGeometryType = geometryType;
for(key in this.drawControls) {
var control = this.drawControls[key];
if(geometryType == key) {
control.activate();
this.drawingButtons[key].activate();
} else {
control.deactivate();
this.drawingButtons[key].deactivate();
}
}
}

Creating a map using only toggleable overlays?

I'm trying to create map (using the Google Maps JavaScript API V3) which consists of several partially-transparent layers. By default, these layers should all be overlaid on top of one another to form a complete map, but the user should be able to turn any combination of them on or off (while preserving order) to create whatever view they prefer.
So far, I've had a great deal of luck getting this working for a single layer using map.mapTypes, but when adding all the layers via map.overlayMapTypes, I've hit a couple of snags:
The map doesn't seem to get fully initialized if map.setMapTypeId() is not called (no controls appear and the map is not correctly centered) and it cannot be called with an overlay.
It isn't clear how to toggle the visibility of an overlay without directly modifying the map.overlayMapTypes array, which complicates keeping them correctly ordered. I'd much prefer something analogous to the Traffic/Transit/Photos/etc. control available within Google Maps itself.
Here's the initialize function I'm working with. I'd post a link, but the map imagery isn't publicly available:
function initialize() {
map = new google.maps.Map(document.getElementById("map_canvas"), {
zoom: 0,
center: center
});
/* if these lines are uncommented, the single layer displays perfectly */
//map.mapTypes.set("Layer 3", layers[3]);
//map.setMapTypeId("Layer 3");
//return;
var dummy = new google.maps.ImageMapType({
name: "Dummy",
minZoom: 0,
maxZoom: 6,
tileSize: new google.maps.Size(256, 256),
getTileUrl: function() {return null; }
});
map.mapTypes.set("Dummy", dummy);
map.setMapTypeId("Dummy");
// layers is an array of ImageMapTypes
for (var i = 0; i < layers.length; i++) {
map.overlayMapTypes.push(layers[i]);
}
}
As you can see, I've tried creating a "dummy" maptype (which always returns null for tile URLs) to serve as the base map. While this does cause the controls to display, it still doesn't center correctly.
What's the best way to create a map which consists only of toggleable overlays?
Update: Turns out the dummy maptype works perfectly well if you also remember to set a projection. That's one problem solved, at least. :-)
I use ImageMapType, but I don't add it to mapTypes. I just add it to overlayMapTypes and when I need to remove it I use setAt to set the entry in overlayMapTypes to null.
You will need to add individual controls to the UI that toggle the individual layers.

Categories