@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.
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@macrostrat/map-interface",
3
+ "version": "0.0.1",
4
+ "description": "Map interface for Macrostrat",
5
+ "type": "module",
6
+ "source": "src/index.ts",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.mjs",
9
+ "dependencies": {
10
+ "@macrostrat/hyper": "^2.2.1",
11
+ "@macrostrat/mapbox-react": "2.0.0",
12
+ "@macrostrat/mapbox-utils": "1.1.1",
13
+ "@mapbox/tilebelt": "^1.0.2",
14
+ "@turf/bbox": "^6.5.0",
15
+ "chroma-js": "^2.4.2",
16
+ "classnames": "^2.3.2",
17
+ "d3-array": "^3.2.4",
18
+ "d3-format": "^3.1.0",
19
+ "mapbox-gl": "^2.11.0",
20
+ "transition-hook": "^1.5.2",
21
+ "underscore": "^1.13.6",
22
+ "use-resize-observer": "^9.1.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@blueprintjs/core": "^3.43.0 || ^4.3.0",
26
+ "react": "^16.8.6||^17.0.0||^18.0.0"
27
+ },
28
+ "scripts": {
29
+ "dev": "parcel watch",
30
+ "build": "parcel build"
31
+ },
32
+ "exports": {
33
+ "typescript": "./src/index.ts"
34
+ }
35
+ }
@@ -0,0 +1,140 @@
1
+ import hyper from "@macrostrat/hyper";
2
+ import { HTMLDivProps } from "@blueprintjs/core";
3
+ import styles from "./main.module.sass";
4
+ import classNames from "classnames";
5
+ import { useTransition } from "transition-hook";
6
+ import {
7
+ MapboxMapProvider,
8
+ ZoomControl,
9
+ useMapStatus,
10
+ } from "@macrostrat/mapbox-react";
11
+ import { ToasterContext } from "@macrostrat/ui-components";
12
+ import { MapBottomControls } from "./controls";
13
+ import { mapViewInfo, MapPosition } from "@macrostrat/mapbox-utils";
14
+ import { Card } from "@blueprintjs/core";
15
+
16
+ import { ReactNode } from "react";
17
+
18
+ export * from "./location-panel";
19
+ export * from "./context-panel";
20
+ //export * from "./dev";
21
+
22
+ export function MapInterface() {
23
+ return h("div", "Hello world");
24
+ }
25
+
26
+ const h = hyper.styled(styles);
27
+
28
+ type AnyElement = React.ReactNode | React.ReactElement | React.ReactFragment;
29
+
30
+ export const PanelCard = (props) =>
31
+ h(Card, { ...props, className: classNames("panel-card", props.className) });
32
+
33
+ function _MapAreaContainer({
34
+ children,
35
+ className,
36
+ navbar,
37
+ contextPanel = null,
38
+ detailPanel = null,
39
+ detailPanelOpen,
40
+ contextPanelOpen = true,
41
+ bottomPanel = null,
42
+ mainPanel,
43
+ mapControls = h(MapBottomControls),
44
+ contextStackProps = null,
45
+ detailStackProps = null,
46
+ mapPosition = null,
47
+ fitViewport = true,
48
+ ...rest
49
+ }: {
50
+ navbar: AnyElement;
51
+ children?: AnyElement;
52
+ mapControls?: AnyElement;
53
+ contextPanel?: AnyElement;
54
+ mainPanel?: AnyElement;
55
+ detailPanel?: AnyElement;
56
+ bottomPanel?: AnyElement;
57
+ className?: string;
58
+ detailPanelOpen?: boolean;
59
+ contextPanelOpen?: boolean;
60
+ contextStackProps?: HTMLDivProps;
61
+ detailStackProps?: HTMLDivProps;
62
+ mapPosition?: MapPosition;
63
+ fitViewport?: boolean;
64
+ }) {
65
+ const _detailPanelOpen = detailPanelOpen ?? detailPanel != null;
66
+ const contextPanelTrans = useTransition(contextPanelOpen, 800);
67
+ const detailPanelTrans = useTransition(_detailPanelOpen, 800);
68
+
69
+ /*- We apply a custom style to the panel container when we are interacting
70
+ with the search bar, so that we can block map interactions until search
71
+ bar focus is lost.
72
+ - We also apply a custom style when the infodrawer is open so we can hide
73
+ the search bar on mobile platforms
74
+ - These styles are doubly applied so we can have both namespaced and
75
+ outside-accessible styles for each case.
76
+ */
77
+ const _className = classNames(
78
+ {
79
+ searching: false,
80
+ "detail-panel-open": _detailPanelOpen,
81
+ "map-context-open": _detailPanelOpen,
82
+ "fit-viewport": fitViewport,
83
+ },
84
+ `context-panel-${contextPanelTrans.stage}`,
85
+ `map-context-${contextPanelTrans.stage}`,
86
+ `detail-panel-${detailPanelTrans.stage}`,
87
+ `map-detail-${detailPanelTrans.stage}`
88
+ );
89
+
90
+ return h(
91
+ MapStyledContainer,
92
+ { className: classNames("map-page", className), mapPosition },
93
+ [
94
+ h("div.main-ui", { className: _className, ...rest }, [
95
+ h("div.context-stack", contextStackProps, [
96
+ navbar,
97
+ h.if(contextPanelTrans.shouldMount)([contextPanel]),
98
+ ]),
99
+ //h(MapView),
100
+ children ?? mainPanel,
101
+ h("div.detail-stack.infodrawer-container", detailStackProps, [
102
+ detailPanel,
103
+ h(ZoomControl, { className: "zoom-control" }),
104
+ h("div.spacer"),
105
+ mapControls,
106
+ ]),
107
+ ]),
108
+ h("div.bottom", null, bottomPanel),
109
+ ]
110
+ );
111
+ }
112
+
113
+ const MapProviders = ({ children }) =>
114
+ h(ToasterContext, h(MapboxMapProvider, children));
115
+
116
+ export const MapAreaContainer = (props) =>
117
+ h(MapProviders, h(_MapAreaContainer, props));
118
+
119
+ interface MapContainerProps {
120
+ className?: string;
121
+ mapPosition?: MapPosition;
122
+ children?: ReactNode;
123
+ }
124
+
125
+ export function MapStyledContainer({ className, children }: MapContainerProps) {
126
+ const { mapPosition } = useMapStatus();
127
+ if (mapPosition != null) {
128
+ const { mapIsRotated, mapUse3D, mapIsGlobal } = mapViewInfo(mapPosition);
129
+ className = classNames(className, {
130
+ "map-is-rotated": mapIsRotated,
131
+ "map-3d-available": mapUse3D,
132
+ "map-is-global": mapIsGlobal,
133
+ });
134
+ }
135
+
136
+ return h("div", { className }, children);
137
+ }
138
+
139
+ export * from "./map-view";
140
+ //const _MapPage = compose(HotkeysProvider, MapPage);
@@ -0,0 +1,53 @@
1
+ import { useMemo } from "react";
2
+ import { Navbar, Button, InputGroup, Spinner, Card } from "@blueprintjs/core";
3
+ import hyper from "@macrostrat/hyper";
4
+ import styles from "./main.module.sass";
5
+ import { useMapStatus } from "@macrostrat/mapbox-react";
6
+
7
+ const h = hyper.styled(styles);
8
+
9
+ const spinnerElement = h(Spinner, { size: 16 });
10
+
11
+ export function LoadingButton({
12
+ isLoading = false,
13
+ onClick,
14
+ active = false,
15
+ icon = "menu",
16
+ }) {
17
+ return h(Button, {
18
+ icon: isLoading ? spinnerElement : icon,
19
+ large: true,
20
+ minimal: true,
21
+ onClick,
22
+ active: active && !isLoading,
23
+ });
24
+ }
25
+
26
+ export function MapLoadingButton(props) {
27
+ const { isLoading } = useMapStatus();
28
+ const mapIsLoading = useMemo(() => isLoading, [isLoading]);
29
+ return h(LoadingButton, { ...props, isLoading: mapIsLoading });
30
+ }
31
+
32
+ type AnyChildren = React.ReactNode | React.ReactFragment;
33
+
34
+ export function FloatingNavbar({
35
+ className,
36
+ children,
37
+ statusElement = null,
38
+ }: {
39
+ className?: string;
40
+ children?: AnyChildren;
41
+ statusElement?: AnyChildren;
42
+ }) {
43
+ return h("div.searchbar-holder", { className }, [
44
+ h("div.navbar-holder", [
45
+ h(Navbar, { className: "searchbar panel" }, children),
46
+ ]),
47
+ h.if(statusElement != null)(
48
+ Card,
49
+ { className: "status-tongue" },
50
+ statusElement
51
+ ),
52
+ ]);
53
+ }
@@ -0,0 +1,37 @@
1
+ .searchbar-holder
2
+ transition: margin 300ms
3
+ display: flex
4
+ flex-direction: column
5
+ margin: 0
6
+ .navbar-holder
7
+ display: flex
8
+ flex-direction: row
9
+ .searchbar
10
+ width: 100%
11
+ background-color: var(--panel-background-color)
12
+ border-radius: 3px
13
+ padding: 0 5px
14
+ display: flex
15
+ flex-direction: row
16
+ align-items: center
17
+ :global(.bp4-input-group)
18
+ flex-grow: 1
19
+ cursor: text
20
+ &>*
21
+ margin-right: 5px
22
+ &:last-child
23
+ margin-right: 0
24
+
25
+ .status-tongue
26
+ background-color: var(--panel-background-color)
27
+ margin: 5px
28
+ margin-top: -12px
29
+ padding: 0
30
+ padding-top: 12px
31
+
32
+ @media screen and (max-width: 768px)
33
+ .status-tongue
34
+ max-width: 100vw
35
+ margin: 0
36
+ border-radius: 0
37
+ margin-top: -3px
@@ -0,0 +1,52 @@
1
+ import { useRef } from "react";
2
+ import { GeolocateControl } from "mapbox-gl";
3
+ import hyper from "@macrostrat/hyper";
4
+ import styles from "./main.module.sass";
5
+ import {
6
+ CompassControl,
7
+ GlobeControl,
8
+ ThreeDControl,
9
+ MapControlWrapper,
10
+ } from "@macrostrat/mapbox-react";
11
+ import { ScaleControl as BaseScaleControl } from "mapbox-gl";
12
+
13
+ const h = hyper.styled(styles);
14
+
15
+ function ScaleControl(props) {
16
+ const optionsRef = useRef({
17
+ maxWidth: 200,
18
+ unit: "metric",
19
+ });
20
+ return h(MapControlWrapper, {
21
+ className: "map-scale-control",
22
+ control: BaseScaleControl,
23
+ options: optionsRef.current,
24
+ ...props,
25
+ });
26
+ }
27
+
28
+ function GeolocationControl(props) {
29
+ const optionsRef = useRef({
30
+ showAccuracyCircle: true,
31
+ showUserLocation: true,
32
+ trackUserLocation: true,
33
+ positionOptions: {
34
+ enableHighAccuracy: true,
35
+ },
36
+ });
37
+ return h(MapControlWrapper, {
38
+ control: GeolocateControl,
39
+ options: optionsRef.current,
40
+ ...props,
41
+ });
42
+ }
43
+
44
+ export function MapBottomControls() {
45
+ return h("div.map-controls", [
46
+ h(ScaleControl),
47
+ h(ThreeDControl, { className: "map-3d-control" }),
48
+ h(CompassControl, { className: "compass-control" }),
49
+ h(GlobeControl, { className: "globe-control" }),
50
+ h(GeolocationControl, { className: "geolocation-control" }),
51
+ ]);
52
+ }
@@ -0,0 +1,179 @@
1
+ // Import other components
2
+ import { Switch } from "@blueprintjs/core";
3
+ import hyper from "@macrostrat/hyper";
4
+ import { Spacer, useDarkMode, useStoredState } from "@macrostrat/ui-components";
5
+ import mapboxgl from "mapbox-gl";
6
+ import { useCallback, useState, useEffect } from "react";
7
+ import { buildInspectorStyle, buildXRayStyle } from "./xray";
8
+ import { MapAreaContainer, PanelCard } from "../container";
9
+ import { FloatingNavbar, MapLoadingButton } from "../context-panel";
10
+ import { MapMarker } from "../helpers";
11
+ import { LocationPanel } from "../location-panel";
12
+ import { MapView } from "../map-view";
13
+ import styles from "./main.module.sass";
14
+ import { TileExtentLayer } from "./tile-extent";
15
+ import {
16
+ FeaturePanel,
17
+ FeatureSelectionHandler,
18
+ TileInfo,
19
+ } from "./vector-tile-features";
20
+ import { MapPosition } from "@macrostrat/mapbox-utils";
21
+
22
+ export enum MacrostratVectorTileset {
23
+ Carto = "carto",
24
+ CartoSlim = "carto-slim",
25
+ IGCPOrogens = "igcp-orogens",
26
+ }
27
+
28
+ export enum MacrostratRasterTileset {
29
+ Carto = "carto",
30
+ Emphasized = "emphasized",
31
+ }
32
+
33
+ export const h = hyper.styled(styles);
34
+
35
+ export function DevMapPage({
36
+ title = "Map inspector",
37
+ headerElement = null,
38
+ transformRequest = null,
39
+ mapPosition = null,
40
+ mapboxToken = null,
41
+ overlayStyle = null,
42
+ children,
43
+ style,
44
+ focusedSource = null,
45
+ focusedSourceTitle = null,
46
+ projection = null,
47
+ }: {
48
+ headerElement?: React.ReactElement;
49
+ transformRequest?: mapboxgl.TransformRequestFunction;
50
+ title?: string;
51
+ style: mapboxgl.Style | string;
52
+ children?: React.ReactNode;
53
+ mapboxToken?: string;
54
+ overlayStyle?: mapboxgl.Style | string;
55
+ focusedSource?: string;
56
+ focusedSourceTitle?: string;
57
+ projection?: string;
58
+ mapPosition?: MapPosition;
59
+ }) {
60
+ /* We apply a custom style to the panel container when we are interacting
61
+ with the search bar, so that we can block map interactions until search
62
+ bar focus is lost.
63
+ We also apply a custom style when the infodrawer is open so we can hide
64
+ the search bar on mobile platforms
65
+ */
66
+
67
+ const dark = useDarkMode();
68
+ const isEnabled = dark?.isEnabled;
69
+
70
+ if (mapboxToken != null) {
71
+ mapboxgl.accessToken = mapboxToken;
72
+ }
73
+
74
+ style ??= isEnabled
75
+ ? "mapbox://styles/mapbox/dark-v10"
76
+ : "mapbox://styles/mapbox/light-v10";
77
+
78
+ const [isOpen, setOpen] = useState(false);
79
+
80
+ const [state, setState] = useStoredState("macrostrat:dev-map-page", {
81
+ showTileExtent: false,
82
+ xRay: false,
83
+ });
84
+ const { showTileExtent, xRay } = state;
85
+
86
+ const [actualStyle, setActualStyle] = useState(style);
87
+
88
+ useEffect(() => {
89
+ buildInspectorStyle(style, overlayStyle, {
90
+ mapboxToken,
91
+ inDarkMode: isEnabled,
92
+ xRay,
93
+ }).then(setActualStyle);
94
+ }, [style, xRay, mapboxToken, isEnabled, overlayStyle]);
95
+
96
+ const [inspectPosition, setInspectPosition] =
97
+ useState<mapboxgl.LngLat | null>(null);
98
+
99
+ const [data, setData] = useState(null);
100
+
101
+ const onSelectPosition = useCallback((position: mapboxgl.LngLat) => {
102
+ setInspectPosition(position);
103
+ }, []);
104
+
105
+ let detailElement = null;
106
+ if (inspectPosition != null) {
107
+ detailElement = h(
108
+ LocationPanel,
109
+ {
110
+ onClose() {
111
+ setInspectPosition(null);
112
+ },
113
+ position: inspectPosition,
114
+ },
115
+ [
116
+ h(TileInfo, {
117
+ feature: data?.[0] ?? null,
118
+ showExtent: showTileExtent,
119
+ setShowExtent() {
120
+ setState({ ...state, showTileExtent: !showTileExtent });
121
+ },
122
+ }),
123
+ h(FeaturePanel, { features: data, focusedSource, focusedSourceTitle }),
124
+ ]
125
+ );
126
+ }
127
+
128
+ let tile = null;
129
+ if (showTileExtent && data?.[0] != null) {
130
+ let f = data[0];
131
+ tile = { x: f._x, y: f._y, z: f._z };
132
+ }
133
+
134
+ return h(
135
+ MapAreaContainer,
136
+ {
137
+ navbar: h(FloatingNavbar, [
138
+ headerElement ?? h("h2", title),
139
+ h(Spacer),
140
+ h(MapLoadingButton, {
141
+ active: isOpen,
142
+ onClick: () => setOpen(!isOpen),
143
+ }),
144
+ ]),
145
+ contextPanel: h(PanelCard, [
146
+ h(Switch, {
147
+ checked: xRay,
148
+ label: "X-ray mode",
149
+ onChange() {
150
+ setState({ ...state, xRay: !xRay });
151
+ },
152
+ }),
153
+ children,
154
+ ]),
155
+ detailPanel: detailElement,
156
+ contextPanelOpen: isOpen,
157
+ },
158
+ h(
159
+ MapView,
160
+ {
161
+ style: actualStyle,
162
+ transformRequest,
163
+ mapPosition,
164
+ projection: "globe",
165
+ },
166
+ [
167
+ h(FeatureSelectionHandler, {
168
+ selectedLocation: inspectPosition,
169
+ setFeatures: setData,
170
+ }),
171
+ h(MapMarker, {
172
+ position: inspectPosition,
173
+ setPosition: onSelectPosition,
174
+ }),
175
+ h(TileExtentLayer, { tile, color: isEnabled ? "white" : "black" }),
176
+ ]
177
+ )
178
+ );
179
+ }
@@ -0,0 +1,63 @@
1
+ .feature-panel
2
+ position: relative
3
+ overflow-x: hidden
4
+
5
+ .key-value
6
+ display: inline-block
7
+ margin-right: 1em
8
+ .key
9
+ font-weight: bold
10
+ font-size: 0.9em
11
+ &:after
12
+ content: ': '
13
+ .value
14
+ font-size: 0.9em
15
+
16
+ .feature-properties
17
+ position: relative
18
+ &:before
19
+ content: "–"
20
+ position: absolute
21
+ top: 4px
22
+ left: 0
23
+
24
+ .feature-header h3
25
+ margin-bottom: 0
26
+ margin-top: 0.5em
27
+
28
+ .feature-group
29
+ border-bottom: 1px solid var(--panel-rule-inner)
30
+ padding: 0 1em
31
+ margin-left: -1em
32
+ margin-right: -1em
33
+ margin-bottom: 0.5em
34
+ &:last-child
35
+ border-bottom: none
36
+
37
+ .tile-info
38
+ display: flex
39
+ flex-direction: row
40
+ align-items: baseline
41
+ padding: 0 1em
42
+ h3
43
+ margin-right: 0.5em
44
+
45
+ .opacity-slider
46
+ margin: 0 1em 0.5em
47
+ :global
48
+ .bp4-slider-handle .bp4-slider-label
49
+ background-color: var(--secondary-color)
50
+ color: var(--text-color)
51
+
52
+ .unit-number
53
+ .unit
54
+ font-size: 0.9em
55
+ margin-left: 0.2em
56
+ font-weight: bold
57
+
58
+ .page
59
+ margin: 2em auto
60
+ max-width: 50em
61
+
62
+ .dev-index-page
63
+ overflow-y: scroll
@@ -0,0 +1,46 @@
1
+ import { useMapConditionalStyle, useMapRef } from "@macrostrat/mapbox-react";
2
+ import { tileToGeoJSON } from "@mapbox/tilebelt";
3
+ import { useCallback } from "react";
4
+
5
+ type TileIndex = { x: number; y: number; z: number };
6
+
7
+ export function TileExtentLayer({
8
+ tile,
9
+ color = "red",
10
+ }: {
11
+ tile: TileIndex | null;
12
+ color?: string;
13
+ }) {
14
+ const styleCallback = useCallback(
15
+ (map, val: TileIndex) => {
16
+ const style = map.getStyle();
17
+ if (style.layers == null) return;
18
+ style.layers = style.layers.filter((l) => l.id != "tile-extent");
19
+ if (val == null) {
20
+ return map.setStyle(style);
21
+ }
22
+ const { x, y, z } = val;
23
+ const extent = tileToGeoJSON([x, y, z]);
24
+ const source = {
25
+ type: "geojson",
26
+ data: extent,
27
+ };
28
+ const layer = {
29
+ id: "tile-extent",
30
+ type: "line",
31
+ source: "tile-extent",
32
+ paint: {
33
+ "line-color": color,
34
+ "line-width": 2,
35
+ },
36
+ };
37
+ style.sources["tile-extent"] = source;
38
+ style.layers.push(layer);
39
+ map.setStyle(style);
40
+ },
41
+ [color]
42
+ );
43
+ const map = useMapRef();
44
+ useMapConditionalStyle(map, tile, styleCallback);
45
+ return null;
46
+ }