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
Related
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 {};
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);
}
}
}
**
Every time the Map is loading i am calling 500ms after finished the loading process the this.map.invalidateSize()-Method to try to clean the map view up.
But also when I am loading this, the map still appears like this:
What do i have to do, that the map loads directly in my location or at least in the defined center with the defined zoom-Level?
Is there any event, that I can listen for on Angular/Leaflet, which indicates, that the map has loaded properly and also the loaded KML-Data is loaded?
EDIT
The following code is producing this error. Please take note, that i have removed unused content, which is not relevant for this problem (like adding a marker to the map).
ngOnInit() {
this.initMap();
}
processBaseLayers() {
this.layerControl = L.control
.layers(this.layerControl, null, {
position: 'bottomright',
})
.addTo(this.map);
let defaultLayerSet = false;
this.baseLayers.forEach((layer) => {
const baseLayer = new L.TileLayer(layer.serverURL, {
maxZoom: layer.maxZoom,
attribution: layer.attribution,
});
if (!defaultLayerSet) {
baseLayer.addTo(this.map);
defaultLayerSet = true;
}
this.layerControl.addBaseLayer(baseLayer, layer.name);
});
}
fixMapOccurences() {
setTimeout(() => { this.map.invalidateSize() }, 500)
}
async initMap() {
this.map = L.map('map', {
center: [46.947222222222, 7.4441666666667],
zoom: 12,
zoomControl: false,
});
this.map.whenReady(() => {
this.processBaseLayers();
setTimeout(async () => {
this.fixMapOccurences();
await this.loadKML();
}, 1000)
});
}
async loadKML() {
this.dataLayers.forEach((dataLayer) => {
const kmlLayer = omnivore.kml(dataLayer.serverURL).on('ready', () => {
kmlLayer.eachLayer((layer) => {
if (layer.feature.geometry.type === 'Point') {
// adding point here
} else if (layer.feature.geometry.type === 'GeometryCollection') {
// adding polyline here
}
});
});
});
}
when user get close to some area on map, display specific data from one other function? I am calculating average of each area estate prices in computed functions. it's below. you can see the average function on jsfiddle...
Already displaying averages but, what I need to do here, when user get zoomed in that region/city then display that areas average... The original code with map down below...
For example, how to sets bounds and connect those bounds to average function??? Thank you for helping.!
code updated!
data() {
return {
avg:"",
map: {},
mapName: "map",
estates: [],
},
mounted() {
axios.get('/ajax').then((response) => {
this.estates = response.data
});
this.initMap();
},
methods: {
initMap: function(){
var mapOptions =
{
zoom : 6,
center : {
lat:34.652500,
lng:135.506302
}
};
this.map = new google.maps.Map(document.getElementById(this.mapName), mapOptions);
google.maps.event.addListener(this.map, 'bounds_changed', function() {
console.log("bound changed alert");
});
},
avgArray: function (region) {
const sum = arr => arr.reduce((a,c) => (a += c),0);
const avg = arr => sum(arr) / arr.length;
return avg(region);
},
},
computed: {
groupedPricesByRegion () {
return this.estates.reduce((acc, obj) => {
var key = obj.region;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(obj.m2_price);
return acc;
}, {});
},
averagesByRegion () {
let arr = [];
Object.entries(this.groupedPricesByRegion)
.forEach(([key, value]) => {
arr.push({ [key]: Math.round(this.avgArray(value)) });
});
return arr;
},
},
I don't think this is specific to vue, its more about google-maps.
You can listen to the bounds_changed event on the map object: bounds_changed
And then get the boundaries of your current view
Have a look at this excellent answer which should help you out.
If you are using vuejs, you an look at this library vue-google-maps which should help you out.
P.S make sure to debounce the function you call on bounds_changed or you may make a lot of unnecessary calls to your generating-averages function
I am making a vue project and I want to use leaflet inside of my components. I have the map showing but I run into an error when I try to add a marker to the map. I get
Uncaught TypeError: events.forEach is not a function
at VueComponent.addEvents (VM2537 Map.vue:35)
at e.boundFn (VM2533 vue.esm.js:191)
at HTMLAnchorElement. (leaflet.contextmenu.js:328)
at HTMLAnchorElement.r (leaflet.js:5)
<template>
<div>
<div id="map" class="map" style="height: 781px;"></div>
</div>
</template>
<script>
export default {
data() {
return {
map: [],
markers: null
};
},
computed: {
events() {
return this.$store.state.events;
}
},
watch: {
events(val) {
this.removeEvents();
this.addEvents(val);
}
},
methods: {
addEvents(events) {
const map = this.map;
const markers = L.markerClusterGroup();
const store = this.$store;
events.forEach(event => {
let marker = L.marker(e.latlng, { draggable: true })
.on("click", el => {
store.commit("locationsMap_center", e.latlng);
})
//.bindPopup(`<b> ${event.id} </b> ${event.name}`)
.addTo(this.map);
markers.addLayer(marker);
});
map.addLayer(markers);
this.markers = markers;
},
removeEvent() {
this.map.removeLayer(this.markers);
this.markers = null;
}
},
mounted() {
const map = L.map("map", {
contextmenu: true,
contextmenuWidth: 140,
contextmenuItems: [
{
text: "Add Event Here",
callback: this.addEvents
}
]
}).setView([0, 0], 1);
L.tileLayer("/static/map/{z}/{x}/{y}.png", {
maxZoom: 4,
minZoom: 3,
continuousWorld: false,
noWrap: true,
crs: L.CRS.Simple
}).addTo(map);
this.map = map;
}
};
</script>
New2Dis,
Here is your example running in a jsfiddle.
computed: {
events: function () {
return this.store.events;
}
},
watch: {
events: function (val) {
this.removeEvents();
this.addEvents(val);
}
},
methods: {
addEvents(events) {
console.log("hoi")
const map = this.map;
const markers = L.markerClusterGroup();
const store = this.$store;
events.forEach(event => {
let marker = L.marker(event.latlng, { draggable: true })
.on("click", el => {
//store.commit("locationsMap_center", event.latlng);
})
.bindPopup(`<b> ${event.id} </b> ${event.name}`)
.addTo(this.map);
markers.addLayer(marker);
});
map.addLayer(markers);
this.markers = markers;
},
removeEvents() {
if (this.markers != null) {
this.map.removeLayer(this.markers);
this.markers = null;
}
}
},
I did replace some things to make it works, like the $store as I don't have it, and removeEvent was not written correctly, so I'm not sure what I actually fixed...
I have also created a plugin to make it easy to use Leaflet with Vue.
You can find it here
You will also find a plugin for Cluster group here
Give it a try and let me know what you think.