Skip to content

Commit 5abcfb6

Browse files
authored
feature: Update to use Maplibre with Protomaps API (#988)
* Add MapUtils * Load map_utils pack with defer: false * Add new Map service for configuring default map configurations * Use default map configuration * feat: Use Protomaps API for Home map * Update maplibre-gl * feat: use Maplibre & Protomaps in Rails * Cleanup unused method & dependencies * Ensure Tileserver config is set to serve all fonts for appropriate fallback * Update .env.example with new keys * Add Protomaps API key and basemap style attributes to Theme * Update Theme form to update protomaps attrs * Update Map's to utilize protomaps API when provided Also update basemap style theme when provided, default to Contrast otherwise. * fix: Story show map should allow panning * Allow static map to have zoom nav controls * Update manage community specs w/ new Map service * Add default value for Mapbox marker colors * Add/update Theme specs --------- Co-authored-by: Laura Mosher <[email protected]>
1 parent c59ca7a commit 5abcfb6

File tree

33 files changed

+1215
-717
lines changed

33 files changed

+1215
-717
lines changed

.env.example

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,45 @@
1-
# This file is used to build your local dev or offline Terrastories server
2-
# in Docker Compose.
1+
##################################
2+
# Configure MapLibre [Recommended]
3+
#
4+
# By default, Terrastories is configured to serve maps using
5+
# Maplibre GL JS with Protomap's free API Tileserver.
6+
#
7+
# If you configure Mapbox, the settings in this section are ignored.
38

4-
# ==> CORS
5-
# CORS, or Cross Origin Request Sharing, allows external requests from allowed
6-
# sites to access the resources in this application.
7-
# The Public Community feature provides an API. For now, this API should only be available from
8-
# allowed origins. These can be configured via this ENV variable to be configurable
9-
# on servers without needing a code change.
10-
CORS_ORIGINS="localhost:1080,\Ahttps:\/\/[a-z0-9]{4}-[0-9]{2}-[0-9]{3}-[0-9]{3}-[0-9]{3}.ngrok.io\z"
9+
# ==> Protomaps Hosted API
10+
#
11+
# Protomaps also maintains a Tiles API - get a free API key.
12+
# It's free for non-commercial use, or commercial use paired with a GitHub sponsorship.
13+
#
14+
# Sign up at https://app.protomaps.com/signup and provide your API key:
15+
# PROTOMAPS_API_KEY=
16+
17+
# ==> Tileserver (hosted or local)
18+
#
19+
# Previously, we provided a hosted Tileserver GL server along side
20+
# Terrastories to serve map tiles in offline mode.
21+
#
22+
# TILESERVER_URL=https://localhost:8080/styles/my-style/style.json
1123

12-
# If you are setting up your own online production instance, please see this page
13-
# https://docs.terrastories.app/setting-up-a-terrastories-server/hosting-environments/hosting-terrastories-online
14-
# If you have any additional questions, reach out to the Terrastories Stewards team
15-
# for help.
24+
##################
25+
# Configure Mapbox
26+
#
27+
# For backwards compatibility, Terrastories still supports map rendering with
28+
# Mapbox and Mapbox styles when configured.
1629

17-
# ==> Default Online Mapbox Configuration
18-
# Mapbox access token and style are required to run Terrastories in an internet-
19-
# connected environment, including local Development. These settings do not need
20-
# to be set here; however, each community must configure them in their Theme
21-
# settings for any mapping functionality to work.
30+
# Set your Mapbox Access Token (DEFAULT_MAPBOX_TOKEN works, but is deprecated)
31+
# MAPBOX_ACCESS_TOKEN=pk.ey
2232

23-
# Configure a Mapbox personal access token to use the mapping functionality
24-
# of terrastories. This Access Token will be used by default across all
25-
# onboarded communities to this instance.
26-
# DEFAULT_MAPBOX_TOKEN=pk.set-your-key-here
33+
# Set your Mapbox Style (DEFAULT_MAP_STYLE works, but is deprecated)
34+
# If unset, Terrastories defaults to Mapbox's streets-v11
35+
# MAPBOX_STYLE=mapbox://styles/mapbox/streets-v11
2736

28-
# Configure a custom Mapbox style. This can be any off-the-shelf style provided
29-
# by Mapbox, or a custom style associated with your personal access token. This
30-
# default map style will be used by default across all onboarded communities to
31-
# this instance.
32-
# DEFAULT_MAP_STYLE=mapbox://styles/mapbox/streets-v11
37+
##################
38+
# Configure CORS
39+
#
40+
# CORS (Cross Origin Request Sharing) allows external requests from allowed
41+
# sites to access resources in this application.
42+
#
43+
# Default origins are provide for local development and ngrok.
44+
# Additional origins can be added with comma-separation.
45+
CORS_ORIGINS="localhost:1080,\Ahttps:\/\/[a-z0-9]{4}-[0-9]{2}-[0-9]{3}-[0-9]{3}-[0-9]{3}.ngrok.io\z"

rails/Gemfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ gem 'pg', '>= 0.18', '< 2.0'
1717
gem 'rgeo', '~> 2.4.0'
1818
gem 'rgeo-geojson', '~> 2.1.1'
1919

20-
# Geocoder for center calculation
21-
gem 'geocoder', '~> 1.8.1'
22-
2320
# Use css_bundling for stylesheets
2421
gem 'cssbundling-rails', '~> 1.1.2'
2522

rails/Gemfile.lock

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ GEM
143143
activerecord (>= 4.2, < 8)
144144
flipper (~> 0.26.2)
145145
formatador (1.1.0)
146-
geocoder (1.8.2)
147146
globalid (1.1.0)
148147
activesupport (>= 5.0)
149148
guard (2.18.0)
@@ -356,7 +355,6 @@ DEPENDENCIES
356355
factory_bot_rails
357356
flipper (~> 0.26.0)
358357
flipper-active_record (~> 0.26.0)
359-
geocoder (~> 1.8.1)
360358
guard-rspec
361359
image_processing (~> 1.12.2)
362360
jbuilder (~> 2.5)

rails/app/controllers/application_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def current_community
2828
end
2929

3030
def offline_community?
31-
Rails.application.config.offline_mode
31+
Map.offline?
3232
end
3333

3434
def user_not_authorized

rails/app/controllers/dashboard/themes_controller.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def theme_params
3636
:mapbox_style_url,
3737
:mapbox_access_token,
3838
:mapbox_3d,
39+
:protomaps_api_key,
40+
:protomaps_basemap_style,
3941
:center_lat,
4042
:center_long,
4143
:sw_boundary_lat,

rails/app/javascript/components/App.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ class App extends Component {
2626
stories: PropTypes.array,
2727
use_local_map_server: PropTypes.bool,
2828
mapbox_access_token: PropTypes.string,
29-
mapbox_style: PropTypes.string,
29+
map_style: PropTypes.string,
3030
mapbox_3d: PropTypes.bool,
3131
map_projection: PropTypes.string,
32+
basemap_style: PropTypes.string,
3233
logo_path: PropTypes.string,
3334
user: PropTypes.object,
3435
center_lat: PropTypes.string,
@@ -347,7 +348,8 @@ class App extends Component {
347348
points={this.state.points}
348349
mapboxAccessToken={this.props.mapbox_access_token}
349350
useLocalMapServer={this.props.use_local_map_server}
350-
mapStyle={this.props.mapbox_style}
351+
basemapStyle={this.props.basemap_style}
352+
mapStyle={this.props.map_style}
351353
mapbox3d={this.props.mapbox_3d}
352354
mapProjection={this.props.map_projection}
353355
clearFilteredStories={this.resetStoriesAndMap}

rails/app/javascript/components/Map.jsx

Lines changed: 34 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import ReactDOM from "react-dom";
22
import React, { Component } from "react";
33
import PropTypes from "prop-types";
4+
import Minimap from "../vendor/mapgl-minimap.js";
45
import Popup from "./Popup";
6+
import { mapStyleLayers } from '../global/protomaps';
57

68
import 'mapbox-gl/dist/mapbox-gl.css';
79
import 'maplibre-gl/dist/maplibre-gl.css';
@@ -18,9 +20,7 @@ export default class Map extends Component {
1820
this.state = {
1921
activePopup: null,
2022
mapGL: null,
21-
isMapLibre: false,
2223
mapModule: null,
23-
minimapModule: null
2424
};
2525
}
2626

@@ -39,34 +39,23 @@ export default class Map extends Component {
3939
};
4040

4141
componentDidMount() {
42+
if (this.state.mapModule) {
43+
this.initializeMap(this.state.mapModule);
44+
return;
45+
}
46+
4247
if (this.props.useLocalMapServer) {
43-
if (!this.state.mapModule) {
44-
import('!maplibre-gl').then(module => {
45-
this.setState({ mapModule: module.default }, () => {
46-
this.initializeMap(this.state.mapModule, true);
47-
});
48+
import('!maplibre-gl').then(module => {
49+
this.setState({ mapModule: module.default }, () => {
50+
this.initializeMap(this.state.mapModule);
4851
});
49-
} else {
50-
this.initializeMap(this.state.mapModule, true);
51-
}
52+
});
5253
} else {
53-
if (!this.state.mapModule || !this.state.minimapModule) {
54-
Promise.all([
55-
import('!mapbox-gl'),
56-
import('../vendor/mapboxgl-control-minimap.js')
57-
]).then(([mapboxGLModule, minimapModule]) => {
58-
this.setState({
59-
mapModule: mapboxGLModule.default,
60-
minimapModule: minimapModule.default
61-
}, () => {
62-
this.Minimap = this.state.minimapModule;
63-
this.initializeMap(this.state.mapModule, false);
64-
});
54+
import('!mapbox-gl').then(module => {
55+
this.setState({ mapModule: module.default }, () => {
56+
this.initializeMap(this.state.mapModule);
6557
});
66-
} else {
67-
this.Minimap = this.state.minimapModule;
68-
this.initializeMap(this.state.mapModule, false);
69-
}
58+
});
7059
}
7160
}
7261

@@ -104,11 +93,14 @@ export default class Map extends Component {
10493
}
10594
}
10695

107-
initializeMap(mapGL, isMapLibre) {
108-
mapGL.accessToken = this.props.useLocalMapServer ? 'pk.ey' : this.props.mapboxAccessToken;
96+
initializeMap(mapGL) {
97+
if (!this.props.useLocalMapServer) {
98+
mapGL.accessToken = this.props.mapboxAccessToken;
99+
}
100+
109101
this.map = new mapGL.Map({
110102
container: this.mapContainer,
111-
style: this.props.mapStyle,
103+
style: mapStyleLayers(this.props.mapStyle, this.props.basemapStyle),
112104
center: [this.props.centerLong, this.props.centerLat],
113105
zoom: this.props.zoom,
114106
maxBounds: this.checkBounds(), // check for bounding box presence
@@ -180,31 +172,21 @@ export default class Map extends Component {
180172
this.addClusterClickHandler();
181173
});
182174

183-
// Hide minimap and nav controls for offline Terrastories
184-
if(!isMapLibre && this.Minimap) {
185-
this.map.addControl(new this.Minimap(
186-
{
187-
style: this.props.mapStyle,
188-
zoomLevels: [
189-
[18, 14, 16],
190-
[16, 12, 14],
191-
[14, 10, 12],
192-
[12, 8, 10],
193-
[10, 6, 8],
194-
[8, 4, 6],
195-
[6, 2, 4],
196-
[3, 0, 2],
197-
[1, 0, 0]
198-
],
199-
lineColor: "#136a7e",
200-
fillColor: "#d77a34",
201-
}), "top-right");
202-
}
175+
// Add MiniMap
176+
this.map.addControl(new Minimap(
177+
mapGL,
178+
{
179+
center: [this.props.centerLong, this.props.centerLat],
180+
maxBounds: this.checkBounds(),
181+
style: mapStyleLayers(this.props.mapStyle, "light"),
182+
lineColor: "#136a7e",
183+
fillColor: "#d77a34",
184+
}), "top-right");
203185

204186
this.map.addControl(new mapGL.NavigationControl());
205187

206188
// Add Maplibre logo for offline Terrastories
207-
if(isMapLibre) {
189+
if(this.props.useLocalMapServer) {
208190
this.map.addControl(new mapGL.LogoControl(), 'bottom-right');
209191
}
210192

@@ -225,8 +207,7 @@ export default class Map extends Component {
225207
})
226208

227209
this.setState({
228-
mapGL: mapGL,
229-
isMapLibre: isMapLibre
210+
mapGL: mapGL
230211
});
231212
}
232213

@@ -287,7 +268,7 @@ export default class Map extends Component {
287268
type: "symbol",
288269
layout: {
289270
'text-field': '{point_count_abbreviated}',
290-
'text-font': ['Open Sans Bold'],
271+
'text-font': this.props.useLocalMapServer ? ['Noto Sans Medium'] : ['Open Sans Bold'],
291272
'text-size': 16,
292273
'text-offset': [0.2, 0.1]
293274
},

0 commit comments

Comments
 (0)