Skip to content

Commit

Permalink
Add and use MapLibre in React if running Terrastories in offline mode (
Browse files Browse the repository at this point in the history
…#943)

* Add and apply maplibre-gl package and css if useLocalMapServer is true

* Use dynamic importing for Maplibre/Mapbox in Map component

* Revert unintended db schema change (not sure how that happened?)

* Use querySelectorAll to add the right map controls

* Ensure modules are only imported once by checking state

* Rebase with master
  • Loading branch information
rudokemper committed Oct 20, 2023
1 parent d2d5b56 commit 5ee441a
Show file tree
Hide file tree
Showing 8 changed files with 248 additions and 58 deletions.
2 changes: 1 addition & 1 deletion rails/app/assets/stylesheets/components/_balloon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
width: 100%;
}

.mapboxgl-popup-content {
.mapboxgl-popup-content, .maplibregl-popup-content {
h1, h2, h3, h4, h5, p, img {
margin: 0;
}
Expand Down
2 changes: 1 addition & 1 deletion rails/app/assets/stylesheets/components/home.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.mapboxgl-map.ts-MainMap {
.mapboxgl-map.ts-MainMap, .maplibregl-map.ts-MainMap {
position: fixed;
height: 100%;
width: 100%;
Expand Down
1 change: 1 addition & 0 deletions rails/app/assets/stylesheets/dashboard.sass.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import 'mapbox-gl/dist/mapbox-gl';
@import 'maplibre-gl/dist/maplibre-gl';

@import 'core/reset';
@import 'core/colors';
Expand Down
1 change: 1 addition & 0 deletions rails/app/assets/stylesheets/react.sass.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import 'mapbox-gl/dist/mapbox-gl';
@import 'maplibre-gl/dist/maplibre-gl';

@import 'core/core';
@import 'components/components';
2 changes: 1 addition & 1 deletion rails/app/javascript/components/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ class App extends Component {
points={this.state.points}
mapboxAccessToken={this.props.mapbox_access_token}
useLocalMapServer={this.props.use_local_map_server}
mapboxStyle={this.props.mapbox_style}
mapStyle={this.props.mapbox_style}
mapbox3d={this.props.mapbox_3d}
mapProjection={this.props.map_projection}
clearFilteredStories={this.resetStoriesAndMap}
Expand Down
149 changes: 98 additions & 51 deletions rails/app/javascript/components/Map.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import ReactDOM from "react-dom";
import React, { Component } from "react";
import PropTypes from "prop-types";
import Minimap from "../vendor/mapboxgl-control-minimap.js";
import Popup from "./Popup";

import mapboxgl from '!mapbox-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import 'maplibre-gl/dist/maplibre-gl.css';

import center from '@turf/center'
import bboxPolygon from '@turf/bbox-polygon'
Expand All @@ -16,9 +15,12 @@ const STORY_POINTS_DATA_SOURCE = "ts-points-data";
export default class Map extends Component {
constructor(props) {
super(props);
mapboxgl.accessToken = this.props.mapboxAccessToken;
this.state = {
activePopup: null
activePopup: null,
mapGL: null,
isMapLibre: false,
mapModule: null,
minimapModule: null
};
}

Expand All @@ -27,7 +29,7 @@ export default class Map extends Component {
points: PropTypes.object,
framedView: PropTypes.object,
onMapPointClick: PropTypes.func,
mapboxStyle: PropTypes.string,
mapStyle: PropTypes.string,
mapboxAccessToken: PropTypes.string,
mapbox3d: PropTypes.bool,
mapProjection: PropTypes.string,
Expand All @@ -37,15 +39,82 @@ export default class Map extends Component {
};

componentDidMount() {
this.map = new mapboxgl.Map({
container: this.mapContainer,
style: this.props.mapboxStyle,
center: [this.props.centerLong, this.props.centerLat],
zoom: this.props.zoom,
maxBounds: this.checkBounds(), // check for bounding box presence
pitch: this.props.pitch,
bearing: this.props.bearing,
projection: this.props.mapProjection
if (this.props.useLocalMapServer) {
if (!this.state.mapModule) {
import('!maplibre-gl').then(module => {
this.setState({ mapModule: module.default }, () => {
this.initializeMap(this.state.mapModule, true);
});
});
} else {
this.initializeMap(this.state.mapModule, true);
}
} else {
if (!this.state.mapModule || !this.state.minimapModule) {
Promise.all([
import('!mapbox-gl'),
import('../vendor/mapboxgl-control-minimap.js')
]).then(([mapboxGLModule, minimapModule]) => {
this.setState({
mapModule: mapboxGLModule.default,
minimapModule: minimapModule.default
}, () => {
this.Minimap = this.state.minimapModule;
this.initializeMap(this.state.mapModule, false);
});
});
} else {
this.Minimap = this.state.minimapModule;
this.initializeMap(this.state.mapModule, false);
}
}
}

componentDidUpdate(prevProps, prevState) {
if (!this.map) {
return;
}

if (prevProps.points !== this.props.points) {
this.updateMapPoints();
}

if (prevProps.activePoint && !this.props.activePoint) {
this.closeActivePopup();
}

// Open active popup
if (this.props.activePoint && prevProps.activePoint !== this.props.activePoint) {
this.openPopup(this.props.activePoint);
}

// Set map framed view
if (
this.props.framedView &&
this.props.framedView !== prevProps.framedView
) {
const { bounds, ...frameOptions } = this.props.framedView;
if (bounds) {
const bboxPoly = bboxPolygon(bounds);
const centerPoint = center(bboxPoly).geometry.coordinates;
this.map.fitBounds(bounds, { center: centerPoint, padding: 50, duration: 2000.0, maxZoom: 12, ...frameOptions });
} else {
this.map.easeTo({ duration: 2000.0, ...frameOptions });
}
}
}

initializeMap(mapGL, isMapLibre) {
mapGL.accessToken = this.props.useLocalMapServer ? 'pk.ey' : this.props.mapboxAccessToken;
this.map = new mapGL.Map({
container: this.mapContainer,
style: this.props.mapStyle,
center: [this.props.centerLong, this.props.centerLat],
zoom: this.props.zoom,
maxBounds: this.checkBounds(), // check for bounding box presence
pitch: this.props.pitch,
bearing: this.props.bearing,
projection: this.props.mapProjection
});

this.map.on("load", () => {
Expand Down Expand Up @@ -112,10 +181,10 @@ export default class Map extends Component {
});

// Hide minimap and nav controls for offline Terrastories
if(!this.props.useLocalMapServer) {
this.map.addControl(new Minimap(
if(!isMapLibre && this.Minimap) {
this.map.addControl(new this.Minimap(
{
style: "mapbox://styles/mapbox/light-v10",
style: this.props.mapStyle,
zoomLevels: [
[18, 14, 16],
[16, 12, 14],
Expand All @@ -132,7 +201,12 @@ export default class Map extends Component {
}), "top-right");
}

this.map.addControl(new mapboxgl.NavigationControl());
this.map.addControl(new mapGL.NavigationControl());

// Add Maplibre logo for offline Terrastories
if(isMapLibre) {
this.map.addControl(new mapGL.LogoControl(), 'bottom-right');
}

// Change mouse pointer when hovering over ts-marker points
this.map.on('mouseenter', STORY_POINTS_LAYER_ID, () => {
Expand All @@ -149,36 +223,11 @@ export default class Map extends Component {
this.map.on('mouseleave', 'clusters', () => {
this.map.getCanvas().style.cursor = ''
})
}

componentDidUpdate(prevProps, prevState) {
if (prevProps.points !== this.props.points) {
this.updateMapPoints();
}

if (prevProps.activePoint && !this.props.activePoint) {
this.closeActivePopup();
}

// Open active popup
if (this.props.activePoint && prevProps.activePoint !== this.props.activePoint) {
this.openPopup(this.props.activePoint);
}

// Set map framed view
if (
this.props.framedView &&
this.props.framedView !== prevProps.framedView
) {
const { bounds, ...frameOptions } = this.props.framedView;
if (bounds) {
const bboxPoly = bboxPolygon(bounds);
const centerPoint = center(bboxPoly).geometry.coordinates;
this.map.fitBounds(bounds, { center: centerPoint, padding: 50, duration: 2000.0, maxZoom: 12, ...frameOptions });
} else {
this.map.easeTo({ duration: 2000.0, ...frameOptions });
}
}
this.setState({
mapGL: mapGL,
isMapLibre: isMapLibre
});
}

addMapPoints() {
Expand Down Expand Up @@ -296,7 +345,7 @@ export default class Map extends Component {
this.closeActivePopup();
}} />, popupNode);
// set popup on map
const popup = new mapboxgl.Popup({
const popup = new this.state.mapGL.Popup({
offset: 15,
className: "ts-markerPopup",
closeButton: false, // We add our own custom close button
Expand Down Expand Up @@ -332,9 +381,7 @@ export default class Map extends Component {

addHomeButton() {
const homeButton = this.createHomeButton();
const navControl = document.getElementsByClassName(
"mapboxgl-ctrl-zoom-in"
)[0];
let navControl = document.querySelectorAll('button[class$="-ctrl-zoom-in"]')[0];
if (navControl) {
navControl.parentNode.insertBefore(homeButton, navControl);
}
Expand Down
1 change: 1 addition & 0 deletions rails/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"i18next-http-backend": "^2.1.1",
"jest": "24.0.0",
"mapbox-gl": "^2.12.0",
"maplibre-gl": "^3.3.1",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down

0 comments on commit 5ee441a

Please sign in to comment.