I am trying to create a visualization that shows all the trips I have completed in 2022. I found that DeckGL had the highest chance of helping me accomplish this, but I am not able to dynamically access the data of the current trip being drawn (lat, lng, timestamp) on the map.
I would like to be able to pan to the coordinate on the map where the trip is taking place, and make it smooth. Currently, I have hardcoded the values in using currentTime, but I would like to have a more robust and less cluttered way of doing this.
I am not very proficient in Java-/Type-script but I have been using the tutorial and reading all the docs, have been trying to solve this for 2 days. Nothing stood out to me. Please help!
My current implementation is as follows:
// import { GoogleMapsOverlay } from "#deck.gl/google-maps";
// import { TripsLayer } from "deck.gl";
const GoogleMapsOverlay = deck.GoogleMapsOverlay;
const TripsLayer = deck.TripsLayer;
interface Data {
path: [number, number][];
timestamps: number[];
}
const DATA_URL =
"https://raw.githubusercontent.com/charlieforward9/animated_heatmap/master/data/2022.json";
const LOOP_LENGTH = 31557600;
function initMap(): void {
const map = new google.maps.Map(
document.getElementById("map") as HTMLElement,
{
center: { lat: 29.64462421696083, lng: -82.33479384825146},
mapId: '1ae1962daafbdd69',
tilt: 45,
zoom: 12,
disableDefaultUI: true,
} as google.maps.MapOptions
);
const view = new deck.MapView({id:"view", x:29.64462421696083, y: -82.33479384825146, width: 300, height: 200});
let currentTime = -1000000;
let playSpeed = 30000;
const props = {
id: "trips",
data: DATA_URL,
getPath: (d: Data) =>d.path,
getTimestamps: (d: Data) => d.timestamps,
getColor: [255, 87, 51],
opacity: 1,
widthMinPixels: 4,
trailLength: 31557600,
currentTime,
shadowEnabled: false,
jointRounded: true,
capRounded: true
};
//Hardcoded panning
function autoAnimate(): void {
window.setInterval(viewportAnimation, 80);
}
function viewportAnimation(): void {
const heading = map.getHeading() || 0;
const zoom = map.getZoom() || 10;
const tilt = map.getTilt() || 45;
console.log(currentTime);
if (currentTime < 3600000) {
//Gainesville
map.setHeading(heading + 0.1);
map.setZoom(zoom - 0.01);
} else if (currentTime < 4000000) {
//Orlando SoFlo
map.setHeading(0);
map.setZoom(zoom + 0.1);
map.setTilt(tilt - 0.1);
map.panTo({ lat: 28.6024, lng: -81.2001});
} else if (currentTime < 7300000) {
//Gainesville
map.setHeading(heading - 0.1);
map.setZoom(zoom - 0.01);
map.panTo({ lat: 29.64462421696083, lng: -82.33479384825146});
}
}
const overlay = new GoogleMapsOverlay({});
const animate = () => {
currentTime = (currentTime + playSpeed) % LOOP_LENGTH;
const tripsLayer = new TripsLayer({
...props,
currentTime,
});
overlay.setProps({
layers: [tripsLayer],
});
window.requestAnimationFrame(animate);
};
window.requestAnimationFrame(animate);
autoAnimate()
overlay.setMap(map);
}
declare global {
interface Window {
initMap: () => void;
}
}
window.initMap = initMap;
export {};
Related
I am trying to create a cluster map with multiple locations in the same area. Now, I have this pop-up with data which is coming from my API. I have a click event that toggles the pop-up on click for each location. But the problem is even when I specified the click unique ID to toggle it still opens all the pop-ups. Also, the zoom is not working for the clusters count circles.
This is my code:
import mapboxgl from 'mapbox-gl';
import MapboxLanguage from '#mapbox/mapbox-gl-language';
import apiFetch from '#wordpress/api-fetch';
(() => {
const mapContainer = document.querySelector('[data-gewoon-wonen]');
if (!mapContainer) {
return;
}
mapboxgl.accessToken = process.env.MAP_TOKEN_KEY;
const filterGroup = document.getElementById('map-filter');
const resetButton = filterGroup.querySelector('.hidden');
const center = [4.387733, 51.862419];
const locations = {
type: 'FeatureCollection',
features: []
};
apiFetch({ path: '/wp-json/wp/v2/map?_embed' }).then((maps) => {
maps.forEach((item) => {
const {
id,
title: { rendered: title },
_embedded,
acf
} = item;
const image =
_embedded && _embedded['wp:featuredmedia'][0]?.source_url;
const {
map_location_subtitle: subtitle,
map_location_delivery: delivery,
map_location_project: project,
map_location_content: description,
map_location_coordinates_lat: lat,
map_location_coordinates_lng: lng,
map_location_status: status,
map_location_website: website
} = acf;
const getStatus = (currentStatus) => {
let statusObj = {
bouwfase: 'marker-gray',
planfase: 'marker-bright-pink',
opgeleverd: 'marker-bright-blue',
default: ''
};
let icon = statusObj[currentStatus] || statusObj['default'];
return icon;
};
const object = {
type: 'Feature',
properties: {
id,
status,
image,
icon: getStatus(status),
title,
subtitle,
project,
website,
delivery,
description
},
geometry: {
type: 'Point',
coordinates: [lng, lat]
}
};
locations.features.push(object);
});
});
const map = new mapboxgl.Map({
container: mapContainer,
style: 'mapbox://styles/.../clcz9eocm000p14o3vh42tqfj',
center,
zoom: 10,
minZoom: 10,
maxZoom: 18,
attributionControl: false,
cooperativeGestures: true
});
map.addControl(
new MapboxLanguage({
defaultLanguage: 'mul'
})
);
const resetActiveItems = () => {
document
.querySelectorAll('.active')
.forEach((e) => e.classList.remove('active'));
};
const closePopUps = () => {
const popups = document.getElementsByClassName('mapboxgl-popup');
if (popups.length) {
popups[0].remove();
}
};
map.on('load', () => {
map.addSource('locations', {
type: 'geojson',
data: locations,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)s
});
locations.features.forEach((location) => {
const {
description,
delivery,
title,
subtitle,
project,
status,
image,
website,
id
} = location.properties;
const { coordinates } = location.geometry;
const layerID = `status-${status}-${id}`;
// Add a layer for this symbol type if it hasn't been added already.
if (!map.getLayer(layerID)) {
map.addLayer({
id: layerID,
type: 'symbol',
source: 'locations',
layout: {
'icon-image': ['get', 'icon'],
'icon-size': 0.9,
'icon-allow-overlap': true
},
// filter: ['==', 'id', id]
filter: ['!', ['has', 'point_count']]
});
map.addLayer({
id: `clusters-${id}`,
type: 'circle',
source: 'locations',
filter: ['has', 'point_count'],
paint: {
// Use step expressions (https://docs.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
// with three steps to implement three types of circles:
// * Blue, 20px circles when point count is less than 100
// * Yellow, 30px circles when point count is between 100 and 750
// * Pink, 40px circles when point count is greater than or equal to 750
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
});
map.addLayer({
id: `cluster-count-${id}`,
type: 'symbol',
source: 'locations',
filter: ['has', 'point_count'],
layout: {
'text-field': ['get', 'point_count_abbreviated'],
'text-font': [
'DIN Offc Pro Medium',
'Arial Unicode MS Bold'
],
'text-size': 12
}
});
}
const hasImage = image
? `<img src=${image} class="map__image" />`
: '';
const statusClass = image ? 'map__status--overlay' : '';
const popup = new mapboxgl.Popup({
offset: 25,
maxWidth: null,
className: image ? 'map__content--image' : ''
});
const content = `
${hasImage}
<span class="map__status map__status--${status} ${statusClass}">${status}</span>
<h4 class="map__title">${title}</h4>
<p class="map__subtitle">${subtitle}</p>
<p class="map__subtitle">${
delivery ? `Oplevering ${delivery}` : ''
}</p>
<p class="map__project">${project}</p>
<p class="map__info">${description}</p>
Meer informatie
`;
map.on('click', layerID, (e) => {
// Copy coordinates array.
const coordinateSlice = coordinates.slice();
resetButton.classList.remove('hidden');
// Ensure that if the map is zoomed out such that multiple
// copies of the feature are visible, the popup appears
// over the copy being pointed to.
while (Math.abs(e.lngLat.lng - coordinateSlice[0]) > 180) {
coordinateSlice[0] +=
e.lngLat.lng > coordinateSlice[0] ? 360 : -360;
}
popup.setLngLat(coordinates).setHTML(content).addTo(map);
});
// Change the cursor to a pointer when the mouse is over the places layer.
map.on('mouseenter', layerID, () => {
map.getCanvas().style.cursor = 'pointer';
});
// Change it back to a pointer when it leaves.
map.on('mouseleave', layerID, () => {
map.getCanvas().style.cursor = '';
});
});
const statusTypes = locations.features.map(
(feature) => feature.properties.status
);
const uniqueStatusTypes = Array.from(new Set(statusTypes));
const newGeoJSON = { ...locations };
resetButton.addEventListener('click', () => {
newGeoJSON.features = [...locations.features];
map.getSource('locations').setData(newGeoJSON);
map.setZoom(10).setCenter(center);
resetButton.classList.add('hidden');
resetActiveItems();
closePopUps();
});
const sortedStatusTypes = [
uniqueStatusTypes[0],
uniqueStatusTypes[2],
uniqueStatusTypes[1]
].filter((status) => status !== undefined && status !== null);
sortedStatusTypes.forEach((statusType) => {
const input = document.createElement('input');
input.value = statusType;
input.name = statusType;
input.type = 'button';
input.className = `status--${statusType}`;
input.innerText = statusType;
filterGroup.appendChild(input);
input.onclick = () => {
const statusType = input.value;
resetActiveItems();
closePopUps();
if (statusType) {
input.classList.add('active');
resetButton.classList.remove('hidden');
newGeoJSON.features = locations.features.filter(
(feature) => feature.properties.status === statusType
);
}
map.getSource('locations').setData(newGeoJSON);
};
});
// disable map rotation using right click + drag
map.dragRotate.disable();
// disable map rotation using touch rotation gesture
map.touchZoomRotate.disableRotation();
// Add zoom and rotation controls to the map.
const nav = new mapboxgl.NavigationControl({
showCompass: false
});
const fullscreen = new mapboxgl.FullscreenControl();
map.addControl(fullscreen, 'top-right');
map.addControl(nav, 'bottom-right');
});
})();
I want to create a map like this image
in open layers.
How we can create this type map in open layers and how we get API service that will ping coordinate in sequence every 3 seconds (to simulate 3rd party service)
I want to integrate this map in this code. I have no idea ho we can do this
import Feature from 'ol/Feature';
import LineString from 'ol/geom/LineString';
import Map from 'ol/Map';
import Point from 'ol/geom/Point';
import Polyline from 'ol/format/Polyline';
import VectorSource from 'ol/source/Vector';
import View from 'ol/View';
import OSM from 'ol/source/OSM';
import {Icon, Stroke, Style} from 'ol/style';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import {getVectorContext} from 'ol/render';
const center = [-5639523.95, -3501274.52];
const map = new Map({
target: document.getElementById('map'),
view: new View({
center: center,
zoom: 10,
minZoom: 2,
maxZoom: 19,
}),
layers: [
new TileLayer({
source: new OSM(),
}),
],
});
// The polyline string is read from a JSON similiar to those returned
// by directions APIs such as Openrouteservice and Mapbox.
fetch('data/polyline/route.json').then(function (response) {
response.json().then(function (result) {
const polyline = result.routes[0].geometry;
const route = new Polyline({
factor: 1e6,
}).readGeometry(polyline, {
dataProjection: 'EPSG:4326',
featureProjection: 'EPSG:3857',
});
const routeFeature = new Feature({
type: 'route',
geometry: route,
});
const startMarker = new Feature({
type: 'icon',
geometry: new Point(route.getFirstCoordinate()),
});
const endMarker = new Feature({
type: 'icon',
geometry: new Point(route.getLastCoordinate()),
});
const position = startMarker.getGeometry().clone();
const geoMarker = new Feature({
type: 'geoMarker',
geometry: position,
});
const styles = {
route: new Style({
stroke: new Stroke({
width: 6,
color: [237, 212, 0, 0.8],
}),
}),
icon: new Style({
image: new Icon({
anchor: [0.5, 1],
src: 'data/icon.png',
}),
}),
geoMarker: new Style({
image: new Icon({
src:
'https://cdn1.iconfinder.com/data/icons/basic-ui-elements-color-round/3/19-32.png',
rotation: getAngleAt(route, 0) + Math.PI / 2,
}),
}),
};
const vectorLayer = new VectorLayer({
source: new VectorSource({
features: [routeFeature, geoMarker, startMarker, endMarker],
}),
style: function (feature) {
return styles[feature.get('type')];
},
});
map.addLayer(vectorLayer);
const speedInput = document.getElementById('speed');
const startButton = document.getElementById('start-animation');
let animating = false;
function getAngleAt(lineString, distance) {
const length = lineString.getLength();
const coordinates = lineString.getCoordinates();
for (let i = 1, len = coordinates.length; i <= len; ++i) {
if (
new LineString(coordinates.slice(0, i + 1)).getLength() >=
length * distance
) {
return -Math.atan2(
coordinates[i][1] - coordinates[i - 1][1],
coordinates[i][0] - coordinates[i - 1][0]
);
}
}
}
const lastTimes = [];
function moveFeature(event) {
const speed = Number(speedInput.value);
const time = event.frameState.time;
for (let i = 0, ii = lastTimes.length; i < ii; ++i) {
let {lastTime, distance} = lastTimes[i];
const elapsedTime = time - lastTime;
distance = (distance + (speed * elapsedTime) / 1e6) % 2;
lastTime = time;
lastTimes[i] = {lastTime, distance};
const lineDistance = distance > 1 ? 2 - distance : distance;
const direction = distance > 1 ? -Math.PI / 2 : Math.PI / 2;
const currentCoordinate = route.getCoordinateAt(lineDistance);
const angle = getAngleAt(route, lineDistance) + direction;
styles.geoMarker.getImage().setRotation(angle);
position.setCoordinates(currentCoordinate);
const vectorContext = getVectorContext(event);
vectorContext.setStyle(styles.geoMarker);
vectorContext.drawGeometry(position);
}
// tell OpenLayers to continue the postrender animation
map.render();
}
function startAnimation() {
lastTimes.push({lastTime: Date.now(), distance: 0});
if (!animating) {
animating = true;
//startButton.textContent = 'Stop Animation';
vectorLayer.on('postrender', moveFeature);
// hide geoMarker and trigger map render through change event
geoMarker.setGeometry(null);
}
}
startButton.addEventListener('click', startAnimation);
});
});
{
"name": "feature-move-animation",
"dependencies": {
"ol": "7.1.0"
},
"devDependencies": {
"vite": "^3.0.3",
"#babel/core": "latest"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Marker Animation</title>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<style>
.map {
width: 100%;
height: 400px;
}
</style>
</head>
<body>
<div id="map" class="map"></div>
<label for="speed">
speed:
<input id="speed" type="range" min="10" max="999" step="10" value="60">
</label>
<button id="start-animation">Start Animation</button>
<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
<script src="https://unpkg.com/elm-pep#1.0.6/dist/elm-pep.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>
Using LeafletJS and JavaScript........................................
I'm having an issue with my map. It's getting removed when I drag my marker, is there a way to fix it?
function to drag marker and route
// marker and route dragging enable
marker.dragging.enable();
marker.on("dragend", (e) => {
console.log("Marker has been moved!!!");
var lat = e.target.getLatLng().lat;
var lng = e.target.getLatLng().lng;
var pair = {lat: lat, lng: lng};
markers.set(pair, marker);
routes = [];
const tempMarker = []
markers.forEach((v, index) => {
tempMarker.push(v.getLatLng())
})
console.log({tempMarker});
**for (let i in map._layers) {
if (map._layers[i]._path != undefined) {
map.removeControl(map._layers[i]);
try {
markers.removeLayer(map._layers[i]);
} catch (e) {
console.log(e);
}
}
}**
// loop markers and move polyline
tempMarker.forEach((v, index) => {
// const lt = marker.getLatLng();
console.log('loop', v);
if (tempMarker[index + 1]) {
const nPolyline = L.polyline(
[v, tempMarker[index + 1]], {
enableDraggableLines: true,
color: "black",
weight: 5,
opacity: 0.5,
smoothFactor: 1,
}
).addTo(map);
routes.push(nPolyline);
}
})
console.log({map, routes, markers})
});
// function to drag marker and route ends here
This area area below is where it makes my map remove. how can I fix this issue?
**
for (let i in map._layers) {
if (map._layers[i]._path != undefined) {
map.removeControl(map._layers[i]);
try {
markers.removeLayer(map._layers[i]);
} catch (e) {
console.log(e);
}
}
}
**
I've searched high and low for proper documentation on how to configure Azure Maps with Angular and haven't found anything. How do I do this?
(Please look to the comments for my self-answered question)
As documentation for configuring Azure Maps with Angular does not exist, this post will accomplish that instead. By the end of this post, you should have a working Angular version of Azure Maps with map markers. Before adding any code, please follow the steps from the Microsoft website to set up your Azure Map keys: https://learn.microsoft.com/en-us/azure/azure-maps/
The first step to create your Azure Maps component is to create a new Angular component and add the following to your .html file:
<div id="azure-map"></div>
The id can be used for styling your component in the .scss file.
Next, we will work on the .ts file. First, let's set up the map. We'll add the following class variables for the map and coordinates:
map: any;
defaultLat: number = 47.608013; // Seattle coordinates
defaultLng: number = -122.335167;
and this output to emit coordinates to the map's parent component:
#Output() outputCoordinates: EventEmitter<number[]> = new EventEmitter<number[]>();
Now we will make a function called InitMap() and add this code snippet inside to initialize the base map and its properties:
this.map = new atlas.Map('azure-map', {
center: [this.defaultLng, this.defaultLat],
zoom: 12,
language: 'en-US',
showLogo: true,
showFeedbackLink: false,
dragRotateInteraction: false,
authOptions: {
authType: AuthenticationType.subscriptionKey,
subscriptionKey: 'YOUR_SUBSCRIPTION_KEY_HERE'
}
});
Next, we will add this code snippet inside InitMap() to register the map click hander and zoom controls:
this.map.events.add('ready', () => {
// Register the map click handler
this.map.events.add('click', (e) => {
this.outputCoordinates.emit([e.position[0], e.position[1]]); // 0 = longitude, 1 = latitude
});
//Construct a zoom control and add it to the map.
this.map.controls.add(new atlas.control.ZoomControl({
style: ControlStyle.auto,
zoomDelta: 1
}), {position: ControlPosition.BottomLeft});
});
We must also call the InitMap() function inside of ngOnInit().
The next step is to create the functionality to allow the user to drop and move pins on the map. This function will erase the current marker on the map, set the new marker's coordinates, initialize the marker drag handler, and set the boundaries of the map to track the newly placed pin marker. To handle all these operations, we will add this class variable:
markersReference: Marker[] = [];
and this function:
setMarkers(markers: Marker[]) {
if (markers && markers.length > 0) {
this.markersReference = markers;
this.map.markers.clear();
let boundsPositions: Array<{lng: number, lat:number}> = [];
for (let marker of markers) {
if (marker.latitude && marker.longitude) {
let htmlMarker = new atlas.HtmlMarker({
draggable: true,
position: [marker.longitude, marker.latitude] // longitude first
});
// Register the marker drag handler
this.map.events.add('dragend', htmlMarker, (e) => {
var pos = htmlMarker.getOptions().position;
this.outputCoordinates.emit([pos[0], pos[1]]); // 0 = longitude, 1 = latitude
});
boundsPositions.push({lng: marker.longitude, lat: marker.latitude}) // lat, lng
this.map.markers.add(htmlMarker);
}
}
this.map.setCamera({padding: {top: 20, bottom: 20, left: 20, right: 20}, maxZoom: 16,
bounds: atlas.data.BoundingBox.fromLatLngs(boundsPositions)});
}
Now we will add a function that allows us to center the map focus onto the dropped pin:
centerMapWithCoords(lon: number, lat: number) {
this.map.setCamera({zoom: 12, maxZoom: 16, center: [lon, lat]});
}
Lastly, in order to pick up changes that the user makes to the map, we will subscribe to the map subject and its markers. Add these inputs alongside your class variables:
#Input() markerDataSubject: Subject<Marker[]> = new Subject<Marker[]>();
#Input() centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
Next, add these subscriptions to your ngOnInit():
this.subscriptions.push((this.centerMapSubject).asObservable().subscribe((coords) =>
this.centerMapWithCoords(coords.lng, coords.lat)));
this.subscriptions.push((this.markerDataSubject).asObservable().subscribe((markers) =>
this.setMarkers(markers)));
And unsubscribe when the component is closed:
ngOnDestroy() {
for (const s of this.subscriptions) {
s.unsubscribe();
}
}
Overall, the class in your .ts file should look similar to the following:
export class AzureMapComponent implements OnInit {
#Input() markerDataSubject: Subject<Marker[]> = new Subject<Marker[]>();
#Input() centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
#Output() outputCoordinates: EventEmitter<number[]> = new EventEmitter<number[]>();
subscriptions: Subscription[] = [];
map: any;
markersReference: Marker[] = [];
defaultLat: number = 47.608013; // Seattle coordinates
defaultLng: number = -122.335167;
ngOnInit() {
this.InitMap();
this.subscriptions.push((this.centerMapSubject).asObservable().subscribe((coords) =>
this.centerMapWithCoords(coords.lng, coords.lat)));
this.subscriptions.push((this.markerDataSubject).asObservable().subscribe((markers) =>
this.setMarkers(markers)));
}
//Create an instance of the map control and set some options.
InitMap() {
this.map = new atlas.Map('azure-map', {
center: [this.defaultLng, this.defaultLat],
zoom: 12,
language: 'en-US',
showLogo: true,
showFeedbackLink: false,
dragRotateInteraction: false,
authOptions: {
authType: AuthenticationType.subscriptionKey,
subscriptionKey: 'YOUR_SUBSCRIPTION_KEY_HERE'
}
});
this.map.events.add('ready', () => {
// Register the map click handler
this.map.events.add('click', (e) => {
this.outputCoordinates.emit([e.position[0], e.position[1]]); // 0 = longitude, 1 = latitude
});
//Construct a zoom control and add it to the map.
this.map.controls.add(new atlas.control.ZoomControl({
style: ControlStyle.auto,
zoomDelta: 1
}), {position: ControlPosition.BottomLeft});
});
}
setMarkers(markers: Marker[]) {
if (markers && markers.length > 0) {
this.markersReference = markers;
this.map.markers.clear();
let boundsPositions: Array<{lng: number, lat:number}> = [];
for (let marker of markers) {
if (marker.latitude && marker.longitude) {
let htmlMarker = new atlas.HtmlMarker({
draggable: true,
position: [marker.longitude, marker.latitude] // longitude first
});
// Register the marker drag handler
this.map.events.add('dragend', htmlMarker, (e) => {
var pos = htmlMarker.getOptions().position;
this.outputCoordinates.emit([pos[0], pos[1]]); // 0 = longitude, 1 = latitude
});
boundsPositions.push({lng: marker.longitude, lat: marker.latitude}) // lat, lng
this.map.markers.add(htmlMarker);
}
}
this.map.setCamera({padding: {top: 20, bottom: 20, left: 20, right: 20}, maxZoom: 16,
bounds: atlas.data.BoundingBox.fromLatLngs(boundsPositions)});
}
}
centerMapWithCoords(lon: number, lat: number) {
this.map.setCamera({zoom: 12, maxZoom: 16, center: [lon, lat]});
}
ngOnDestroy() {
for (const s of this.subscriptions) {
s.unsubscribe();
}
}
}
Now that your Azure Maps component is complete, all you have to do is call an instance of your component within the .html of whatever view you'd like to place it in and coordinate the required inputs and output:
<app-azure-map
[markerDataSubject]="locationMarkerSubject"
[centerMapSubject]="centerMapSubject"
(outputCoordinates)="updateCoordinates($event)">
</app-azure-map>
The input subjects on your parent component should look something like this:
locationMarkerSubject: Subject<Marker[]> = new Subject<Marker[]>();
centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
And your updateCoordinates() function will handle the marker data sent back from user input upon clicking the map.
Am using vue and have installed the vue-mapbox component located here: https://soal.github.io/vue-mapbox/#/quickstart
I have updated the js and css to the latest versions also that gets added to the index.html:
<!-- Mapbox GL CSS -->
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.51.0/mapbox-gl.css" rel="stylesheet" />
<!-- Mapbox GL JS -->
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v0.51.0/mapbox-gl.js"></script>
I am trying to utilize this component to set the default view of the map bounds using either center or bounds or fitBounds to a list of Lng,Lat coordinates. So, basically, how to plug in lng,lat coordinates and have the map default to centering these coordinates inside of the container?
Here's a Component I created, called Map in vue to output the mapbox using the component vue-mapbox listed above:
<template>
<b-row id="map" class="d-flex justify-content-center align-items-center my-2">
<b-col cols="24" id="map-holder" v-bind:class="getMapType">
<mgl-map
id="map-obj"
:accessToken="accessToken"
:mapStyle.sync="mapStyle"
:zoom="zoom"
:center="center"
container="map-holder"
:interactive="interactive"
#load="loadMap"
ref="mapbox" />
</b-col>
</b-row>
</template>
<script>
import { MglMap } from 'vue-mapbox'
export default {
components: {
MglMap
},
data () {
return {
accessToken: 'pk.eyJ1Ijoic29sb2dob3N0IiwiYSI6ImNqb2htbmpwNjA0aG8zcWxjc3IzOGI1ejcifQ.nGL4NwbJYffJpjOiBL-Zpg',
mapStyle: 'mapbox://styles/mapbox/streets-v9', // options: basic-v9, streets-v9, bright-v9, light-v9, dark-v9, satellite-v9
zoom: 9,
map: {}, // Holds the Map...
fitBounds: [[-79, 43], [-73, 45]]
}
},
props: {
interactive: {
default: true
},
resizeMap: {
default: false
},
mapType: {
default: ''
},
center: {
type: Array,
default: function () { return [4.899, 52.372] }
}
},
computed: {
getMapType () {
let classes = 'inner-map'
if (this.mapType !== '') {
classes += ' map-' + this.mapType
}
return classes
}
},
watch: {
resizeMap (val) {
if (val) {
this.$nextTick(() => this.$refs.mapbox.resize())
}
},
fitBounds (val) {
if (this.fitBounds.length) {
this.MoveMapCoords()
}
}
},
methods: {
loadMap () {
if (this.map === null) {
this.map = event.map // store the map object in here...
}
},
MoveMapCoords () {
this.$refs.mapbox.fitBounds(this.fitBounds)
}
}
}
</script>
<style lang="scss" scoped>
#import '../../styles/custom.scss';
#map {
#map-obj {
text-align: justify;
width: 100%;
}
#map-holder {
&.map-modal {
#map-obj {
height: 340px;
}
}
&.map-large {
#map-obj {
height: 500px;
}
}
}
.mapboxgl-map {
border: 2px solid lightgray;
}
}
</style>
So, I'm trying to use fitBounds method here to get the map to initialize centered over 2 Lng,Lat coordinates here: [[-79, 43], [-73, 45]]
How to do this exactly? Ok, I think I might have an error in my code a bit, so I think the fitBounds should look something like this instead:
fitBounds: () => {
return { bounds: [[-79, 43], [-73, 45]] }
}
In any case, having the most difficult time setting the initial location of the mapbox to be centered over 2 or more coordinates. Anyone do this successfully yet?
Ok, so I wound up creating a filter to add space to the bbox like so:
Vue.filter('addSpaceToBBoxBounds', function (value) {
if (value && value.length) {
var boxArea = []
for (var b = 0, len = value.length; b < len; b++) {
boxArea.push(b > 1 ? value[b] + 2 : value[b] - 2)
}
return boxArea
}
return value
})
This looks to be good enough for now. Than just use it like so:
let line = turf.lineString(this.markers)
mapOptions['bounds'] = this.$options.filters.addSpaceToBBoxBounds(turf.bbox(line))
return mapOptions
setting the initial location of the map to be centered over 2 or
more coordinates
You could use Turf.js to calculate the bounding box of all point features and initialize the map with this bbox using the bounds map option:
http://turfjs.org/docs#bbox
https://www.mapbox.com/mapbox-gl-js/api/#map
I created a few simple functions to calculate a bounding box which contains the most southwestern and most northeastern corners of the given [lng, lat] pairs (markers). You can then use Mapbox GL JS map.fitBounds(bounds, options?) function to zoom the map to the set of markers.
Always keep in mind:
lng (lon): longitude (London = 0, Bern = 7.45, New York = -74)
→ the lower, the more western
lat: latitude (Equator = 0, Bern = 46.95, Capetown = -33.9)
→ the lower, the more southern
getSWCoordinates(coordinatesCollection) {
const lowestLng = Math.min(
...coordinatesCollection.map((coordinates) => coordinates[0])
);
const lowestLat = Math.min(
...coordinatesCollection.map((coordinates) => coordinates[1])
);
return [lowestLng, lowestLat];
}
getNECoordinates(coordinatesCollection) {
const highestLng = Math.max(
...coordinatesCollection.map((coordinates) => coordinates[0])
);
const highestLat = Math.max(
...coordinatesCollection.map((coordinates) => coordinates[1])
);
return [highestLng, highestLat];
}
calcBoundsFromCoordinates(coordinatesCollection) {
return [
getSWCoordinates(coordinatesCollection),
getNECoordinates(coordinatesCollection),
];
}
To use the function, you can just call calcBoundsFromCoordinates and enter an array containing all your markers coordinates:
calcBoundsFromCoordinates([
[8.03287, 46.62789],
[7.53077, 46.63439],
[7.57724, 46.63914],
[7.76408, 46.55193],
[7.74324, 46.7384]
])
// returns [[7.53077, 46.55193], [8.03287, 46.7384]]
Overall it might even be easier to use Mapbox' mapboxgl.LngLatBounds() function.
As mentioned in the answer from jscastro in Scale MapBox GL map to fit set of markers you can use it like this:
const bounds = mapMarkers.reduce(function (bounds, coord) {
return bounds.extend(coord);
}, new mapboxgl.LngLatBounds(mapMarkers[0], mapMarkers[0]));
And then just call
map.fitBounds(bounds, {
padding: { top: 75, bottom: 30, left: 90, right: 90 },
});
If you don't want to use yet another library for this task, I came up with a simple way to get the bounding box, here is a simplified vue component.
Also be careful when storing your map object on a vue component, you shouldn't make it reactive as it breaks mapboxgl to do so
import mapboxgl from "mapbox-gl";
export default {
data() {
return {
points: [
{
lat: 43.775433,
lng: -0.434319
},
{
lat: 44.775433,
lng: 0.564319
},
// Etc...
]
}
},
computed: {
boundingBox() {
if (!Array.isArray(this.points) || !this.points.length) {
return undefined;
}
let w, s, e, n;
// Calculate the bounding box with a simple min, max of all latitudes and longitudes
this.points.forEach((point) => {
if (w === undefined) {
n = s = point.lat;
w = e = point.lng;
}
if (point.lat > n) {
n = point.lat;
} else if (point.lat < s) {
s = point.lat;
}
if (point.lng > e) {
e = point.lng;
} else if (point.lng < w) {
w = point.lng;
}
});
return [
[w, s],
[e, n]
]
},
},
watch: {
// Automatically fit to bounding box when it changes
boundingBox(bb) {
if (bb !== undefined) {
const cb = () => {
this.$options.map.fitBounds(bb, {padding: 20});
};
if (!this.$options.map) {
this.$once('map-loaded', cb);
} else {
cb();
}
}
},
// Watch the points to add the markers
points: {
immediate: true, // Run handler on mount (not needed if you fetch the array of points after it's mounted)
handler(points, prevPoints) {
// Remove the previous markers
if (Array.isArray(prevPoints)) {
prevPoints.forEach((point) => {
point.marker.remove();
});
}
//Add the new markers
const cb = () => {
points.forEach((point) => {
// create a HTML element for each feature
const el = document.createElement('div');
el.className = 'marker';
el.addEventListener('click', () => {
// Marker clicked
});
el.addEventListener('mouseenter', () => {
point.hover = true;
});
el.addEventListener('mouseleave', () => {
point.hover = false;
});
// make a marker for each point and add to the map
point.marker = new mapboxgl.Marker(el)
.setLngLat([point.lng, point.lat])
.addTo(this.$options.map);
});
};
if (!this.$options.map) {
this.$once('map-loaded', cb);
} else {
cb();
}
}
}
},
map: null, // This is important to store the map without reactivity
methods: {
mapLoaded(map) {
this.$options.map = map;
this.$emit('map-loaded');
},
},
}
It should work fine as long as your points aren't in the middle of the pacific juggling between 180° and -180° of longitude, if they are, simply adding a check to invert east and west in the return of the bounding box should do the trick