Overlays in OpenLayers with React.js - javascript

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)

Related

FlyTo on existing Mapbox Object in Svelte

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(...)

altering arcgis map location onClick in vue

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

OpenLayers can't initalize map using Component class and webpack

I'm trying to rebuild this tutorial. Instead of using Leaflet (which is working, but for different reasons, I don't want to use Leaflet), I want to rebuild it using Openlayers, but I can't initialize the map using OL.
I get this error message in Chrome browser, saying my Map Object is null:
Uncaught TypeError: Cannot set property 'innerHTML' of null
at new Component (component.js:17)
at new Map (map.js:28)
at new Map (map.js:33)
at ViewController.initializeComponents (main.js:26)
at new ViewController (main.js:18)
at eval (main.js:30)
at Object.<anonymous> (bundle.js:1580)
at __webpack_require__ (bundle.js:20)
at bundle.js:64
at bundle.js:67
The ol package is loaded in webpack, as I can see in Chromiums DevTools Sources.
I'm using a class for the Map Component, which extends the class "Component" and initializes the Map in the same way the original code with leaflet does:
export class Component {
/*Base component class to provide view ref binding, template insertion, and event listener setup
*/
/** SearchPanel Component Constructor
* #param { String } placeholderId - Element ID to inflate the component into
* #param { Object } props - Component properties
* #param { Object } props.events - Component event listeners
* #param { Object } props.data - Component data properties
* #param { String } template - HTML template to inflate into placeholder id
*/
constructor (placeholderId, props = {}, template) {
this.componentElem = document.getElementById(placeholderId)
if (template) {
// Load template into placeholder element
this.componentElem.innerHTML = template
// Find all refs in component
this.refs = {}
const refElems = this.componentElem.querySelectorAll('[ref]')
refElems.forEach((elem) => { this.refs[elem.getAttribute('ref')] = elem })
}
if (props.events) { this.createEvents(props.events) }
}
/** Read "event" component parameters, and attach event listeners for each */
createEvents (events) {
Object.keys(events).forEach((eventName) => {
this.componentElem.addEventListener(eventName, events[eventName], false)
})
}
/** Trigger a component event with the provided "detail" payload */
triggerEvent (eventName, detail) {
const event = new window.CustomEvent(eventName, { detail })
this.componentElem.dispatchEvent(event)
}
}
import Map from 'ol/Map'
import View from 'ol/View'
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import { Component } from '../component'
const template = '<div ref="mapContainer" class="map-container"></div>'
/**
* Openlayers Map Component
* #extends Component
*/
export class Map extends Component {
/** Map Component Constructor
* #param { String } placeholderId Element ID to inflate the map into
* #param { Object } props.events.click Map item click listener
*/
constructor (placeholderId, props) {
super(placeholderId, props, template)
const target = this.refs.mapContainer
// Initialize Openlayers Map
this.map = new Map({
target,
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
center: [0, 0],
zoom: 2
})
});
}
}
Here's the original Leaflet version:
export class Map extends Component {
constructor (placeholderId, props) {
super(placeholderId, props, template)
// Initialize Leaflet map
this.map = L.map(this.refs.mapContainer, {
center: [ 5, 20 ],
zoom: 4,
maxZoom: 8,
minZoom: 4,
maxBounds: [ [ 50, -30 ], [ -45, 100 ] ]
})
this.map.zoomControl.setPosition('bottomright') // Position zoom control
this.layers = {} // Map layer dict (key/value = title/layer)
this.selectedRegion = null // Store currently selected region
// Render Carto GoT tile baselayer
L.tileLayer(
'https://cartocdn-gusc.global.ssl.fastly.net/ramirocartodb/api/v1/map/named/tpl_756aec63_3adb_48b6_9d14_331c6cbc47cf/all/{z}/{x}/{y}.png',
{ crs: L.CRS.EPSG4326 }).addTo(this.map)
}
}
You are using Map ambiguously. Try
import {Map as olMap} from 'ol'
...
export class Map extends Component {
...
// Initialize Openlayers Map
this.map = new olMap({

Having trouble getting google map to appear on react page

I am simply trying to put a google map on a page to start in a react project, and am having trouble. The div with the id="map" shows, but not the map inside it.
I'm following the google map API docs for JS, but obviously I must be doing something wrong. I would like to avoid using react-google-maps since I am used to using straight google maps api in another framework.
Here is my component google_map.js:
import React, { Component } from 'react';
class GoogleMap extends Component {
componentDidMount() {
new google.maps.Map(this.refs.map, {
zoom: 12,
center: {
lat: 37.7952,
lng: -122.4029
}
});
}
render() {
return <div ref="map" />;
}
}
export default GoogleMap;
import React, { Component } from 'react';
import GoogleMap from './google_map';
Here is where I am trying to put the map:
class Feature extends Component {
render() {
return (
<div id="map">
<GoogleMap />
</div>
);
}
}
export default Feature;
Style.css:
#map {
height: 300px;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
I presume you have to add
shouldComponentUpdate {
return false
}
to your GoogleMap component.
Explanation: probably, React.js synchronizes its virtual DOM with real (which is going to be modified by google maps). Further reading: https://reactjs.org/docs/react-component.html#shouldcomponentupdate

Openlayers map undefined in a class

I'm kinda new in an angular (and javascript generally). I have this code
import {Injectable, OnInit} from '#angular/core';
import OlMap from 'ol/map';
import OSM from 'ol/source/osm'
import OlXYZ from 'ol/source/xyz';
import OlTileLayer from 'ol/layer/tile';
import OlView from 'ol/view';
import OlProj from 'ol/proj';
#Injectable()
export class MapService {
public map: OlMap;
private _source: OlXYZ;
private _layer: OlTileLayer;
private _view: OlView;
constructor() { }
/**
* Function initializes the map
* #returns {} Object of map
*/
initMap() {
this._source = new OSM({
});
this._layer = new OlTileLayer({
source: this._source
});
this._view = new OlView({
center: OlProj.fromLonLat([6.661594, 50.433237]),
zoom: 10,
});
this.map = new OlMap({
target: 'map',
layers: [this._layer],
view: this._view
});
this.map.on("moveend", function () {
console.log(this.map);
})
}
}
The problem is on the last line. I'm trying to console log the object of map on moveend (so when user drag the map and release button- I want to load some data depends on the center of a map). But the console says the object this.map is undefined (even I'm calling a method on that object and It's working fine- It's called on mouse button release.
I guess It's gonna be something javascript special, some local or global object references, etc.
Can anyone help me, please?
NOTE: the initMap() method is called in the map component like this
ngOnInit() {
this.map = this.mapService.initMap();
console.log(this.mapService.map);
}
(In this console.log case its working fine, there is object of type _ol_map)
Your problem is the diff between function () {} and () => {}.
In function () {}, the context of "this" is the caller of the function so here is "OlMap".
In () => {}, the context of "this" is where you create it so here is "MapService".
this.map doesn't exist in OlMap.
To fix your issue, simply replace function () by () => into this.map.on().

Categories