I have a map where I need to show some locations based on the year the has selected. There are about 100k locations per year.
Everything works fine during the first load (year 2015). But if the user chooses another year (let's say year 2016), the browser will freeze.
Below is my code. I am callin markers.clearLayers() in order to remove the current points (from year 2015) and update the map with the new ones (year 2016).
var map = L.map('map', {
center: latlng,
zoom: 10,
layers: [tiles]
});
var markers = L.markerClusterGroup({
chunkedLoading: true
});
fetchLocations(yearSlider.value)
function fetchLocations(year) {
markers.clearLayers();
fetch('http://localhost:3003/locations/year/' + year)
.then(response => response.json())
.then(json => {
for (var i = 0; i < json.length; i++) {
const geo = json[i].geo;
const coords = geo.coordinates;
var marker = L.marker(L.latLng(coords[1], coords[0]), {
title: "abc"
});
marker.bindPopup("abc");
markers.addLayer(marker);
}
map.addLayer(markers);
});
}
First way is to reduce the visible markers for just what is on screen. This will increase a lot of performance.
var markers = new L.MarkerClusterGroup(....);
// Remove everything outside the current view (Performance)
markers._getExpandedVisibleBounds = function () {
return mcg._map.getBounds();
};
Second way is increasing maxClusterRadius will give you less clusters over the map. This also improve dragging performance.
Other freezing issue might be creating L.marker(...) of 100k locations.
Editted:
fetchLocations(yearSlider.value)
var map;
function initMap () {
// Reset whole map and redraw it.
if (map) {
map.off();
map.remove();
}
// Create a new leaflet map
map = L.map('map', {
center: latlng,
zoom: 10,
layers: [tiles]
});
return;
}
function fetchLocations(year) {
initMap();
fetch('http://localhost:3003/locations/year/' + year)
.then(response => response.json())
.then(json => {
// Create new marker cluster group
var markers = L.markerClusterGroup({
chunkedLoading: true
});
for (var i = 0; i < json.length; i++) {
const geo = json[i].geo;
const coords = geo.coordinates;
var marker = L.marker(L.latLng(coords[1], coords[0]), {
title: "abc"
});
marker.bindPopup("abc");
markers.addLayer(marker);
}
// Add individual group to the map
map.addLayer(markers);
});
}
The issue is that you are individually adding Markers to your Cluster Group while the latter is already on map:
function () {
// On next calls, `markers` is still on map
for (var i = 0; i < json.length; i++) {
markers.addLayer(marker); // synchronously processes when the group is on map
}
map.addLayer(markers); // on first call, `markers` is not yet on map
}
You can accumulate your markers in an array within your loop, then once it is full, you use markers.addLayers(arrayOfMarkers)
Or remove your markers group from map at the beginning of your function, so that it internally accumulates your Markers and batch processes them when it is added on map at the end.
Even though parndepu's solution also works, I managed to solve the problem by simply adding map.removeLayer(markers); after markers.clearLayers();.
Related
I'm trying to assign the marker objects that I have added to my map into an array of markers to use in order to calculate the distances to each marker from a specific location. I begin with converting addresses from a text file that is fetched with a callback method that loops through each address and for each iteration sends a request to get the coordinates from the Geolocation API. In the geocode function, each request callback I add the corresponding LatLng to a new instance of a marker that is added to my map. Next, I try to push the marker onto my markers array.
When I try to console.log the values of the marker object through a button with an event listener, only empty objects are displayed.
I tried to declare the markers[] variable as a global variable. I'm still a beginner with Javascript but I think my issue may lie with the asynchronous action of the geocode request?
I get empty objects when I console log the markers array, although I get correct number of array elements (10 markers)
Here is my code:
let map;
let geocoder;
let markers = [];
window.onload = function() {
convertAddress();
document.getElementById('find_aed').addEventListener("click", function() {
let length = markers.length;
let aMarker;
for (let i = 0; i < length; i++) {
aMarker = markers[i]
console.log(aMarker.getPosition()); // ** This is where I print values **
}
});
}
function initMap() {
map = new google.maps.Map(document.getElementById("map"), {
zoom: 15,
center: { lat: 43.5327, lng: -80.2262 },
});
geocoder = new google.maps.Geocoder();
convertAddress();
const currLocation = new google.maps.LatLng(43.5327, -80.2262); object
console.log(currLocation.toString());
}
// convert address to latlng and add marker to map
function geocode(request) {
geocoder
.geocode(request)
.then((result) => {
const { results } = result;
// Add marker to map
let marker = new google.maps.Marker({
position: results[0].geometry.location,
map: map,
});
markers.push(marker); // ** This is where I push values to array **
return results; // return results
})
.catch((e) => {
alert("Geocode not successful for location:" + request + e);
})
}
// convert addresses into coordinates through google API request
function convertAddress() {
fetch('guelph_add.txt')
.then(response => response.text())
.then((text) => {
let addresses = text.split('\n');
let length = addresses.length;
let result;
console.log(length);
for (let i = 0; i < 10 ; i++) {
result = geocode ( {address: addresses[i]} );
}
});
}
window.initMap = initMap;
I am plotting thousands of data points on a map, and at any point I want to fetch the data points falling in a particular map bounding box. For example, attached below is the complete view of the available data points,
If I zoom in a bit, i will have fewer points and I want to fetch the data associated with those points.
How I am approaching this is fetching the bounding box of the map, and then checking the coordinates of each point if they belong in that bounding box. Sample code is below,
const data = [
{lat: 33.93911, lng: 67.709953},
{lat: -11.2027, lng: 17.8739},
{lat: -33.8688, lng: 151.2093},
{lat: -42.8821, lng: 147.3272},
{lat: 26.0275, lng: 50.55}
]
const boundingBox = window.map.getMapBounds();
const result = [];
for(let i = 0; i < data.length; i++) {
if(/* point lies within bounding box */) {
result.push(point);
}
}
How can I optimise it or is there a better solution? Thanks a lot.
You can try using bbox-polygon and boolean-intersects from turf:
Use onViewStateChange from deck instance within a debounce function so it won't be executed too often
Create a polygon from the deck viewState using #turf/bbox-polygon
Pass it to #turf/boolean-intersects with the whole features using filter JS method
import { WebMercatorViewport } from '#deck.gl/core';
import bboxPolygon from '#turf/bbox-polygon';
import intersects from '#turf/boolean-intersects';
function debounce(fn, ms) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, ms);
};
}
function getViewport(viewState) {
const bounds = new WebMercatorViewport(viewState).getBounds()
return bboxPolygon(bounds)
}
function getViewportFeatures({ viewState }) {
const viewport = getViewport(viewState)
const viewportFeatures = features.filter((f) => intersects(f, viewport))
console.log(viewportFeatures) // Your target
}
<DeckGL
...
onViewStateChange={debounce(getViewportFeatures, 500)}
/>
Advanced: if your data is really big, you can use Web Workers
I'm currently working on an internal CRM application for my company. Its built with Blazor, and utilizes the Azure Maps javascript SDK. I'm using Azure Maps to visualize Map Items collected by player's of my company's game. All of the map setup is done in javascript according to the documentation provided by Microsoft.
The map has a symbol layer for displaying points, and a line layer for rendering the path traveled by a given player. Additionally, we are generating a summary of the player's collection stats (speed, distance, and time between collections). The page this component lives on has a DateRangePicker, a drop down menu of different item types, and a button to run the query. Currently, users of this web page can change the date range and the item type and rerun the query. This causes the Summary as well as the map with all points and the line to be re-rendered correctly. However, when the map is re-rendered, the popups attached to the points are no longer on the map, or are at least unresponsive to mouse events. If anyone has any ideas on how to keep and update my popups across map renderings, they would be greatly appreciated.
I was able to solve for this problem disposing of any map resources before attempting to create and render another map. This ensures that any html from previous map renders do not get in the way of the current map.
window.adminMap = (() => {
let map;
let datasource;
let point;
let popup;
const setupItemsMap = (subscriptionKey, mapItems) => {
if (map != null) {
map.dispose();
}
map = new atlas.Map('itemsMap', {
center: [mapItems[0].coordinate.long, mapItems[0].coordinate.lat],
centerOffest: [0, 0],
zoom: 15,
pitch: 90,
style: 'road',
language: 'en-US',
view: 'Auto',
authOptions: {
authType: 'subscriptionKey',
subscriptionKey: subscriptionKey
}
});
map.events.add('ready', () => {
//Create a data source and add it to the map.
datasource = new atlas.source.DataSource(null, {
lineMetrics: true
});
map.sources.add(datasource);
var points = [];
mapItems.forEach(function (mapItem) {
point = new atlas.data.Feature(new atlas.data.Point([mapItem.coordinate.long, mapItem.coordinate.lat]), {
collectedAt: mapItem.collectedAt,
speed: 0
});
points.push(point);
});
points.forEach(function (point) {
let previous;
let current = points.indexOf(point);
if (current > 0) {
previous = current - 1;
point.properties.speed = atlas.math.getSpeedFromFeatures(points[previous], points[current], "collectedAt", "milesPerHour", 1);
}
else {
point.properties.speed = 0;
}
});
datasource.add(points);
var line = createLineFrom(points);
datasource.add(line);
//Calculate a color gradient expression based on the speed of each data point.
var speedGradient = calculateGradientExpression(points, line);
//Create a line layer and pass in a gradient expression for the strokeGradient property.
map.layers.add(new atlas.layer.LineLayer(datasource, null, {
strokeWidth: 5,
strokeGradient: speedGradient
}));
//Create a layer to render each data point along the path.
var pointLayer = new atlas.layer.SymbolLayer(datasource, null, {
//Only render point data in this layer, not the points of the line.
filter: ['==', ['geometry-type'], 'Point']
});
//Create a popup. *This needs to update with the rest of the map!
popup = new atlas.Popup();
popup.open(map);
map.layers.add(pointLayer);
map.events.add('click', pointLayer, pointClicked);
});
}
Glad to here you resolved your issue and posted the answer for others. One recommendation, when ever possible it is best to reuse the map instance and layers. You could run the code above once in your app, then in the future use datasource.setShapes and pass in the new points. This would automatically update the data on the map. If the popup is open when adding new data, simply close it. It also looks like some optimizations can be done around your point conversion code/speed calculations. Here is a modified version of your code that may provide some performance enhancements.
window.adminMap = (() => {
let map;
let datasource;
let point;
let popup;
let lineLayer;
const setupItemsMap = (subscriptionKey, mapItems) => {
if (map != null) {
map.dispose();
}
map = new atlas.Map('itemsMap', {
center: [mapItems[0].coordinate.long, mapItems[0].coordinate.lat],
centerOffest: [0, 0],
zoom: 15,
pitch: 90,
style: 'road',
language: 'en-US',
view: 'Auto',
authOptions: {
authType: 'subscriptionKey',
subscriptionKey: subscriptionKey
}
});
map.events.add('ready', () => {
//Create a data source and add it to the map.
datasource = new atlas.source.DataSource(null, {
lineMetrics: true
});
map.sources.add(datasource);
lineLayer = new atlas.layer.LineLayer(datasource, null, {
strokeWidth: 5,
//strokeGradient: speedGradient
});
//Create a line layer and pass in a gradient expression for the strokeGradient property.
map.layers.add(lineLayer);
//Create a layer to render each data point along the path.
var pointLayer = new atlas.layer.SymbolLayer(datasource, null, {
//Only render point data in this layer, not the points of the line.
filter: ['==', ['geometry-type'], 'Point']
});
//Create a popup. *This needs to update with the rest of the map!
popup = new atlas.Popup();
popup.open(map);
map.layers.add(pointLayer);
map.events.add('click', pointLayer, pointClicked);
//Add mapItems to data source.
setMapItems(mapItems);
});
}
const setMapItems = (mapItems) => {
let points = [];
let previous;
let point;
for(let i = 0, len = mapItems.length; i < len; i++){
point = new atlas.data.Feature(new atlas.data.Point([mapItem.coordinate.long, mapItem.coordinate.lat]), {
collectedAt: mapItem.collectedAt,
speed: 0
});
if(previous){
point.properties.speed = atlas.math.getSpeedFromFeatures(previous, point, "collectedAt", "milesPerHour", 1);
}
points.push(point);
previous = point;
}
var line = createLineFrom(points);
//Calculate a color gradient expression based on the speed of each data point.
lineLayer.setOptions({
strokeGradient: calculateGradientExpression(points, line)
});
points.push(line);
//Add data to data source using setShapes. This clears and adds the data and only triggers one re-render of the map.
datasource.setShapes(points);
if(popup){
popup.close();
}
}
}
I'm showing using a bing map on my web page and I'm trying to use a custom pin for the location but it won't show up. My project is ASP.Net Core, My image is stored in wwwroot/images and this JavaScript is in wwwroot/js/site.js. I'm not sure if my path is just wrong or what.
var renderRequestsMap = function (divIdForMap, requestData) {
if (requestData) {
var bingMap = createBingMap(divIdForMap);
addRequestPins(bingMap, requestData);
}
}
function createBingMap(divIdForMap) {
var map = new Microsoft.Maps.Map(
document.getElementById(divIdForMap), {
credentials: BingMapKey,
zoom: 2
});
// tile url from Iowa Environmental Mesonet of Iowa State University
var urlTemplate = 'https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-{timestamp}/{zoom}/{x}/{y}.png';
var timestamps = ['900913-m50m', '900913-m45m', '900913-m40m', '900913-m35m', '900913-m30m', '900913-m25m', '900913-m20m', '900913-m15m', '900913-m10m', '900913-m05m', '900913'];
var tileSources = [];
for (var i = 0; i < timestamps.length; i++) {
var tileSource = new Microsoft.Maps.TileSource({
uriConstructor: urlTemplate.replace('{timestamp}', timestamps[i])
});
tileSources.push(tileSource);
}
var animatedLayer = new Microsoft.Maps.AnimatedTileLayer({ mercator: tileSources, frameRate: 500 });
map.layers.insert(animatedLayer);
return map;
}
function addRequestPins(bingMap, requestData) {
var locations = [];
$.each(requestData, function (index, data) {
var location = new Microsoft.Maps.Location(data.lat, data.long);
locations.push(location);
var order = index + 1;
alert(data.pinurl);
var pin = new Microsoft.Maps.Pushpin(location, { icon: 'images/low-risk-south-pin.png' });
bingMap.entities.push(pin);
});
var rect = Microsoft.Maps.LocationRect.fromLocations(locations);
bingMap.setView({ bounds: rect, padding: 80 });
}
Also when the map loads it is super zoomed into my pin and whatever I do I can't get it to start with a far out zoom.
If you can, take a look at the network trace. Here you should see where the images are being requested from and will help you verify if it is requesting from the right location.
As for the zooming, you are calculating in the LocationRect from the locations of the pins and then setting the map view based on that. Sounds like that is working as expected. If you don't want to zoom in on your pins, remove that code.
Probably simple answer for someone that knows JavaScript, I don't know it very well. I'm getting JSON data back and applying markers to a map. However it's generating markers for those that have null which really messes things up. So what I need to do is create a conditional variable based on data being present. I have the following in the code:
let mapElement = document.getElementById('map-banner');
let pointMarkers = mapElement.getAttribute('data-location');
let marked = JSON.parse(pointMarkers);
let bounds = new google.maps.LatLngBounds();
console.log(pointMarkers);
marked.forEach(marked => {
if (marked.lat > 0 && marked.lat !== null) {
let lat = marked.lat;
}
let lng = marked.lng;
const marker = new google.maps.Marker({
position: new google.maps.LatLng(lat, lng),
map: map,
icon: '/marker.png',
title: marked.name
});
bounds.extend(marker.position);
});
map.fitBounds(bounds);
};
Specifically I'm working with the variables lat and lng. I've also tried:
let lat = marked.lat;
if (lat > 0 && lat !== null) {
const lat = marked.lat;
}
In this case it presents all the data and it doesn't appear to be applying the condition.
You are conditionally declaring the variable, which for javascript is optional.
What you want is to skip that iteration in your loop with a guard clause:
marked.forEach(marked => {
if (marked.lat == null)
return;
const marker = new google.maps.Marker({
position: new google.maps.LatLng(marked.lat, marked.lng),
map: map,
icon: '/marker.png',
title: marked.name
});
bounds.extend(marker.position);
});
I think a filter is what you're looking for. Filtering can remove entries from arrays which you don't want to use.
Docs (by MDN) -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
Also, const/let are used to achieve block scope in JavaScript.
consts are used for variables that do not change in value and are (preferably) immutable, see Docs -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
lets are used for values that do change in value, and have different values in different block scopes, see Docs -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
const mapElement = document.getElementById('map-banner');
const pointMarkers = mapElement.getAttribute('data-location');
// catch any error from JSON.parse
try {
const coords = JSON.parse(pointMarkers);
const bounds = new google.maps.LatLngBounds();
// filter out incorrect coords
const markers = coords.filter(coord => {
return (marked.lat > 0 && marked.lat !== null)
});
// create markers, extend bounds
markers.forEach(({ lat, lng }) => {
const marker = new google.maps.Marker({
position: new google.maps.LatLng(lat, lng),
map,
icon: '/marker.png',
title: marked.name
});
bounds.extend(marker.position);
});
// fit bounds
map.fitBounds(bounds);
} catch (error) {
// handle error to keep users happy
}
Maybe just show the marker if it exists?:
if(marked.lat && marked.lat > 0) {
const marker = new google.maps.Marker({
position: new google.maps.LatLng(lat, lng),
map: map,
icon: '/marker.png',
title: marked.name
});
}