I am working with the application Beta_Here which uses leaflet plugins, all libraries are local except for few(css related)
Usage of application live
First View:This application get input from user and set the distance
calculation formula accordingly....
Second View : After entering input e.g 9, second view will be loaded
where we can draw shapes....
Introduction
I have setup the script which will load two imageoverlays(layers) and
we can toggle them from top right and we can draw or measure from
bottom left....
Problem
When we draw shapes or put markers on an image, controls work nearly
perfect but when we toggle the layers, there starts the problem....
all shapes go to the background or (it seems they disappeared)
Main Question
How can we bind the drawings and marker to the specific
layer(imageoverlay) if there is a way as we can see the drawing are
not bind with the images but the map container..... (Pardon me if you
feel i am doing something stupid because i have limited knowledge
about layers so i came up with my question here....
If someone has idea about how to solve this problem, please do help or
any kind of reference will be appreciated... Thanks for your time
Working Script
var map = L.map('map', {
minZoom: 1,
maxZoom: 4,
center: [0, 0],
zoom: 0,
crs: L.CRS.Simple
});
// dimensions of the image
var w = 3200,
h = 1900,
mainurl = 'assets/img/isbimg.jpg';
childurl = 'assets/img/fjmap.png';
// calculate the edges of the image, in coordinate space
var southWest = map.unproject([0, h], map.getMaxZoom() - 1);
var northEast = map.unproject([w, 0], map.getMaxZoom() - 1);
var bounds = new L.LatLngBounds(southWest, northEast);
var featureGroup = L.featureGroup().addTo(map);
var drawControl = new L.Control.Draw({
edit: {
featureGroup: featureGroup
},
draw: {
polygon: true,
polyline: true,
rectangle: true,
circle: true,
marker: true
}
}).addTo(map);
map.on('draw:created', showPolygonArea);
map.on('draw:edited', showPolygonAreaEdited);
// add the image overlay,so that it covers the entire map
L.control.layers({
Main: L.imageOverlay(mainurl, bounds),
Child: L.imageOverlay(childurl, bounds)
}, null, { collapsed: false }).addTo(map);
L.control.nanomeasure({ nanometersPerPixel: 10000 }).addTo(map);
// tell leaflet that the map is exactly as big as the image
map.setMaxBounds(bounds);
L.tileLayer({
attribution: 'SmartMinds',
maxZoom: 18
}).addTo(map);
//polygon area customization
function showPolygonAreaEdited(e) {
e.layers.eachLayer(function (layer) {
showPolygonArea({ layer: layer });
});
}
function showPolygonArea(e) {
var userInputCustom = prompt("Please enter image name : choose between a to f");
featureGroup.addLayer(e.layer);
e.layer.bindPopup("<div style='width:200px;height:200px;background-image: url(assets/img/" + userInputCustom + ".png);background-size: 195px 195px;;background-repeat: no-repeat;'></div>");
e.layer.openPopup();
}
});
I would contain those FeatureGroup and ImageOverlay pairs into L.LayerGroup's. Then you can switch between those groups. Then you can keep track of the currently selected group, and add your features to the featurelayer of that group. I can explain it better with code through comments:
Basic map, nothing special:
var map = L.map('map', {
'center': [0, 0],
'zoom': 1,
'layers': [
L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
'attribution': 'Map data © OpenStreetMap contributors'
})
]
});
// Bounds for the map and imageoverlays
var bounds = L.latLngBounds([[40.712216, -74.22655],[40.773941, -74.12544]]);
// Set bounds on the map
map.fitBounds(bounds);
The grouping part:
// New layergroup, note it's not added to the map yet
var layerGroup = new L.LayerGroup(),
imageOverlayUrl = 'https://placeholdit.imgix.net/~text?txtsize=33&txt=Overlay 1&w=294&h=238',
// New imageoverlay added to the layergroup
imageOverlay = new L.ImageOverlay(imageOverlayUrl, bounds).addTo(layerGroup),
// New featuregroup added to the layergroup
featureGroup = new L.FeatureGroup().addTo(layerGroup);
// Second layergroup not added to the map yet
var layerGroup2 = new L.LayerGroup(),
imageOverlayUrl2 = 'https://placeholdit.imgix.net/~text?txtsize=33&txt=Overlay 2&w=294&h=238',
// New imageoverlay added to the second layergroup
imageOverlay2 = new L.imageOverlay(imageOverlayUrl2, bounds).addTo(layerGroup2),
// New featuregroup added to the second layergroup
featureGroup2 = new L.FeatureGroup().addTo(layerGroup2);
Default drawcontrol and layercontrol with both layergroups added as baselayers:
var layerControl = new L.control.layers({
'Group 1': layerGroup,
'Group 2': layerGroup2
}).addTo(map);
var drawControl = new L.Control.Draw().addTo(map);
Here's where the magic happens ;) :
// Variable to hold the selected layergroup's featuregroup.
var currentFeatureGroup;
// Catch the layer change event
map.on('baselayerchange', function (layersControlEvent) {
// Loop over the layers contained in the current group
layersControlEvent.layer.eachLayer(function (layer) {
// If it's the imageoverlay make sure it's in the background
if (layer instanceof L.ImageOverlay) {
layer.bringToBack();
// If not then it's the featuregroup, reference with variable.
} else {
currentFeatureGroup = layer;
}
});
});
// Catch draw created event
map.on('draw:created', function (e) {
// Store created feature into the current featuregroup
currentFeatureGroup.addLayer(e.layer);
});
That's it. Pretty basic just meant as an example but it does what you want it to do. A real implementation would look different, with errorhandling because for instance when you draw and have no baselayer/overlay selected it fail etc. Here's a working example on Plunker to play with: http://plnkr.co/edit/6cGceX?p=preview
Related
// Create the map object with center, zoom level and default layer.
let map = L.map('mapid', {
center: [40.7, -94.5],
zoom: 3,
layers: [streets]
});
// Create a base layer that holds all three maps.
let baseMaps = {
"Streets": streets,
"Satellite": satelliteStreets,
"Night Navigation": nightNav
};
// 1. Add a 2nd layer group for the tectonic plate data.
let allEarthquakes = new L.LayerGroup();
let allTectonics = new L.LayerGroup();
let majorEQ = new L.LayerGroup();
// 2. Add a reference to the tectonic plates group to the overlays object.
let overlays = {
"Earthquakes": allEarthquakes,
"Tectonic Plates": allTectonics,
"Major Earthquakes": majorEQ
};
// Then we add a control to the map that will allow the user to change which
// layers are visible.
L.control.layers(baseMaps, overlays).addTo(map);
let tectonicData = "https://raw.githubusercontent.com/fraxen/tectonicplates/master/GeoJSON/PB2002_boundaries.json";
let tstyle = map.on('baselayerchange', function(feature) {
console.log('base layer has been changed');
return {
fillColor: tecStyle(L.overlays),
fillOpacity: 0.8,
weight: 0.5
};
});
function tecStyle(feature) {
if (feature === baseMaps["Streets"]) { return "purple";
}
if (feature === baseMaps["Satellite"]) {
return "red";
}
if (feature === baseMaps["Night Navigation"]) {
return "red";
}
};
// 3. Use d3.json to make a call to get our Tectonic Plate geoJSON data.
d3.json(tectonicData).then(function(data) {
// Create GeoJSON layer with the retrieved data.
L.geoJSON(data, {
style: tstyle,
onEachFeature: function(feature, layer){
layer.bindPopup("<h3><b> Plate Boundary Name: " + feature.properties.Name + "</h3></b>");
}
}).addTo(allTectonics);
allTectonics.addTo(map);
});
});
I want the tectonic fault line color to change colors depending on whether the user is on the 'Streets', 'Satellite', or 'Night Navigation' layers. For example, I want the fault line color to be 'purple' when on 'Streets', 'red' when on 'Satellite', and 'blue' when on 'Night Navigation' base layer maps. I was hoping I could get it to work using map.on and the event listener is working, because it comes up in my DevTools console, but the color of the fault line remains the default color of 'blue' no matter what layer I am on. What am I doing wrong here? or is there an easier way to do this?
update "style: tstyle" in step 3 to "style: tecStyle" and you should be good to go!
I already saw the same question here: StackOverflow
But none of the answers helped or atleast I didn't understand it.
If I place a new marker on the map by clicking and then remove the marker by clicking again and then attempt to zoom, I get the error:
TypeError: Cannot read properties of null (reading '_latLngToNewLayerPoint')
at NewClass._animateZoom
A more simple example of this happening is when I click a marker to see the popup. After seeing the popup, I close the popup and then try to zoom and I get errors on every zoom.
When I first close the popup, I get a warning saying that listener not found which is odd, because I close the popup by simply clicking on the map and I DO have a listener for when I click on the map.
here is the code:
var mapLink = 'Esri';
var wholink = 'i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community';
var satelliteLayer = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '© ' + mapLink + ', ' + wholink,
maxZoom: 18,
});
var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
});
var mapboxUrl = 'https://api.mapbox.com/styles/v1/johnmichel/ciobach7h0084b3nf482gfvvr/tiles/{z}/{x}/{y}?access_token=pk.eyJ1Ijoiam9obm1pY2hlbCIsImEiOiJjaW9iOW1vbHUwMGEzdnJseWNranhiMHpxIn0.leVOjMBazNl6v4h9MT7Glw';
var mapboxAttribution = 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox'
var streets = L.tileLayer(mapboxUrl, { id: 'mapbox/streets-v11', tileSize: 512, zoomOffset: -1, attribution: mapboxAttribution });
var map = L.map("mapContainer", {
center: [lat, lng],
zoom: 10,
layers: [osm, satelliteLayer, streets],
maxZoom: 18,
minZoom: 10,
smoothWheelZoom: true, // enable smooth zoom
smoothSensitivity: 1, // zoom speed. default is 1
smoothZoom: true,
smoothZoomDelay: 1000 // Default to 1000
}).on('click', this.onMapClick);
map.on('moveend', this.onMapMoved)
var baseMaps = {
"OpenStreetMap": osm,
"Satellite": satelliteLayer,
"Mapbox Streets": streets
};
var kayakLaunchLayer = L.layerGroup([]);
var yourPin = L.layerGroup([]);
this.markerLayerGroup = L.layerGroup();
this.singleMarkerLayerGroup = L.layerGroup();
var layerControl = L.control.layers(baseMaps).addTo(map);
map.addLayer(kayakLaunchLayer);
map.addLayer(yourPin);
this.markerLayerGroup = kayakLaunchLayer;
this.singleMarkerLayerGroup = yourPin;
// layerControl.addOverlay(this.markerLayerGroup, "Kayak Launches");
// layerControl.addOverlay(this.singleMarkerLayerGroup, "Your Pin");
map.invalidateSize();
this.map = map;
this.map is a variable defined in data in my Vue component and so is this.singleMarkerLayerGroup and this.markerLayerGroup.
And this is the function where I actually add the markers to the this.markerLayerGroup so that I can see them as an overlay on the map:
var lat = launch.loc.coordinates[1];
var lng = launch.loc.coordinates[0];
var marker = L.marker([lat, lng]).addTo(this.markerLayerGroup).on('click', this.onMarkerClick);
var photoImgwithContent = `<h2>${launch.name}</h2><img src="${launch.images[0]}" height="150px" width="150px"/><h3></h3>${this.capitalizeFirstLetter(launch.waterType)}`;
marker.bindPopup(photoImgwithContent);
Now what is wrong with any of this? I have restructured the code to follow documentation as closely as possible and still stumped.
But strangely, if I turn off zoom animation by applying this map option: zoomAnimation:false, then the error is gone but the map look terrible without zoom animation.
This seems like a similar situation as in Uncaught TypeError: this._map is null (Vue.js 3, Leaflet), since you seem to use Vue.js 3, and the error occurs when zooming the map after some Layers/Popup are removed:
the culprit is the proxying of this.map by Vue, which seems to interfere with Leaflet events (un)binding. It looks like Vue 3 now automatically performs deep proxying
A solution consists in "unwrapping" / un-proxying the map and the Layer Groups (i.e. all Leaflet objects that you store in this Vue component data) whenever you use them, e.g. with Vue3's toRaw utility function:
var marker = L.marker([lat, lng])
.addTo(toRaw(this.markerLayerGroup));
See also the Vue 3 guide to Reduce Reactivity Overhead for Large Immutable Structures.
Let's say I have some overlapping layers and each layer has a click event. When I click on the map, I'd like to know which layers are clicked on, though the click event stops after the first layer and does not propagate to its underlying layers. How can I achieve this?
Here's a sample fiddle and its code: https://jsfiddle.net/r0r0xLoc/
<div id="mapid" style="width: 600px; height: 400px;"></div>
<script>
var mymap = L.map('mapid').setView([51.505, -0.09], 13);
L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
maxZoom: 18,
attribution: 'Map data © OpenStreetMap contributors, ' +
'CC-BY-SA, ' +
'Imagery © Mapbox',
id: 'mapbox.streets'
}).addTo(mymap);
L.polygon([
[51.509, -0.08],
[51.503, -0.06],
[51.51, -0.047]
]).addTo(mymap).on('click', function() {
console.log('clicked on 1st polygon')
});
L.polygon([
[51.609, -0.1],
[51.503, -0.06],
[51.51, -0.047]
]).addTo(mymap).on('click', function() {
console.log('clicked on 2nd polygon')
});
</script>
If you click on each polygon, you see its related message. If you click on the overlapping part, you only see the message for the second polygon.
You have to listen directly to the map "click" event and to "manually" determine which layers contain the clicked position.
You can use leaflet-pip plugin (point in polygon) for example for this determination:
map.on("click", function (event) {
var clickedLayers = leafletPip.pointInLayer(event.latlng, geoJSONlayerGroup);
// Do something with clickedLayers
});
Demo: https://jsfiddle.net/ve2huzxw/526/ (listening to "mousemove" instead of "click")
There is a leaflet plugin for propagating events to the underlying layers: https://github.com/danwild/leaflet-event-forwarder
You can use it in your javascript to enable event-forwarding, e.g.:
const myEventForwarder = new L.eventForwarder({
map: map,
events: {click: true, mousemove: false}
});
The problems is the order in which the layers (geometries) are added. In my case I had an array of geometries and what I did was just sort the geometries array using its bounds L.LatLngBounds#contains, so if a geometry contains another it should be added later.
var geometries = [layerOne, layerTwo, ...];
geometries
.sort((a, b) => {
// in my case a separate function is required because the geometry could be a Circle, Rectangle, Polygon or Marker
// and the methods to get the corresponding bounds are different.
var boundsA = this.getBoundsFromGeometry(a);
var boundsB = this.getBoundsFromGeometry(b);
// if the second geometry contains the first, the order must be change so the layers don't overlap
return boundsB.contains(boundsA) ? 1 : -1;
})
.forEach(l => this.map.addLayer(l));
For me the solution was to use the option interactive: false when creating the upper layer:
var options = {
style: feature => this.__determineStyle(feature),
interactive: false,
};
var overlay = L.geoJson(geoJsonData, options);
overlay.addTo(this.map);
https://leafletjs.com/reference-1.0.3.html#interactive-layer
You can listen on the click event on the map. Then, getting the coordinates (lat, lng) of the clicked point, you can filter your features by checking which of them contain it.
For this, you can use booleanPointInPolygon function of turf.js
The following (sample code and fiddle) use polygons as an example.
map.on('click', e => {
const { lat, lng } = e.latlng;
const point = turf.point([lng, lat]);
/* polygons is an array where all your polygon layers are stored */
polygons.forEach((p, i) => {
const polygon= p.toGeoJSON();
if (turf.booleanPointInPolygon(point, polygon)) {
/* do whatever you want with your clicked polygon */
}
});
});
Check fiddle: https://jsfiddle.net/yfg7mdkx/
currently i have a vector map displayed using a KML file as the vector source.
what i want to do is have an image underlay this vector map.
The map is an indoor floormap, the image is the exact same as the vector map only there is more details in it, text written on it etc. What i need is for the map image to underlay the vector map so that the walls of the vector map align perfectly with the walls of the image. This can happen because the KML was created by tracing on top of the images using QGIS.
so far ive been able to have the KML vector map and png image appear on the map, but they are not alligned with each other and are not the same size. This is what i need help with!
here some code for what i currently have:
create the map, no layers yet (maps selected from dropdown boxes)
var map = new ol.Map({
layers: [],
target: 'floormap',
interactions: ol.interaction.defaults({mouseWheelZoom:false}),
view: new ol.View({
center: [0, 0],
zoom: 19,
minZoom: 15,
maxZoom: 30
})
});
add selected map (KML) to map
map.removeLayer(vector);
vector = new ol.layer.Vector({
source: new ol.source.Vector({
url: MAPS_URL + maps[map_id],
format: new ol.format.KML()
})
});
map.addLayer(vector);
setMapExtent(vector);
now i tried to add the image, which works but its not alligned
// this part here i feel may be the problem,
// i just copied and pasted from an example om openlayers.org,
// i dont actually know much about the extent and how to match it to
// the vector map
var extent = [0,0,1024,684];
var projection = new ol.proj.Projection({
code: 'xkcd-image',
units: 'pixels',
extent: extent
});
image = new ol.layer.Image({
source: new ol.source.ImageStatic({
attributions: [
new ol.Attribution({
html: '© xkcd'
})
],
url: MAPS_URL + images[map_id],
projection: projection,
imageExtent: extent
})
});
map.addLayer(image);
the setMapExtent method
function setMapExtent(vectorMap) {
var vectorSource = vectorMap.getSource();
var listenerKey = vectorSource.on('change', function () {
if (vectorSource.getState() === 'ready') {
var extent = vectorSource.getExtent();
map.getView().fitExtent(extent, map.getSize());
vectorSource.unByKey(listenerKey);
}
});
}
at this point i have a vector map with an image sitting way above the map, and the image seems to be smaller too.
Can anyone help me with this issue?
*** Solution! ***
a working solution, although probably not the best way to do it, but it works none the less.
var map = new ol.Map({
layers: [],
target: 'floormap',
interactions: ol.interaction.defaults({mouseWheelZoom:false}),
view: new ol.View({
center: [0, 0],
zoom: 19,
minZoom: 15,
maxZoom: 30
})
});
add new map layer
map.removeLayer(vector);
vector = new ol.layer.Vector({
source: new ol.source.Vector({
url: MAPS_URL + maps[map_id],
format: new ol.format.KML()
})
});
map.addLayer(vector);
setMapExtent(vector);
// call image adding function pass in vector
// to get its extend
addImage(vector);
the addImage function
function addImage(vectorMap) {
var vectorSource = vectorMap.getSource();
// listen for one change on the vector to get the extent of it
// for use in setting the image extent. tried to use on.('load')
// but it didnt work
var listenerKey = vectorSource.once('change', function () {
var extent = vectorSource.getExtent();
var projection = new ol.proj.Projection({
code: 'xkcd-image',
units: 'pixels',
extent: extent
});
image = new ol.layer.Image({
source: new ol.source.ImageStatic({
attributions: [],
url: MAPS_URL + images[map_id],
projection: projection,
imageExtent: extent
})
});
// remove vector layer else they keep stacking up
map.removeLayer(vector);
// add image
map.addLayer(image);
// re-add vector only push so it goes above the image
map.getLayers().push(vector);
});
}
seems to work pretty well! can anyone help me with layer ordering?
Your static image must be georeferenced correctly with the view's projection.
Default view's projection is EPSG:3857 (Spherical Mercator)., the extent of this projection is [-20026376.39, -20048966.10, 20026376.39, 20048966.10]
In your code you specify a projection in pixels for your static layer. You need to use the view's projection, something like this :
// Here the extent of your layer in EPSG:3857 -> [minx, miy, max, mayy]
var extent = [-10000000, -10000000, 10000000, 10000000];
image = new ol.layer.Image({
source: new ol.source.ImageStatic({
attributions: [
new ol.Attribution({
html: '© xkcd'
})
],
url: MAPS_URL + images[map_id],
imageSize: [1024, 684], // Don't forget the image size here
imageExtent: extent
})
});
map.addLayer(image);
Update:
For layer ordering if you want your vector layer on top use push:
http://openlayers.org/en/v3.8.2/apidoc/ol.Collection.html#push
map.getLayers().push(vector)
I'm attempting to add multiple kml layers to a map that can be turned on and off with check boxes. I got that part working (yay!). When I click on a layer to turn it on, it zooms in (this is fine), but when I unclick to turn the layer off, it zooms back out to my map extent. How do I get it to preserve the zoom of the last loaded layer? Code below.
<script>
var map;
var watershedLayer = new google.maps.KmlLayer ({
url: 'http://mvihes.bc.ca/mapping/watersheds.kmz'
});
var ere1949Layer = new google.maps.KmlLayer ({
url: 'http://mvihes.bc.ca/mapping/ere1949.kmz'
});
function initialize() {
var parksville= new google.maps.LatLng(49.316786, -124.308768);
var mapOptions = {
zoom: 9,
center: parksville
};
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
check();
}
function check()
{
if(document.getElementById('watersheds').checked)
{watershedLayer.setMap(map);}
else
{watershedLayer.setMap(null);}
if(document.getElementById('ere1949').checked)
{ere1949Layer.setMap(map);}
else
{ere1949Layer.setMap(null);}
}
google.maps.event.addDomListener(window, 'load', initialize);
</script>
I tried using the preserveViewport function but it just stopped the layer from zooming in, which is not what I wanted. I'm new to javaScript so possibly I'm missing something obvious...any help would be appreciated!
jsfiddle
Set the map-property of a selected layer only when it's not set yet:
function check()
{
if(document.getElementById('watersheds').checked)
{if(!watershedLayer.getMap())watershedLayer.setMap(map);}
else
{watershedLayer.setMap(null);}
if(document.getElementById('ere1949').checked)
{if(!ere1949Layer.getMap())ere1949Layer.setMap(map);}
else
{ere1949Layer.setMap(null);}
}
http://jsfiddle.net/jhagmq7L/16/
You can also attach max and min zoom levels to the map, something to consider the over all map zoom when loading kml layers.
// sets the min and max zoom levels of the map
var opt = { minZoom: 6, maxZoom: 18 };
map.setOptions(opt);