I am working on an arcgis map, I'm trying to update the map center by calling goTo() on my mapview but for some reason the map just changes to be blank and never updates, I am logging the new coordinates and they are correct.
I am using the reference docs here: https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html
Can someone with some arcgis experience help me out. I know this isn't an issue with my code specifically but it might be an issue with vue and component rendering as it relates to arcgis
so far I have tried
- getting rid of props and updating everything within the component locally
- using keys to force re-render the component
as an interesting note, if I just enter in some magic numbers for my new location the map updates correctly, however when i use some function to get the location and then pass it in, it does not work and just shows as a blank map
my app.vue
<template>
<div id="app">
<web-map v-bind:centerX="lat" v-bind:centerY="long" ref="map"/>
<div class="center">
<b-button class="btn-block" #click="updateCenter()" 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: null,
long: null,
}
},
methods:{
updateCenter(){
this.$refs.map.getLocation()
}
},
};
</script>
my map component
<template>
<div></div>
</template>
<script>
import { loadModules } from 'esri-loader';
export default {
name: 'web-map',
data: function(){
return{
X: -118,
Y: 34,
}
},
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: [-118,34], ///USE PROPS HERE FOR NEW CENTER
zoom: 8
});
});
},
beforeDestroy() {
if (this.view) {
// destroy the map view
this.view.container = null;
}
},
methods:{
showPos(pos){
console.log('new location',pos.coords.latitude,pos.coords.longitude)
this.view.goTo({center:[pos.coords.latitude,pos.coords.longitude]})
},
getLocation(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.showPos);
} else {
console.log("Geolocation is not supported by this browser.");
}
},
}
};
</script>
Switch:
this.view = new MapView({
container: this.$el,
map: map,
center: [-118,34], ///USE PROPS HERE FOR NEW CENTER
zoom: 8
});
to
this.view = new MapView({
container: this.$el,
map: map });
this.view.center.longitude = -118;
this.view.center.latitude = 34;
this.view.zoom = 8;
The other answer by Tao has the long/latitude backwards in the .goTo({center: []}) method call, which is why it goes to the ocean: https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html#goTo
Here's something that works:
https://codesandbox.io/s/frosty-glitter-39wpe?file=/src/App.vue
I made it from scratch, only taking small bits from yours and combining them with some examples from ArcGIS (which I'm not familiar with, at all).
One thing to note is that the .goTo({center: [lat, long]}) didn't work as expected: it kept centering in the middle of some ocean.
I then imported Point from esri and passed the center as new Point(long, lat), which seems to produce the expected result. Since it works, I haven't looked further, but I guess it should be doable without the conversion. You probably need to pass in the coordinates system or something along these lines.
As far as I can tell, what's wrong in your example is the way you try to pass data down from parent to child. You expect this.$refs.map to be a Vue instance, but it's not. It's a DOM element. It's basically the Vue instance's $el. Accessing child methods from parent instance is not so straight forward.
Another thing to notice is that, even though you bind centerX and centerY on child in your example, you never seem to use them (but I guess that's just a left over from when you tried with props !?).
Anyways, in my example, I chose to simply update the coords prop of the children while having a watch fn to handle re-centering.
Related
I am trying to figure out how I can access the ArCGIS JS API from a map after the map has been rendered, outside of require (ArcGIS JS API uses Dojo). For example, so I can do stuff like add (or remove) points, and perform other operations on the map.
I can create a map as follows:
require(["esri/config", "esri/Map", "esri/views/MapView", "esri/Graphic",
"esri/layers/GraphicsLayer"
], function(esriConfig, Map, MapView, Graphic, GraphicsLayer) {
esriConfig.apiKey = "";
const map = new Map({
basemap: "arcgis-topographic"
});
const view = new MapView({
map: map,
center: [-81, 41],
zoom: 9,
container: "viewDiv"
});
});
And I can add a point using this function:
function plotPoint(lat, long, props) {
const popupTemplate = {
title: "{Name}",
content: "{Description}"
}
const attributes = {
Name: props.name,
Description: props.desc
}
const graphicsLayer = new GraphicsLayer();
map.add(graphicsLayer);
const point = {
type: "point",
longitude: long,
latitude: lat
};
const simpleMarkerSymbol = {
type: "simple-marker",
color: [226, 119, 40],
outline: {
color: [255, 255, 255],
width: 1
}
};
const pointGraphic = new Graphic({
geometry: point,
symbol: simpleMarkerSymbol,
attributes: attributes,
popupTemplate: popupTemplate
});
graphicsLayer.add(pointGraphic);
}
But plotPoint needs to be within the require callback so it can access the referenced modules (like GraphicsLayer). I could assign it to the global window object so I could call it outside of require, but then I may run into an issue where the function is called before it's defined.
I may need to perform other operations too from other points in the code, like removing points, adding feature layers, etc. Unfortunately, this must all exist inside some legacy code, so I can't refactor the entire application.
Is there a better pattern for accessing the API outside of require?
I think that the easy way to achieve what you want, if I understand you correctly, is just to define modules and include it in you application.
A simple example base on you code would be something like this,
GraphicsManager.js
define([
"dojo/_base/declare",
"esri/Graphic",
"esri/layers/GraphicsLayer"
], function(declare, Graphic, GraphicsLayer){
return declare(null, {
plotPoint: function(lat, long, props){
// .. here the logic
return graphicsLayer;
}
});
});
main.js
require(["esri/config", "esri/Map", "esri/views/MapView", "app/GraphicsManager"
], function(esriConfig, Map, MapView, GraphicsManager) {
esriConfig.apiKey = "";
const map = new Map({
basemap: "arcgis-topographic",
});
const view = new MapView({
map: map,
center: [-81, 41],
zoom: 9,
container: "viewDiv"
});
// ... some logic to get the point data
const gm = new GraphicsManager();
map.add(gm.plotPoint(lat, long, props));
// .. some other logic
});
There you see that the main.js is where the application start, things are set there or in others modules. You know, map, layers, widgets, etc.
Then you have your other code in modules, and you use import them as required.
dojotoolkit - intro to modules
I am using VueMapbox (0.4.1) to utilize Mapbox GL in a Vue project.
<template>
<MglMap
:accessToken="accessToken"
:mapStyle.sync="mapStyle"
:repaint="true"
#load="onMapLoaded">
<MglMarker
:coordinates="someCoordinates"
class="map-marker-wrapper">
<div
slot="marker"
class="map-marker">
</div>
</MglMarker>
</MglMap>
</template>
<script>
import Mapbox from 'mapbox-gl'
import { MglMap, MglMarker } from 'vue-mapbox'
import * as MAP from '#/constants/map'
// Vue-Mapbox documentation: https://soal.github.io/vue-mapbox/guide/basemap.html#adding-map-component
export default {
name: 'Map',
components: {
MglMap,
MglMarker
},
props: {
someCoordinates: {
type: Array,
required: true
},
darkMode: {
type: Boolean,
required: true
}
},
data () {
return {
accessToken: MAP.ACCESS_TOKEN,
mapbox: null,
map: null,
actionsDispatcher: null
}
},
computed: {
mapStyle () {
return this.darkMode ? MAP.COLOR_PROFILES.DARK : MAP.COLOR_PROFILES.LIGHT
}
},
created () {
this.mapbox = Mapbox
},
methods: {
async onMapLoaded (event) {
this.map = event.map
this.actionsDispatcher = event.component.actions
await this.actionsDispatcher.flyTo({
center: this.someCoordinates
})
}
}
}
</script>
On the first load, everything works as expected:
But if I move to a different pseudo-route (say from /#/Map to /#/Profile and back), some of the map layers specified by my mapStyle (roads, city names,..) are not rendered anymore (default layers instead). The map also stops honoring any change of the mapStyle url, even when I specify mapStyle.sync in my template.
If I hit the browser's reload button it loads the layers as expected as the entire app is reloaded from scratch, but I unfortunately cannot afford to do this by default.
Any ideas are greatly appreciated.
I found a solution, in the example of vue-mapbox, the variable map (this.map) is set by "event.map" which causes an error because the card is not refreshed afterwards.
In my case i just remove that (this.map = event.map) in my onMapLoaded function and this is great.
Have a good day.
Despite I don’t know the syntax of Vue.js, the problem you are facing is that you are creating your layers in map.on('load', ... which is an event that happens only once, so when the style change happens, all the layers of the map style (including the ones created by custom code) are removed.
If you want to recreate your layers on style change, you have to do it in the event map.on('style.load', ..., but as said, I don’t see in your vue.js code where that is being done. If you share the part of the code where vue.js is invoking the methods it’ll be easier to help you
I have a geodjango queryset containing several fields but want to use only user_name and location (a point field) which I want to use as a marker in google maps API 3.
Bear with me as I don’t know Javascript and I have a series of questions.
Take this as conceptual brainstorming for a novice:
My SO search suggests that I need to serialize the queryset objects
to JSON. I use the built-in serializer module to convert to JSON.
I think the JSON objects are converted in views.py (let’s
call it json_data). Are these JSON objects stored in the PostGIS database? Wouldn’t that be redundant?
Furthermore, how do I reference them in the map.js (google maps
API 3) javascript file? I want to (import?link?) JSON objects to display them as location markers.
I want to know how to declare and iterate the javascript variable
locations.
For var(i=0;i< locations.length;i++){[
[json_data.user_name, json_data.point],
]
var map = new google.maps.Map(document.getElementById('map'), {
center: new google.maps.LatLng(49.279504, -123.1162),
zoom: 14,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var infowindow = new google.maps.InfoWindow();
var marker, i;
for (i = 0; i < locations.length; i++) {
marker = new google.maps.Marker({
position: new google.maps.LatLng(locations[i][1], locations[i][2]),
map: map,
icon: 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png'
});
google.maps.event.addListener(marker, 'click', (function(marker, i) {
return function() {
infowindow.setContent(locations[i][0]);
infowindow.open(map, marker);
}
})(marker, i));
}
Guide me if I went unnecessarily convoluted way to do a simple task.
TL;DR
No, what you are doing is not redundant and nothing get's written to the database from those answers.
You need to make a getJSON() or similar call to your API's endpoint to access the data.
You can do it on the 2nd step's call and declare it as a list.
What you are thinking is pretty much correct but there is room for improvement (thus the long answer below).
Answer:
Some time ago I read a very good initiation tutorial on building a GIS application with geodjango and google maps. Read it and it should give you a great jump start.
After you read that we will follow a somewhat different way which leaves more room to play around with your front-end (use react for example or whatever comes to mind).
The back-end:
Create a view to retrieve the information you want (user_name, location) as JSON, using the values() queryset method which returns a list of dictionaries.
Since we have to JSONify a list, we will use JsonResponse and we will mark it as unsafe:
from django.http import JsonResponse
def my_view(request):
resp = MyModel.objects.all().values('user_name', 'location')
return JsonResponse(list(resp), safe=False)
Add an endpoint to access that view on urls.py:
urlpatterns = [
...
url(r'^my_endpoint/$', my_view, name='my_endpoint'),
...
]
Now whenever we access the my_endpoint/ we will get a JSON representation of every object's user_name and location in our database which will look like this:
[
{user_name: a_user, location: [lat, lng]},
{user_name: another_user, location: [lat, lng]},
...
]
Moving to the front-end now:
Make a getJSON() or an ajax() or any other type of call to the API and in the same time create a marker list (close to what #MoshFeu suggests):
let map = new google.maps.Map(document.getElementById('map'), {
center: new google.maps.LatLng(49.279504, -123.1162),
zoom: 14,
mapTypeId: google.maps.MapTypeId.ROADMAP
});
let markers = [];
$.getJSON( "my_base_url/my_endpoint", function(data) {
$.each(data, function() {
markers.push(
new google.maps.Marker({
position: {
lat: data['location'][0],
lng: data['location'][1]
},
map: map,
icon: 'http://maps.google.com/mapfiles/ms/icons/blue-dot.png'
})
);
});
});
...
And we are pretty much done!
You don't need to make any special serialization to your data.
You can query the data from any type of front-end you can imagine which gives you designing freedom.
My use-case. I used django.contrib.gis (django.contrib.gis.db.models.PolygonField) and needed to replace default map with Google maps + change default map coordinates, zoom, etc.
TL;DR
I created a new app called gis_addons with custom template and widget to use.
I instructed my model admin (using formfield_overrides) to use my own map widget.
Make sure to add the gis_addons to INSTALLED_APPS.
File: gis_addons/templates/gis_addons/openlayers_googlemaps.html
{% extends "gis/openlayers.html" %}
{% load i18n l10n %}
{% block base_layer %}
var base_layer = new ol.layer.Tile({
source: new ol.source.XYZ({
attributions: [new ol.Attribution({ html: '' })],
maxZoom: 25,
url: "http://mt0.google.com/vt/lyrs=r&hl=en&x={x}&y={y}&z={z}&s=Ga"
})
});
{% endblock %}
{% block options %}var options = {
base_layer: base_layer,
geom_name: '{{ geom_type }}',
id: '{{ id }}',
map_id: '{{ id }}_map',
map_options: map_options,
map_srid: {{ map_srid|unlocalize }},
name: '{{ name }}',
default_lat: 53.2193835,
default_lon: 6.5665018,
default_zoom: 15
};
{% endblock %}
File: gis_addons/widgets.py
from django.contrib.gis.forms.widgets import OpenLayersWidget
class GoogleMapsOpenLayersWidget(OpenLayersWidget):
"""Google Maps OpenLayer widget."""
template_name = 'gis_addons/openlayers_googlemaps.html'
File: my_app/models.py
from django.db import models
from django.contrib.gis.db import models as gis_models
from django.utils.translation import ugettext_lazy as _
class MyModel(models.Model):
# ...
coordinates = gis_models.PolygonField(
verbose_name=_("Geo coordinates"),
)
def __str__(self):
return self.name
File: my_app/admin.py
from django.contrib import admin
from django.contrib.gis.db.models import PolygonField
from gis_addons.widgets import GoogleMapsOpenLayersWidget
from my_app.models import MyModel
#admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
# ...
formfield_overrides = {
PolygonField: {"widget": GoogleMapsOpenLayersWidget}
}
I'm using Vue and Leaflet for displaying polygons (zones) on a map and display appropriate information (messages) about the specific polygons after clicking on them on the map. The div, where I render the messages in, has the id "#messagearea" and is bound to the "el" object. To display the appropriate messages, I am dependant on the "Zone-id".
Now I also want to display information into another div with a different id. I am also dependant on the "Zone-id" here, so I would like to do this in the same Vue. If I would create another Vue, I would have to render the Leaflet map again to write another polygon.on('click',...) function, which displays appropriate information for the polygons. What is the most elegant and/or easiest way to realize this?
Here my vue object:
var mapVue = new Vue({
el: '#messagearea',
data: {
function() {
return {
map: false
};
},
zones: [],
messages: [],
},
ready: function () {
this.map = L.map('map').setView([51.959, 7.623], 14);
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
this.$http.get('/api/zones', function (data) {
this.$set('zones', data);
for (var i = 0; i < this.zones['Zones'].length; i++) {
polygon = L.polygon(
this.zones['Zones'][i]['Geometry']['Coordinates']).addTo(this.map);
polygon.bindPopup(this.zones['Zones'][i]['Name']);
polygon.on('click', messageCallback(i))
// HERE I WOULD LIKE TO ADD THE FUNCTION FOR THE OTHER DIV
}
function messageCallback(i) {
return function () {
mapVue.getMessages(mapVue.zones['Zones'][i]['Zone-id']);
}
}
});
},
methods:
{
getMessages: function (id) {
this.$http.get('/api/messages?zone=' + id, function (data) {
console.log("messages called");
this.$set('messages', data['Messages']);
});
}
}
})
I solved this issue by making use of Vue.component(), the vue.$dispatch() and vue.$broadcast() functions. I just dispatched the zone id to a parent component and then delivered it with the broadcast function to all child components, which are in need of the zone id. Displaying the appropriate messages was no problem then anymore.
I'm using ember-leaflet to display Leaflet maps in my Ember application. In my case, the map is used to display an appointment location. The user selects an appointment from a list and the corresponding appointment details (including the map) are then displayed.
When an appointment is selected, the details are passed to a component, which contains the map. Unfortunately, after the user selects an appointment and then another is selected, the coordinates on the map do not change. However, outputting the coordinates using Handlebars, I can in fact see that the different coordinates are being passed. That leads me to believe that the map needs to be "refreshed" somehow in order for the new coordinates to be displayed on the map.
I use the component like so: {{ember-leaflet geoJSON=geoJSON}} where geoJSON is a string containing the location data.
My component is as follows:
// components/leaflet-map.js
import Ember from 'ember';
import ENV from '../config/environment';
import EmberLeafletComponent from 'ember-leaflet/components/leaflet-map';
import MarkerCollectionLayer from 'ember-leaflet/layers/marker-collection';
import TileLayer from 'ember-leaflet/layers/tile';
L.Icon.Default.imagePath = '/images/leaflet';
export default EmberLeafletComponent.extend({
center: Ember.computed(function() {
return this.get('coordinates');
}),
/////////////////////////////////////
// PROPERTIES
/////////////////////////////////////
geoJSON: null,
/////////////////////////////////////
// COMPUTED PROPERTIES
/////////////////////////////////////
childLayers: Ember.computed('coordinates', function() {
return [
TileLayer.extend({
tileUrl: 'https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token={accessToken}',
options: {
id: 'XXXXXXX',
accessToken: ENV.APP.MAPBOX_KEY
}
}),
MarkerCollectionLayer.extend({
content: [{ location: this.get('coordinates') }]
})
];
}),
coordinates: Ember.computed('geoJSON', function() {
if (this.get('geoJSON')) {
const coordinates = JSON.parse(this.get('geoJSON')).coordinates;
if (coordinates) {
return L.latLng(coordinates[1], coordinates[0]);
}
}
return null;
}),
});
When setting a breakpoint, it appears that childLayers and coordinates are only being called once regardless of which appointment is selected by the user. I've considered setting up an observer to observe the geoJSON property, but it seems overkill.
Any help would be greatly appreciated!