I have a Svelte application with a Map component as below. I'd like to pass in props for centreCoords and Zoom and then fly to them when they change outside the component.
However, I can't work out how to get the map object outside the onMount function.
<script>
import { onMount, onDestroy } from 'svelte';
import mapboxgl from 'mapbox-gl';
const token = 'xxx';
let mapElement;
let map;
export let zoom;
export let centreCoords = [];
// This but doesn't work
map.flyTo({
center: centreCoords,
zoom: zoom,
essential: true // this animation is considered essential with respect to prefers-reduced-motion
});
onMount(() => {
mapboxgl.accessToken = token;
map = new mapboxgl.Map({
container: mapElement,
zoom: zoom,
center: centreCoords,
// Choose from Mapbox's core styles, or make your own style with Mapbox Studio
style: 'mapbox://styles/mapbox/light-v10',
antialias: true // create the gl context with MSAA antialiasing, so custom layers are antialiased
});
});
onDestroy(async () => {
if (map) {
map.remove();
}
});
</script>
<main>
<div bind:this={mapElement} />
</main>
<style>
#import 'mapbox-gl/dist/mapbox-gl.css';
main div {
height: 800px;
}
</style>
Should be possible to just use a reactive statement with an if:
$: if (map) map.flyTo(...)
Related
With React.js 16 and OpenLayers 6.5 I created a component which displays a map with an overlay:
import React from "react";
import OSM from "ol/source/OSM";
import TileLayer from "ol/layer/Tile";
import Map from "ol/Map";
import View from "ol/View";
import Overlay from "ol/Overlay";
class Map extends React.Component {
constructor(props) {
super(props);
this.mapRef = React.createRef();
this.overlayRef = React.createRef();
}
componentDidMount() {
this.map = new Map({
layers: [
new TileLayer({
source: new OSM(),
}),
],
target: this.mapRef.current,
view: new View({
center: [800000, 5000000],
zoom: 5,
}),
});
const overlay = new Overlay({
position: [800000, 5000000],
element: this.overlayRef.current,
});
this.map.addOverlay(overlay);
}
render() {
return (
<>
<div ref={this.mapRef} id="map"></div>
<div ref={this.overlayRef}>Overlay</div>
</>
);
}
}
export default Map;
This code works fine until the component gets unmounted. Then I receive the error
Uncaught DOMException: Node.removeChild: The node to be removed is not a child of this node
and the app crashes. I guess it happens because OpenLayers is modifying the DOM structure and thus React gets confused.
Does anybody knows how to add an overlay which does not modify the DOM structure? Or any other solution to circumvent the problem?
The problem is that OL Overlay class takes the passed in element this.overlayRef.current and appends it as child to its internal element changing the DOM structure. You can anticipate this and preemptively place your custom overlay element inside Overlay's internal element using React portal:
ReactDOM.createPortal((<div>Overlay</div>), overlay.element)
My Map.vue is below, with parts of the code abbreviated so I don't expose any information. The Map is a component of my app and I would like to add some interactions to it. However, my method won't work, saying read property 'getPitch' of undefined. How can I initialize the map so that Mapbox's functions are still available later on?
<template>
<div id="map" class="map">
<button #click="getPitch">Test</button>
</div>
</template>
<script>
import mapboxgl from 'mapbox-gl';
export default {
name: "Map",
data() {
return {
};
},
props: {
mapdata: {
type: Object,
default: () => { return {}; }
}
},
beforeUpdate() {
mapboxgl.accessToken = "...";
const map = new mapboxgl.Map({
container: "map",
style: "...",
center: ...,
zoom: 11.85,
});
let mapRecords = this.mapdata.data;
map.once('styledata', function() {
map.addSource('...', {
...
});
map.addLayer({
...
})
});
},
methods: {
getPitch() {
map.getPitch();
}
}
};
</script>
<style scoped>
...
</style>
Solved
First, changed all map instances in beforeUpdate() to this.map. Then, in the getPitch function, added this line: let map = this.map;.
beforeUpdate does not look like the right hook. You probably should be using mounted.
Also, your variable map is out of scope for your getPitch method.
I'm trying to update a map to my current location using a vue onClick which updates props and sends them to my map component. I am using a :key to rerender my map component when my map data changes and I get some new x,y for my map center. (based on the esri/arcgis example I would need to rebuild the map, if anyone knows this to be wrong let me know please)
VUE js arcgis starting documentation:
https://developers.arcgis.com/javascript/latest/guide/vue/
for some reason my map does render again and seems like it's about to load but then it just stays blank.
maybe someone can tell me if this is an issue with the component still persisting in some way after I force it to render again?
my app.vue
<template>
<div id="app">
<web-map v-bind:centerX="lat" v-bind:centerY="long" ref="mapRef"/>
<div class="center">
<b-button class="btn-block" #click="getLocation" variant="primary">My Location</b-button>
</div>
</div>
</template>
<script>
import WebMap from './components/webmap.vue';
export default {
name: 'App',
components: { WebMap },
data(){
return{
lat: -118,
long: 34,
}
},
methods:{
showPos(pos){
this.lat = pos.coords.latitude
this.long = pos.coords.longitude
this.$refs.mapRef.updateCoordinates()
console.log('new location',this.lat,this.long, this.$refs)
},
getLocation(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.showPos);
} else {
console.log("Geolocation is not supported by this browser.");
}
},
},
};
</script>
my map component
<template>
<div></div>
</template>
<script>
import { loadModules } from 'esri-loader';
export default {
name: 'web-map',
props:['centerX', 'centerY'],
data: function(){
return{
X: this.centerX,
Y: this.centerY,
view: null
}
},
mounted() {
console.log('new data',this.X,this.Y)
// lazy load the required ArcGIS API for JavaScript modules and CSS
loadModules(['esri/Map', 'esri/views/MapView'], { css: true })
.then(([ArcGISMap, MapView]) => {
const map = new ArcGISMap({
basemap: 'topo-vector'
});
this.view = new MapView({
container: this.$el,
map: map,
center: [this.X,this.Y], ///USE PROPS HERE FOR NEW CENTER
zoom: 8
});
});
},
beforeDestroy() {
if (this.view) {
// destroy the map view
this.view.container = null;
}
},
methods:{
updateCoordinates(){
this.view.centerAt([this.X,this.Y])
}
}
};
</script>
I don't think the key you're passing as a prop to web-map serves any purpose since it's not being used inside the component.
You could try, instead, to force update the component as such:
<web-map v-bind:centerX="lat" v-bind:centerY="long" ref="mapRef" />
this.refs.mapRef.$forceUpdate()
This ensures that you're force updating the whole component, but maybe there's a better solution. Instead of re-rendering the entire component, which means having to create the map once again, you could instead keep the component alive and just use an event to update the coordinates.
Based on https://developers.arcgis.com/javascript/3/jsapi/map-amd.html#centerat, you can re-center the map using the centerAt method.
That way the map component has a method like:
updateCoordinates(coord){
this.view.centerAt(coord)
}
And you can call it on the parent with
this.refs.mapRef.updateCoordinates(newCenter)
Hope it helps, let me know if you do any progress.
I think you can test Watch with setInterVal() for a loop to check your location each 1sec
I am trying to create a map using Leaflet and a Vue component. For some reason the "center: " attribute is not accepting my array coordinates for Latitude and Longitude? When I use it in the template html {{ latlng }} I get an array with the correct coordinates. Any help would be greatly appreciated.
<template>
<div id="mapContainer">{{ latlng }}</div>
</template>
<script>
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import axios from 'axios';
export default {
name: "Map",
data() {
return {
map: null,
latlng: []
};
},
methods: {
get_lat_lng: function(){
axios.get('http://127.0.0.1:5000/api/get_latitude')
.then(res => this.latlng.push(res.data))
axios.get('http://127.0.0.1:5000/api/get_longitude')
.then(res => this.latlng.push(res.data))
}
},
created: function(){
this.get_lat_lng()
},
mounted() {
this.map = L.map("mapContainer", {
center: this.latlng,
zoom: 12,
});
L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
attribution:
'© OpenStreetMap contributors'
}).addTo(this.map);
},
beforeDestroy() {
if (this.map) {
this.map.remove();
}
}
};
</script>
<style scoped>
#mapContainer {
width: 50vw;
height: 50vh;
}
</style>
You have hit a race condition here. The mounted() hook is called before the axios.get asynchronous call have been completed.
The sequence of calls is probably this 1.created(), 2.mounted(), 3.axios.then()
So, you will need to slightly change your logic so the map is initialized when both the creation has completed and the mount hook has been invoked.
Something like this,
add a flag for the mounted call been executed
data() {
return {
map: null,
mounted: false,
latlng: null
}
move the map creation code into a method
createMap: function() {
this.map = L.map("mapContainer", { center: this.latlng ...
at creation go fetch the data, and if already mounted, create the map
created(){
axios.all([axios.get('http://127.0.0.1:5000/api/get_longitude');
axios.get('http://127.0.0.1:5000/api/get_latitude')])
.then((longresp,latresp)=> {
this.latlng=[latresp.data,longresp.data];
if (this.mounted) this.createMap()
})
and then at mounted, check if by chance the data is already available, and if not, set the flag for the creation of the map
mounted() {
if (this.latlng) this.createMap()
this.mounted = true;
...
Create the Map when the data is available ( when axios promise is successful ).
More on promises: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
Use a indicator until content not ready.
I think there is no need for separate requests, so i merged the 2 axios requests. Your backend should return the [lat, long] array.
<template>
<div>
<!--
I also added a simple loading indicator,
obviously you can go for something more fancy like a spinner
--!>
<p v-if="loading">Loading...</p>
<div id="mapContainer">{{ latlng }}</div>
</div>
</template>
<script>
import "leaflet/dist/leaflet.css";
import L from "leaflet";
import axios from 'axios';
export default {
name: "Map",
data() {
return {
map: null,
loading: false,
latlng: []
};
},
methods: {
setupLeafletMap: function () {
this.map = L.map("mapContainer", {
center: this.latlng,
zoom: 12,
});
L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
attribution:
'© OpenStreetMap contributors'
}).addTo(this.map);
}
},
mounted() {
this.loading = true;
axios.get('http://127.0.0.1:5000/api/get_latlng').then(res => {
this.latlng = res.data;
this.setupLeafletMap(); // Creating the Map here ensures that data is already loaded from server side
this.loading = false;
}).catch(error => console.error(error));
},
beforeDestroy() {
if (this.map) {
this.map.remove();
}
}
};
</script>
<style scoped>
#mapContainer {
width: 50vw;
height: 50vh;
}
</style>
I'm currently using a Leaflet map (with vue2leaflet).
What I do is pretty much standard:
A list of places is imported from a REST Api in the app store (vuex)
Then on the map initialization, the markers are generated using these informations in the store
So basically my Map.vue calls the map:
<v-map ref="map" :zoom="zoom" :center="center">
<v-tilelayer url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"></v-tilelayer>
<v-marker-cluster :options="clusterOptions">
<v-marker v-for="(marker, index) in markers"
:key="index"
:lat-lng="makeCoords(marker.location.lat, marker.location.lng)"
v-on:l-click="showSpot(marker._id, marker.slug, marker.location.lat, marker.location.lng)">
</v-marker>
</v-marker-cluster>
</v-map>
Markers is coming from the store ($store.map.markers):
computed: {
markers () {
return this.$store.state.map.markers
}
}
So in the same Template, if I want to get a reference to the map, I just need to do this:
this.$refs.map
But I would need to do the same from another file (let's say "AddMarker.vue", in order to place new markers on the map, using this method:
L.marker([datas.location.lat, datas.location.lng]).addTo(mymap);
where "mymap" should be the object defined in Map.vue
Of course, as the map is not in the same file, this.$refs.map results in "undefined".
I tried to add the map reference in the store, but it's not working and fires an error (call stack), I guess it's not made to store components.
I tried to just commit the new marker in the store, but the map won't just magically adapt and add it. I guess I really need to call the addTo() method for this.
Here's the store:
export const state = () => ({
markers: null
})
export const mutations = {
setMarkers(state, markers) {
state.markers = markers
},
addMarker(state, marker) {
state.markers.push(marker)
}
}
export const actions = {
async init({ commit }) {
let { data } = await this.$axios.get(process.env.api.spots)
commit('setMarkers', data)
}
}
And here's how I call the mutation:
that.$store.commit('map/addMarker', {
title: values.title,
description: values.description,
location: {
city: that.$store.state.position.infos.city,
country: that.$store.state.position.infos.country,
lat: that.$store.state.position.coords.lat,
lng: that.$store.state.position.coords.lng
}
});
The marker is perfectly added in the store, yet nothing happen on the map.
If anyone know how to deal with this?
Thanks!
Your actual problem is: "how do I add another marker to markers?" If you define markers as a computed that is based on the store, then it's a matter of adding a marker to the store.
Vue.component('v-map', Vue2Leaflet.Map);
Vue.component('v-tilelayer', Vue2Leaflet.TileLayer);
Vue.component('v-marker', Vue2Leaflet.Marker);
const store = new Vuex.Store({
state: {
markers: [
[47.42, -1.25],
[47.41, -1.21],
[47.43, -1.22]
].map(p => L.latLng(...p))
},
mutations: {
addMarker(state, payload) {
state.markers.push(payload);
}
},
actions: {
addMarker({
commit
}, payload) {
commit('addMarker', payload)
}
}
})
const v = new Vue({
el: '#app',
store,
data() {
return {
zoom: 13,
center: [47.413220, -1.219482],
url: 'http://{s}.tile.osm.org/{z}/{x}/{y}.png',
attribution: '© OpenStreetMap contributors',
}
},
computed: {
markers() {
return this.$store.state.markers;
}
}
});
setTimeout(() => {
store.dispatch('addMarker', L.latLng(47.412, -1.24));
}, 1400);
html,
body,
#app {
height: 100%;
margin: 0;
}
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<script src="//unpkg.com/leaflet#1.0.3/dist/leaflet.js"></script>
<script src="//unpkg.com/vue2-leaflet#0.0.57/dist/vue2-leaflet.js"></script>
<script src="//unpkg.com/vuex#latest/dist/vuex.js"></script>
<link href="//unpkg.com/leaflet#1.0.3/dist/leaflet.css" rel="stylesheet" />
<div id="app">
<v-map :zoom="zoom" :center="center">
<v-tilelayer :url="url" :attribution="attribution"></v-tilelayer>
<v-marker v-for="marker in markers" :lat-lng="marker"></v-marker>
</v-map>
</div>
Consider an event bus for this situation; you've got components that can add markers on a map, say a list of addresses and when you click one a pin drops to it's location.
// bus.js
import Vue from 'vue';
export const EventBus = new Vue();
// address-list.js
import { EventBus } from './bus.js';
methods: {
onClick () {
EventBus.$emit('add-marker', {x:123,y:345});
}
}
// map.js
import { EventBus } from './bus.js';
EventBus.$on('add-marker', coords => {
this.addMarker(coords).then(() => this.redrawMap())
});
Straightforward, not a lot of code. Being a global bus, obviously you can re-use in any component necessary.