-
Notifications
You must be signed in to change notification settings - Fork 4
/
maplibre_helpers.ts
244 lines (218 loc) · 6.9 KB
/
maplibre_helpers.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
// Helpers for https://maplibre.org/maplibre-gl-js-docs/style-spec/
import type {
Map,
DataDrivenPropertyValueSpecification,
FilterSpecification,
LayerSpecification,
} from "maplibre-gl";
import type { GeoJSON, FeatureCollection, Feature, Geometry } from "geojson";
import turfBbox from "@turf/bbox";
// Some methods take optional params. It's an error to pass in null or undefined, so use default values from
// https://github.com/maplibre/maplibre-style-spec/blob/main/src/reference/v8.json.
const defaultColor = "#000000";
const defaultFilter = true;
const defaultOpacity = 1;
export const isPolygon: FilterSpecification = ["==", "$type", "Polygon"];
export const isLine: FilterSpecification = ["==", "$type", "LineString"];
export const isPoint: FilterSpecification = ["==", "$type", "Point"];
// This sets up a GeoJSON source. MapLibre's API isn't idempotent; you can't
// overwrite an existing source or layer. This complicates Vite's hot-reload
// feature, unless every component correctly tears down all sources and layers.
// These methods workaround that lifetime management hassle by overwriting if
// necessary.
export function overwriteSource(map: Map, id: string, data: GeoJSON) {
if (map.getSource(id)) {
// First remove all layers using this source
let layers = [];
for (let layer of map.getStyle().layers) {
if ("source" in layer && layer.source == id) {
layers.push(layer.id);
}
}
for (let layer of layers) {
map.removeLayer(layer);
}
map.removeSource(id);
}
map.addSource(id, {
type: "geojson",
data,
});
}
// This is an internal helper used by specialized functions for drawing
// circles, lines, and polygons. The layer.id here MUST be present in
// layerZorder.
// TODO It's exported for the LaneDetails Layer helper. Reconsider.
export function overwriteLayer(
map: Map,
layer: LayerSpecification & { source: string }
) {
if (map.getLayer(layer.id)) {
map.removeLayer(layer.id);
}
// layerZorder lists all layers in the desired z-order. map.addLayer takes an
// optional beforeId, placing the new layer beneath this beforeId. Due to
// hot-module reloading and Svelte component initialization being
// unpredictable, we might call overwriteLayer in any order, so use beforeId
// to guarantee we wind up in the correct order.
//
// Find the last layer currently in the map that should be on top of this new
// layer.
let beforeId;
let found = false;
for (let i = layerZorder.length - 1; i >= 0; i--) {
let id = layerZorder[i];
if (id == layer.id) {
found = true;
break;
}
if (map.getLayer(id)) {
beforeId = id;
}
}
// When adding a new layer somewhere, force the programmer to decide where it
// should be z-ordered.
if (!found) {
throw new Error(`Layer ID ${layer.id} not defined in layerZorder`);
}
// If beforeId isn't set, we'll add the layer on top of everything else.
map.addLayer(layer, beforeId);
}
export function overwritePolygonLayer(
map: Map,
params: {
id: string;
source: string;
filter?: FilterSpecification;
color: DataDrivenPropertyValueSpecification<string>;
opacity: DataDrivenPropertyValueSpecification<number>;
}
) {
overwriteLayer(map, {
id: params.id,
source: params.source,
filter: params.filter ?? defaultFilter,
type: "fill",
paint: {
"fill-color": params.color,
"fill-opacity": params.opacity,
},
});
}
export function overwriteCircleLayer(
map: Map,
params: {
id: string;
source: string;
filter?: FilterSpecification;
color?: DataDrivenPropertyValueSpecification<string>;
radius: DataDrivenPropertyValueSpecification<number>;
opacity?: DataDrivenPropertyValueSpecification<number>;
strokeColor?: DataDrivenPropertyValueSpecification<string>;
strokeWidth?: DataDrivenPropertyValueSpecification<number>;
}
) {
overwriteLayer(map, {
id: params.id,
source: params.source,
filter: params.filter ?? defaultFilter,
type: "circle",
paint: {
"circle-radius": params.radius,
"circle-color": params.color ?? defaultColor,
"circle-opacity": params.opacity ?? defaultOpacity,
"circle-stroke-color": params.strokeColor ?? defaultColor,
"circle-stroke-width": params.strokeWidth ?? 0,
},
});
}
export function overwriteLineLayer(
map: Map,
params: {
id: string;
source: string;
filter?: FilterSpecification;
color: DataDrivenPropertyValueSpecification<string>;
width: DataDrivenPropertyValueSpecification<number>;
opacity?: DataDrivenPropertyValueSpecification<number>;
}
) {
overwriteLayer(map, {
id: params.id,
source: params.source,
filter: params.filter ?? defaultFilter,
type: "line",
layout: {
"line-cap": "round",
"line-join": "round",
},
paint: {
"line-color": params.color,
"line-width": params.width,
"line-opacity": params.opacity ?? defaultOpacity,
},
});
}
export function emptyGeojson(): FeatureCollection {
return {
type: "FeatureCollection",
features: [],
};
}
// Helper for https://maplibre.org/maplibre-style-spec/expressions/#case based on one property
export function caseHelper(
getKey: string,
map: { [name: string]: string },
backup: string
): any[] {
let x: any[] = ["case"];
for (let [key, value] of Object.entries(map)) {
x.push(["==", ["get", getKey], key]);
x.push(value);
}
x.push(backup);
return x;
}
// Suitable for passing to map.fitBounds. Work around https://github.com/Turfjs/turf/issues/1807.
export function bbox(gj: GeoJSON): [number, number, number, number] {
return turfBbox(gj) as [number, number, number, number];
}
// Properties are guaranteed to exist
export type FeatureWithProps<G extends Geometry> = Feature<G> & {
properties: { [name: string]: any };
};
// All layer IDs used with overwriteLayer must be defined here, with later
// entries drawn on top.
const layerZorder = [
// Polygons are bigger than lines, which're bigger than points. When geometry
// overlaps, put the smaller thing on top
"interventions-polygons",
// This is an outline, so draw on top
"hover-polygons",
// The hover effect thickens, so draw beneath
"hover-lines",
"interventions-lines",
"interventions-lines-endpoints",
"hover-points",
"interventions-points",
"edit-point-mode",
"edit-polygon-fill",
"edit-polygon-lines",
"edit-polygon-vertices",
"draw-split-route",
"draw-street-view",
"speed-limits",
"lane-polygons-layer",
"intersection-polygons-layer",
"lane-markings-layer",
"intersection-markings-layer",
// When editing a route, draw it over contextual layers like speed limits
"route-points",
"route-lines",
"route-polygons",
// Draw most things beneath text road labels. This is the only layer in this
// list generated by the MapTiler basemap we use.
"road_label",
// Draw the inverted boundary fade on top of basemap labels
"boundary",
];