OpenLayers can't initalize map using Component class and webpack - javascript

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({

Related

Overlays in OpenLayers with React.js

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)

OpenLayers 6 - ES6 project structure

I'm working on a project using OpenLayers 6 in ES6 with Webpack.
It's my first real ES6 project and I want to make it organized (and a bit modular) but I'm struggling with the use of imports and exports.
Currently my structure is :
- all.js
- map/
- index.js
- gpx.js
The all.js file is the "entry point".
all.js
import 'ol/ol.css';
import map from './map/index';
import { vector as GPXvector } from './map/gpx';
map.addLayer(GPXvector);
map/index.js
import { Map, View } from 'ol';
import { OSM } from 'ol/source';
import { Tile as TileLayer } from 'ol/layer';
const map = new Map({
layers: [
new TileLayer({
source: new OSM()
})
],
target: 'map',
view: new View({
center: [1037749, 5135381],
zoom: 10
})
});
export { map as default };
map/gpx.js
import { Vector as VectorLayer } from 'ol/layer';
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';
import { unByKey } from 'ol/Observable';
import VectorSource from 'ol/source/Vector';
import GPX from 'ol/format/GPX';
import map from './index.js'; // Is that good ??
const style = {
// [...] Some style here
};
const source = new VectorSource({
url: 'test.gpx',
format: new GPX()
});
var onChange = source.on('change', function() {
if (source.getState() == 'ready') {
map.getView().fit(source.getExtent()); // Access to "map" from "index.js" HERE
unByKey(onChange);
}
});
const vector = new VectorLayer({
source: source,
style: function (feature) {
return style[feature.getGeometry().getType()];
}
});
export { vector, source };
I want to access to the map instance (initialized in map/index.js) from the map/gpx.js file (see comment in source code).
But I feel like I am importing map from map/index.js inside all.js, which is importing map/gpx.js which himself also imports map from map/index.js.
It sounds to me like some kind of "loop" imports where it will be a mess to handle the order of imports for example when I'll get more files in my project.
Also if you have any advice for me to start properly with ES6 it's cool !
EDIT 1
I changed to something else to see if it allows more granularity.
all.js
import 'ol/ol.css';
import map from './ntrak/index';
import MyGPX from './ntrak/gpx';
const gpx = new MyGPX(map, 'test.gpx');
map/gpx.js
import { Vector as VectorLayer } from 'ol/layer';
import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style';
import { unByKey } from 'ol/Observable';
import VectorSource from 'ol/source/Vector';
import GPX from 'ol/format/GPX';
const style = {
// [...] Some style here
};
const _onSourceChange = function(map, source) {
if (source.getState() == 'ready') {
map.getView().fit(source.getExtent());
unByKey(_onSourceChange);
}
}
export default class {
constructor(map, url, fit = true) {
this.map = map;
this.url = url;
this.fit = fit;
this.loadGPX();
}
loadGPX() {
this.source = new VectorSource({
url: this.url,
format: new GPX()
});
if (this.fit) {
this.source.on('change', () => _onSourceChange(this.map, this.source));
}
this.vector = new VectorLayer({
source: this.source,
style: function(feature) {
return style[feature.getGeometry().getType()];
}
});
this.map.addLayer(this.vector);
}
};
I think it's cool because it allows to get multiple GPX vectors on the same map instance.
But if I want to do more stuff that interacts with my GPX source or vector I will need to pass the instance everytime instead of just importing the GPX file directly.
What do you think?
You can use CircularDependencyPlugin for webpack to track such circular dependencies.
There is no circular dependency in your example, import map from './index.js'; // Is that good ?? is ok.
Your es6 code is fine to me, I see one var usage (var onChange = ...), you should replace that.

Problem when add a new control on map using OpenLayers and Vue.js

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.

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

How to access a map defined in another component?

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.

Categories