@macrostrat/map-interface 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,419 @@
1
+ .map-page
2
+ display: flex
3
+ flex-direction: column
4
+ position: relative
5
+ width: 100%
6
+ height: 100%
7
+ &.fit-viewport
8
+ height: 100vh
9
+ width: 100vw
10
+ /* mobile viewport bug fix */
11
+ max-height: -webkit-fill-available
12
+
13
+ // Compass display
14
+ .compass-control
15
+ display: none
16
+ &.map-is-rotated
17
+ .compass-control
18
+ display: block
19
+
20
+ .map-3d-control
21
+ display: none
22
+ &.map-3d-available .map-3d-control
23
+ display: block
24
+ &.map-is-rotated.map-3d-available .map-3d-control
25
+ display: none
26
+
27
+ .globe-control
28
+ display: none
29
+ svg
30
+ color: var(--secondary-color)
31
+ &.map-is-global .globe-control
32
+ display: block
33
+
34
+ .main-ui
35
+ flex: 1
36
+ position: relative
37
+ display: flex
38
+ flex-direction: column
39
+ max-height: 100%
40
+ height: 100%
41
+ box-shadow: 0 0 10px 4px var(--card-shadow-color)
42
+
43
+ .panel-card
44
+ padding: 10px
45
+ background-color: var(--panel-background-color)
46
+
47
+ & >:last-child
48
+ margin-bottom: 0
49
+
50
+ :global(.bp4-dark) .panel-card
51
+ background-color: var(--panel-background-color)
52
+
53
+ .context-stack
54
+ display: flex
55
+ flex-direction: column
56
+ max-height: 100%
57
+
58
+ &>div
59
+ flex-shrink: 1
60
+
61
+ &>.searchbar
62
+ flex: 0
63
+
64
+ .context-stack,
65
+ .detail-stack
66
+ z-index: 100
67
+
68
+ .panel-container
69
+ display: flex
70
+ flex-direction: column
71
+
72
+ &>div
73
+ pointer-events: all
74
+
75
+ .panel-title
76
+ font-size: 16px
77
+
78
+ .spacer
79
+ flex-grow: 1
80
+ pointer-events: none
81
+
82
+ .map-view-container
83
+ flex-grow: 1
84
+ position: relative
85
+ overflow: hidden
86
+
87
+ .searchbar-holder
88
+ margin-bottom: 0.5em
89
+
90
+ .right-panel
91
+ width: 24em
92
+
93
+ .buttons
94
+ display: flex
95
+ flex-direction: row
96
+ flex: 1
97
+ min-width: 0
98
+
99
+ .tab-button
100
+ flex-shrink: 1
101
+ min-width: 40px
102
+ overflow: hidden
103
+ text-align: right
104
+
105
+ & :global(.bp4-button-text)
106
+ transition: all 0.2s
107
+ transition-delay: 0.1s
108
+
109
+ .menu-card.narrow-card .panel-header:not(.minimal) &:global(.bp4-active) ~ & :global(.bp4-button-text)
110
+ width: 0
111
+ opacity: 0
112
+ margin-left: -7px
113
+
114
+ .context-panel-leave .menu-card .panel-header & :global(.bp4-button-text)
115
+ opacity: 0
116
+ width: 0
117
+
118
+ .narrow-card.narrow-enter .panel-header .buttons
119
+ margin-right: -500px
120
+
121
+ .panel-header.minimal .tab-button:not(:hover):not(:global(.bp4-active))
122
+ padding-left: 0
123
+ padding-right: 0
124
+ min-width: 30px
125
+ width: 30px
126
+
127
+ .panel-header.minimal .tab-button:not(:hover) :global(.bp4-button-text)
128
+ width: 0
129
+ opacity: 0
130
+ margin-left: -7px
131
+
132
+ .menu-group
133
+ margin-bottom: 0.5em
134
+ margin-top: 0.2em
135
+
136
+ .menu-card :global .bp4-text ul,
137
+ .menu-card :global .text-panel ul
138
+ padding-left: 1em
139
+
140
+ .menu-content
141
+ display: flex
142
+ flex-direction: column
143
+ margin-bottom: -8px
144
+
145
+ & .bp4-button-group
146
+ margin-bottom: 4px
147
+
148
+ & hr
149
+ width: 100%
150
+
151
+ :global #map
152
+ position: absolute
153
+ top: 0
154
+ bottom: 0
155
+ left: 0
156
+ right: 0
157
+
158
+ :global #map .mapbox-compass,
159
+ :global #map .mapbox-3d
160
+ display: none
161
+
162
+ :global .mapboxgl-ctrl.mapbox-3d.mapbox-control
163
+ width: unset
164
+
165
+ :global .mapboxgl-ctrl.mapbox-3d.mapbox-control button
166
+ width: unset
167
+ padding-inline: 4px
168
+
169
+ :global .mapboxgl-canvas-container
170
+ width: 100%
171
+ height: 100%
172
+
173
+ :global .mapboxgl-ctrl.mapboxgl-ctrl-attrib
174
+ background-color: var(--translucent-panel-background-color) !important
175
+
176
+ :global .mapboxgl-ctrl.mapboxgl-ctrl-attrib a
177
+ color: var(--text-color)
178
+
179
+ :global .mapboxgl-marker svg path
180
+ fill: var(--panel-background-color) !important
181
+
182
+ :global .mapboxgl-marker svg circle
183
+ fill: var(--secondary-color) !important
184
+
185
+ :global .mapbox-control.mapbox-zoom
186
+ background: var(--translucent-panel-background-inner)
187
+
188
+ :global .mapbox-control.mapbox-zoom svg
189
+ fill: var(--text-color) !important
190
+
191
+ :global .mapboxgl-ctrl-logo
192
+ transform: scale(0.9) translate(-8px, 2px)
193
+
194
+ :global .bp4-dark .mapboxgl-ctrl-logo
195
+ filter: invert(100%)
196
+
197
+ :global .mapboxgl-ctrl-group button + button
198
+ border-top: 1px solid var(--panel-rule-color) !important
199
+
200
+ :global .bp4-dark .mapboxgl-ctrl-group .mapboxgl-ctrl-icon
201
+ filter: invert(40%)
202
+
203
+ :global .bp4-dark .mapboxgl-ctrl-group .mapboxgl-ctrl-icon:hover
204
+ filter: invert(50%)
205
+
206
+ :global .mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon
207
+ filter: invert(40%)
208
+
209
+ :global .mapboxgl-ctrl-geolocate .mapboxgl-ctrl-icon:hover
210
+ filter: invert(50%)
211
+
212
+ .detail-stack
213
+ position: relative
214
+
215
+ .main-ui.detail-panel-open .zoom-control
216
+ opacity: 0
217
+ display: none
218
+
219
+ .zoom-control
220
+ transition: opacity 1s ease-in-out
221
+ width: 30px
222
+ position: absolute
223
+ top: 0
224
+ right: 0
225
+
226
+ .main-ui.searching .map-view-container
227
+ pointer-events: none
228
+
229
+ .map-controls
230
+ display: flex
231
+ flex-direction: row
232
+ justify-content: right
233
+ margin-bottom: 0
234
+
235
+ &>*
236
+ margin-left: 0.5em
237
+
238
+ .map-controls :global(.mapbox-control),
239
+ .map-controls :global(.map-control-wrapper),
240
+ .map-controls .map-control
241
+ min-width: 22px
242
+ min-height: 22px
243
+ border-radius: 4px
244
+ background-color: var(--panel-background-color)
245
+ box-shadow: 0 0 0 1px var(--card-shadow-color)
246
+
247
+ .map-controls :global(.mapbox-control) button,
248
+ .map-controls :global(.map-control-wrapper) button,
249
+ .map-controls .map-control button
250
+ width: 22px
251
+ height: 22px
252
+ background-position: center center
253
+ padding: 0
254
+ background-color: var(--panel-background-color)
255
+ color: var(--text-color)
256
+
257
+ .map-controls :global(.mapbox-control) button:hover,
258
+ .map-controls :global(.map-control-wrapper) button:hover,
259
+ .map-controls .map-control button:hover
260
+ background-color: var(--panel-background-color) !important
261
+
262
+ .map-controls .map-scale-control
263
+ background: none
264
+ box-shadow: none
265
+ padding-top: 8px
266
+
267
+ :global(.mapboxgl-ctrl-scale)
268
+ background-color: var(--translucent-panel-background-color)
269
+ border-color: var(--secondary-text-color)
270
+ color: var(--secondary-text-color)
271
+
272
+ /* For mobile phones, we want to make the most of screen space,
273
+ which in some cases means adding complications to the basic page. */
274
+ @media only screen and (max-width: 768px)
275
+ .main-ui.detail-panel-enter .context-stack
276
+ height: 0
277
+ visibility: hidden
278
+ transition: height 0.5s ease-in-out
279
+
280
+ .detail-stack
281
+ height: fit-content
282
+ position: inherit
283
+ max-height: 70%
284
+
285
+ .infodrawer-stack
286
+ max-height: 70%
287
+
288
+ :global(.exit-active)
289
+ max-height: 0
290
+
291
+ :global(.mapbox-control.mapbox-zoom)
292
+ display: none
293
+
294
+ .map-controls
295
+ position: absolute
296
+ top: -60px
297
+ right: 10px
298
+
299
+ .detail-panel
300
+ border-radius: 0px
301
+
302
+ /* Desktop styling is necessarily much more complicated than mobile
303
+ to handle a two-column layout. */
304
+ @media screen and (min-width: 768px)
305
+ .main-ui
306
+ flex-direction: row
307
+ padding: 1em 1em 2em
308
+ min-height: 80px
309
+ &>*
310
+ margin-right: 0.5em
311
+ &>*:last-child
312
+ margin-right: 0
313
+
314
+ &.searching .context-stack
315
+ width: 24em
316
+
317
+ .context-stack
318
+ max-width: 34em
319
+ min-width: 14em
320
+ width: 16em
321
+ transition: width 300ms ease
322
+ padding-bottom: 0.5em
323
+
324
+ &.panel-open
325
+ width: 34em
326
+ margin-right: 0.5em
327
+
328
+ .context-stack.layers,
329
+ .context-stack.settings
330
+ width: 18em
331
+ margin-right: 0.5em
332
+
333
+ .main-ui.detail-panel-open .detail-stack
334
+ width: 30em
335
+
336
+ .detail-stack
337
+ width: 30em
338
+ pointer-events: none
339
+ display: flex
340
+ flex-direction: column
341
+
342
+ .context-stack,
343
+ .detail-stack
344
+ pointer-events: none
345
+ z-index: 100
346
+
347
+ .context-stack>div,
348
+ .detail-stack>div
349
+ pointer-events: all
350
+ margin-bottom: 0.5em
351
+
352
+ .context-stack>div:last-child,
353
+ .detail-stack>div:last-child
354
+ margin-bottom: 0
355
+
356
+ .context-stack>div.spacer,
357
+ .detail-stack>div.spacer
358
+ pointer-events: none
359
+
360
+ .map-view-container
361
+ position: unset
362
+
363
+ .main-ui .detail-panel
364
+ transition: opacity 0.8s ease, height 0.8s ease, max-height 0.8s ease
365
+
366
+ .main-ui.detail-panel-from .detail-panel
367
+ opacity: 0
368
+
369
+ .main-ui.detail-panel-enter .detail-panel
370
+ opacity: 1
371
+
372
+ .main-ui.detail-panel-leave .detail-panel
373
+ opacity: 0
374
+
375
+ .main-ui .context-panel
376
+ transition: opacity 0.8s ease, height 0.8s ease, max-height 0.8s ease
377
+
378
+ .main-ui.context-panel-from .context-panel
379
+ opacity: 0
380
+
381
+ .main-ui.context-panel-enter .context-panel
382
+ opacity: 1
383
+
384
+ .main-ui.context-panel-leave .context-panel
385
+ opacity: 0
386
+
387
+ @media only screen and (max-width: 768px)
388
+ .main-ui .detail-stack
389
+ transition: opacity 0.8s ease, height 0.8s ease, max-height 0.8s ease
390
+
391
+ .main-ui.detail-panel-from .detail-stack
392
+ max-height: 0
393
+ height: 0
394
+
395
+ .main-ui.detail-panel-leave .detail-stack
396
+ max-height: 0
397
+ max-height: 0
398
+
399
+ .main-ui .context-panel
400
+ transition: opacity 0.8s ease, height 0.8s ease, max-height 0.8s ease
401
+
402
+ .main-ui.context-panel-from .context-panel
403
+ max-height: 0
404
+ height: 0
405
+
406
+ .main-ui.context-panel-leave .context-panel
407
+ max-height: 0
408
+ max-height: 0
409
+
410
+ // Shift UI around to center elements if we're in the global view
411
+ @media only screen and (min-width: 768px)
412
+ .map-page .main-ui.detail-panel-leave .map-view-container
413
+ margin-right: -14em
414
+
415
+ .map-page.map-is-global .main-ui.detail-panel-leave .map-view-container
416
+ margin-right: -30em
417
+
418
+ .map-page.map-is-global .main-ui.context-panel-leave .map-view-container
419
+ margin-left: -16em
@@ -0,0 +1,163 @@
1
+ import hyper from "@macrostrat/hyper";
2
+ import {
3
+ useMapRef,
4
+ useMapStatus,
5
+ useMapDispatch,
6
+ } from "@macrostrat/mapbox-react";
7
+ import {
8
+ mapViewInfo,
9
+ MapPosition,
10
+ setMapPosition,
11
+ } from "@macrostrat/mapbox-utils";
12
+ import classNames from "classnames";
13
+ import mapboxgl from "mapbox-gl";
14
+ import { useEffect, useRef } from "react";
15
+ import styles from "./main.module.sass";
16
+ import rootStyles from "../main.module.sass";
17
+ import { enable3DTerrain } from "./terrain";
18
+ import {
19
+ MapLoadingReporter,
20
+ MapMovedReporter,
21
+ MapPaddingManager,
22
+ MapResizeManager,
23
+ } from "../helpers";
24
+
25
+ const h = hyper.styled({ ...styles, ...rootStyles });
26
+
27
+ type MapboxCoreOptions = Omit<mapboxgl.MapboxOptions, "container">;
28
+
29
+ export interface MapViewProps extends MapboxCoreOptions {
30
+ showLineSymbols?: boolean;
31
+ children?: React.ReactNode;
32
+ accessToken?: string;
33
+ terrainSourceID?: string;
34
+ enableTerrain?: boolean;
35
+ infoMarkerPosition?: mapboxgl.LngLatLike;
36
+ //style: mapboxgl.Style | string;
37
+ //transformRequest?: mapboxgl.TransformRequestFunction;
38
+ mapPosition?: MapPosition;
39
+ }
40
+
41
+ function initializeMap(container, args: MapboxCoreOptions = {}) {
42
+ const map = new mapboxgl.Map({
43
+ container,
44
+ maxZoom: 18,
45
+ //maxTileCacheSize: 0,
46
+ logoPosition: "bottom-left",
47
+ trackResize: true,
48
+ antialias: true,
49
+ optimizeForTerrain: true,
50
+ ...args,
51
+ });
52
+
53
+ //setMapPosition(map, mapPosition);
54
+ return map;
55
+ }
56
+
57
+ const defaultMapPosition: MapPosition = {
58
+ camera: {
59
+ lat: 34,
60
+ lng: -120,
61
+ altitude: 300000,
62
+ },
63
+ };
64
+
65
+ export function MapView(props: MapViewProps) {
66
+ let { terrainSourceID } = props;
67
+ const {
68
+ enableTerrain = true,
69
+ style,
70
+ transformRequest,
71
+ mapPosition = defaultMapPosition,
72
+ children,
73
+ accessToken,
74
+ infoMarkerPosition,
75
+ projection,
76
+ } = props;
77
+ if (enableTerrain) {
78
+ terrainSourceID ??= "mapbox-3d-dem";
79
+ }
80
+
81
+ if (accessToken != null) {
82
+ mapboxgl.accessToken = accessToken;
83
+ }
84
+
85
+ const dispatch = useMapDispatch();
86
+ let mapRef = useMapRef();
87
+ const ref = useRef<HTMLDivElement>();
88
+ const parentRef = useRef<HTMLDivElement>();
89
+
90
+ // Keep track of map position for reloads
91
+
92
+ useEffect(() => {
93
+ if (style == null || ref.current == null || dispatch == null) return;
94
+ if (mapRef?.current != null) return;
95
+ console.log("Initializing map");
96
+ const map = initializeMap(ref.current, {
97
+ style,
98
+ transformRequest,
99
+ projection,
100
+ });
101
+ dispatch({ type: "set-map", payload: map });
102
+ console.log("Map initialized");
103
+ return () => {
104
+ map.remove();
105
+ dispatch({ type: "set-map", payload: null });
106
+ };
107
+ }, [transformRequest, dispatch, style]);
108
+
109
+ // Map style updating
110
+ useEffect(() => {
111
+ if (mapRef?.current == null || style == null) return;
112
+ mapRef?.current?.setStyle(style);
113
+ }, [mapRef.current, style]);
114
+
115
+ useEffect(() => {
116
+ const map = mapRef.current;
117
+ if (map == null || mapPosition == null) return;
118
+ setMapPosition(map, mapPosition);
119
+ }, [mapRef.current]);
120
+
121
+ const { mapPosition: _computedMapPosition } = useMapStatus();
122
+ const { mapUse3D, mapIsRotated } = mapViewInfo(_computedMapPosition);
123
+
124
+ // Get map projection
125
+ const _projection = mapRef.current?.getProjection()?.name ?? "mercator";
126
+
127
+ const className = classNames(
128
+ {
129
+ "is-rotated": mapIsRotated ?? false,
130
+ "is-3d-available": mapUse3D ?? false,
131
+ },
132
+ `${_projection}-projection`
133
+ );
134
+
135
+ return h("div.map-view-container.main-view", { ref: parentRef }, [
136
+ h("div.mapbox-map#map", { ref, className }),
137
+ h(MapLoadingReporter, {
138
+ ignoredSources: ["elevationMarker", "crossSectionEndpoints"],
139
+ }),
140
+ h(MapMovedReporter),
141
+ h(MapResizeManager, { containerRef: ref }),
142
+ h(MapPaddingManager, { containerRef: ref, parentRef, infoMarkerPosition }),
143
+ h(MapTerrainManager, { mapUse3D, terrainSourceID }),
144
+ children,
145
+ ]);
146
+ }
147
+
148
+ export function MapTerrainManager({
149
+ mapUse3D,
150
+ terrainSourceID,
151
+ }: {
152
+ mapUse3D?: boolean;
153
+ terrainSourceID?: string;
154
+ }) {
155
+ const mapRef = useMapRef();
156
+
157
+ useEffect(() => {
158
+ const map = mapRef.current;
159
+ if (map == null) return;
160
+ enable3DTerrain(map, mapUse3D, terrainSourceID);
161
+ }, [mapRef.current, mapUse3D]);
162
+ return null;
163
+ }
@@ -0,0 +1,13 @@
1
+ .map-view-container
2
+ flex-grow: 1
3
+ position: relative
4
+ overflow: hidden
5
+
6
+ /* Desktop styling is necessarily much more complicated than mobile
7
+ to handle a two-column layout. */
8
+ @media screen and (min-width: 768px)
9
+ /* Make map fill page rather than containing div,
10
+ by unsetting map position */
11
+ // We should move this to another file.
12
+ .map-view-container
13
+ position: unset
@@ -0,0 +1,60 @@
1
+ // We should merge this with code in @macrostrat/mapbox-react/src/terrain.ts:
2
+
3
+ export function enable3DTerrain(
4
+ map,
5
+ shouldEnable: boolean,
6
+ sourceID: string | null = null
7
+ ) {
8
+ let demSourceID = sourceID ?? getTerrainSourceID(map) ?? "mapbox-dem";
9
+
10
+ console.log("Enabling 3D terrain with source", demSourceID);
11
+
12
+ if (!map.style?._loaded) {
13
+ map.once("style.load", () => {
14
+ enable3DTerrain(map, shouldEnable, demSourceID);
15
+ });
16
+ return;
17
+ }
18
+
19
+ // Enable or disable terrain depending on our current desires...
20
+ const currentTerrain = map.getTerrain();
21
+ if (!shouldEnable) {
22
+ if (currentTerrain != null) map.setTerrain(null);
23
+ return;
24
+ }
25
+ if (currentTerrain != null) return;
26
+
27
+ // Add a DEM source if one is not found already.
28
+ if (map.getSource(demSourceID) == null) {
29
+ map.addSource(demSourceID, {
30
+ type: "raster-dem",
31
+ url: "mapbox://mapbox.mapbox-terrain-dem-v1",
32
+ tileSize: 512,
33
+ maxzoom: 14,
34
+ });
35
+ }
36
+
37
+ // add a sky layer that will show when the map is highly pitched
38
+ if (map.getLayer("sky") == null) {
39
+ map.addLayer({
40
+ id: "sky",
41
+ type: "sky",
42
+ paint: {
43
+ "sky-type": "atmosphere",
44
+ "sky-atmosphere-sun": [0.0, 0.0],
45
+ "sky-atmosphere-sun-intensity": 15,
46
+ },
47
+ });
48
+ }
49
+
50
+ map.setTerrain({ source: demSourceID, exaggeration: 1 });
51
+ }
52
+
53
+ function getTerrainSourceID(map) {
54
+ for (const [key, source] of Object.entries(map.getStyle().sources)) {
55
+ if (source.type == "raster-dem") {
56
+ return key;
57
+ }
58
+ }
59
+ return null;
60
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { useEffect } from "react";
2
+ import { Marker } from "mapbox-gl";
3
+
4
+ function calcMapPadding(rect, childRect) {
5
+ return {
6
+ left: Math.max(rect.left - childRect.left, 0),
7
+ top: Math.max(rect.top - childRect.top, 0),
8
+ right: Math.max(childRect.right - rect.right, 0),
9
+ bottom: Math.max(childRect.bottom - rect.bottom, 0),
10
+ };
11
+ }
12
+
13
+ export function getMapPadding(ref, parentRef) {
14
+ const rect = parentRef.current?.getBoundingClientRect();
15
+ const childRect = ref.current?.getBoundingClientRect();
16
+ if (rect == null || childRect == null) return;
17
+ return calcMapPadding(rect, childRect);
18
+ }
19
+
20
+ export function useMapMarker(mapRef, markerRef, markerPosition) {
21
+ useEffect(() => {
22
+ const map = mapRef.current;
23
+ if (map == null) return;
24
+ if (markerPosition == null) {
25
+ markerRef.current?.remove();
26
+ return;
27
+ }
28
+ const marker = markerRef.current ?? new Marker();
29
+ marker.setLngLat(markerPosition).addTo(map);
30
+ markerRef.current = marker;
31
+ return () => marker.remove();
32
+ }, [mapRef.current, markerPosition]);
33
+ }