How can I fetch data points belonging to a map bounding box? - javascript

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

Related

I'm trying to add marker objects to an array of markers with the Google Maps API but when I try to access the marker objects they are empty

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;

How do I update popups on Azure Maps when re-rendering my map?

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();
}
}
}

Browser freezes when clearing layers

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();.

Conditionally apply variable if data is present in JavaScript

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
});
}

Mapbox dynamic markers in Meteor.js

Able to successfully set the mapbox viewpoint dynamically by passing the geocoder a street address stored in my database.
But rather than just setting the map view to the address, I want to draw a marker at the address' location.
Template.vendorPage.rendered = function(){
//get address from database by ID
address = function(){
pathname =location.pathname.split("/");
thisId = pathname[2];
return Vendors.findOne({_id: thisId}).address
}
//set variable to the address function
thisAddress = address();
//draw the mapbox
L.mapbox.accessToken = '<My Token Here>';
var geocoder = L.mapbox.geocoder('mapbox.places-v1'),
map = L.mapbox.map('map', 'alexnetsch.j786e624');
geocoder.query(thisAddress, showMap);
function showMap(err, data) {
// The geocoder can return an area, like a city, or a
// point, like an address. Here we handle both cases,
// by fitting the map bounds to an area or zooming to a point.
if (data.lbounds) {
map.fitBounds(data.lbounds);
} else if (data.latlng) {
map.setView([data.latlng[0], data.latlng[1]], 16);
}
}
}
Played around with the documentation for hours and can't figure it out. I'd like to simply pass the marker function 'thisAddress'
Seems like rather than setting the viewport, I could set the map to be zoomedin and centered around my marker.
Here is the example from the documentation but without Geocoding the location.
L.mapbox.accessToken = 'pk.eyJ1IjoiYWxleG5ldHNjaCIsImEiOiJsX0V6Wl9NIn0.i14NX5hv3bkVIi075nOM2g';
var map = L.mapbox.map('map', 'examples.map-20v6611k')
.setView([38.91338, -77.03236], 16);
L.mapbox.featureLayer({
// this feature is in the GeoJSON format: see geojson.org
// for the full specification
type: 'Feature',
geometry: {
type: 'Point',
// coordinates here are in longitude, latitude order because
// x, y is the standard for GeoJSON and many formats
coordinates: [
-77.03221142292,
38.913371603574
]
},
properties: {
title: 'Peregrine Espresso',
description: '1718 14th St NW, Washington, DC',
// one can customize markers by adding simplestyle properties
// https://www.mapbox.com/foundations/an-open-platform/#simplestyle
'marker-size': 'large',
'marker-color': '#BE9A6B',
'marker-symbol': 'cafe'
}
}).addTo(map);
Figured it out finally.
Template.vendorPage.rendered = function(){
address = function(){
pathname =location.pathname.split("/");
thisId = pathname[2];
return Vendors.findOne({_id: thisId}).address
}
thisAddress = address();
//draw the mapbox
L.mapbox.accessToken = 'pk.eyJ1IjoiYWxleG5ldHNjaCIsImEiOiJsX0V6Wl9NIn0.i14NX5hv3bkVIi075nOM2g';
var geocoder = L.mapbox.geocoder('mapbox.places-v1'),
map = L.mapbox.map('map', 'alexnetsch.j786e624');
geocoder.query(thisAddress, showMap);
function showMap(err, data) {
// The geocoder can return an area, like a city, or a
// point, like an address. Here we handle both cases,
// by fitting the map bounds to an area or zooming to a point.
if (data.lbounds) {
map.fitBounds(data.lbounds);
} else if (data.latlng) {
map.setView([data.latlng[0], data.latlng[1]], 16);
}
}
var addMarker;
addMarker = function(geocoder, map, placeName) {
return geocoder.query(placeName, function(error, result) {
var marker;
marker = L.marker(result.latlng);
return marker.addTo(map);
});
};
addMarker(geocoder, map, thisAddress);

Categories