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
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 need to create a vuejs component and pass it into a library (mapbox) as pure html. Mapbox has a setHtml method for popups that I'm trying to populate.
https://docs.mapbox.com/mapbox-gl-js/example/popup/
var popup = new mapboxgl.Popup({ closeOnClick: false })
.setLngLat([-96, 37.8])
.setHTML('<h1>Hello World!</h1>')
.addTo(map);
I haven't been able to find any way to pre-render a specific component into html that I could then insert into the mapbox call. Sort of like v-html in reverse.
set ref attribute for your component, and then you can get rendered HTML content of component by using this.$refs.ComponentRef.$el.outerHTML, and remember don't do this when created.
<template>
<div class="app">
<Hello ref="hello" />
</div>
</template>
<script>
import Hello from './Hello.vue'
export default {
name: 'App',
components: {
Hello,
},
created() {
// wrong, $el is not exists then
// console.log(this.$refs.hello.$el.outerHTML)
},
mounted() {
console.log(this.$refs.hello.$el.outerHTML)
},
}
</script>
I'm trying to add a custom control on my map using the OpenLayers with Vue.js.
I have the component Explore.vue that creates my "map" (olmap) using the OL and I pass it through binding to the child component LeftSideBar2.vue.
When I try to add a new control in my map, the Vue shows the following error:
[Vue warn]: Error in mounted hook: "TypeError: this.olmap.addControl is not a function"
Does someone know what is happening?
My files are:
Explore.vue:
Template:
<explore-left-side-bar2 v-bind:olmap="olmap"/>
Script:
export default {
name: 'Explore',
data () {
return {
olmap: {}
}
},
methods: {
initComponent: function () {
// eslint-disable-next-line
this.olmap = new Map({
target: 'map',
layers: [
baseLayerGroup
],
view: new View({
projection: 'EPSG:4326',
center: [0, 0],
zoom: 5
})
})
}
},
mounted: function () {
this.initComponent()
},
components: {
ExploreLeftSideBar2
}
}
LeftSidebar2.vue:
Script:
export default {
name: 'LeftSideBar2',
props: ['olmap'],
data () {
return {
}
},
methods: {
initComponent: function () {
var sidebar = new Sidebar({ element: 'ol-sb-sidebar', position: 'left' })
this.olmap.addControl(sidebar)
}
},
mounted: function () {
this.initComponent()
},
components: {
LeftSideBarLayerTree
}
}
It looks like you have bound an Object olmap={}to a component which in returns calls a function this.olmap.addControls() that doesn't exist on the object which you pass as a prop. I think you are trying to do instead, is call addControls() on the OpenLayers.Map instance.
As this answer explains, mounted hooks are called for child components before their parents, meaning that LeftSidebar2.vue will call this.olmap.addControl(sidebar) when olmap is still the default empty object declared in the data method of Explore.vue.
There are a cuople of ways you could work around this:
You could initialise olmap in the created hook of Explore.vue rather than the mounted hook.
or
You could use a v-if to exclude LeftSidebar2.vue until after olmap has been initialised.
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.