@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,208 @@
1
+ import { Spinner, Switch } from "@blueprintjs/core";
2
+ import { useMapRef, useMapStatus } from "@macrostrat/mapbox-react";
3
+ import mapboxgl from "mapbox-gl";
4
+ import hyper from "@macrostrat/hyper";
5
+ import styles from "./main.module.sass";
6
+ import { useEffect, useState, useRef } from "react";
7
+ import { JSONView } from "@macrostrat/ui-components";
8
+ import { group } from "d3-array";
9
+ import { ExpansionPanel } from "../expansion-panel";
10
+
11
+ const h = hyper.styled(styles);
12
+
13
+ function usePrevious(value) {
14
+ const ref = useRef();
15
+ useEffect(() => {
16
+ ref.current = value;
17
+ });
18
+ return ref.current;
19
+ }
20
+
21
+ export function FeatureRecord({ feature }) {
22
+ const props = feature.properties;
23
+ return h("div.feature-record", [
24
+ h.if(Object.keys(props).length > 0)("div.feature-properties", [
25
+ h(JSONView, {
26
+ data: props,
27
+ hideRoot: true,
28
+ }),
29
+ ]),
30
+ ]);
31
+ }
32
+
33
+ export function FeatureSelectionHandler({
34
+ selectedLocation,
35
+ setFeatures,
36
+ radius = 2,
37
+ }: {
38
+ selectedLocation: mapboxgl.LngLat;
39
+ setFeatures: (features: mapboxgl.MapboxGeoJSONFeature[]) => void;
40
+ radius?: number;
41
+ }) {
42
+ const mapRef = useMapRef();
43
+ const { isLoading } = useMapStatus();
44
+ const prevLocation = usePrevious(selectedLocation);
45
+
46
+ useEffect(() => {
47
+ const map = mapRef?.current;
48
+ if (map == null) return;
49
+ if (selectedLocation == null) {
50
+ setFeatures(null);
51
+ return;
52
+ }
53
+
54
+ // Don't update if the location hasn't changed
55
+ if (selectedLocation == prevLocation) return;
56
+
57
+ const r = radius;
58
+ const pt = map.project(selectedLocation);
59
+
60
+ const bbox: [mapboxgl.PointLike, mapboxgl.PointLike] = [
61
+ [pt.x - r, pt.y - r],
62
+ [pt.x + r, pt.y + r],
63
+ ];
64
+ const features = map.queryRenderedFeatures(bbox);
65
+ setFeatures(features);
66
+ }, [mapRef?.current, selectedLocation, isLoading]);
67
+
68
+ return null;
69
+ }
70
+
71
+ function FeatureHeader({ feature }) {
72
+ return h("div.feature-header", [
73
+ h("h3", [
74
+ h(KeyValue, { label: "Source", value: feature.source }),
75
+ h(KeyValue, { label: "Source layer", value: feature.sourceLayer }),
76
+ ]),
77
+ ]);
78
+ }
79
+
80
+ function KeyValue({ label, value }) {
81
+ return h("span.key-value", [h("span.key", label), h("code.value", value)]);
82
+ }
83
+
84
+ function LoadingAwareFeatureSet({ features, sourceID }) {
85
+ const map = useMapRef();
86
+ if (map?.current == null) return null;
87
+ const [isLoaded, setIsLoaded] = useState(false);
88
+
89
+ const sourceFeatures = features.filter((d) => d.source == "burwell");
90
+
91
+ useEffect(() => {
92
+ if (sourceFeatures.length > 0) {
93
+ setIsLoaded(true);
94
+ return;
95
+ }
96
+
97
+ const isLoaded = map.current.isSourceLoaded(sourceID);
98
+ setIsLoaded(isLoaded);
99
+ if (!isLoaded) {
100
+ map.current.once("sourcedata", (e) => {
101
+ if (e.sourceId == sourceID) {
102
+ setIsLoaded(true);
103
+ }
104
+ });
105
+ }
106
+ }, [map.current, sourceID, sourceFeatures.length]);
107
+
108
+ if (!isLoaded) return h(Spinner);
109
+ return h(Features, { features: sourceFeatures });
110
+ }
111
+
112
+ export function TileInfo({ feature, showExtent, setShowExtent }) {
113
+ if (feature == null) return null;
114
+ const size = feature._vectorTileFeature._pbf.length;
115
+ return h("div.tile-info", [
116
+ h("h3", "Tile"),
117
+ h("div.tile-index", [
118
+ h(KeyValue, { label: "x", value: feature._x }),
119
+ h(KeyValue, { label: "y", value: feature._y }),
120
+ h(KeyValue, { label: "z", value: feature._z }),
121
+ ]),
122
+ h("div.spacer"),
123
+ h(KeyValue, { label: "Size", value: formatSize(size) }),
124
+ h(Switch, {
125
+ label: "Show extent",
126
+ alignIndicator: "right",
127
+ checked: showExtent,
128
+ onChange() {
129
+ setShowExtent(!showExtent);
130
+ },
131
+ }),
132
+ ]);
133
+ }
134
+
135
+ function formatSize(size: number) {
136
+ if (size > 1000000)
137
+ return h(UnitNumber, { value: size / 1000000, unit: "Mb" });
138
+ if (size > 1000) return h(UnitNumber, { value: size / 1000, unit: "Kb" });
139
+ return `${size} bytes`;
140
+ }
141
+
142
+ function UnitNumber({ value, unit, precision = 1 }) {
143
+ return h("span.unit-number", [
144
+ h("span.number", value.toFixed(precision)),
145
+ h("span.unit", unit),
146
+ ]);
147
+ }
148
+
149
+ export function FeaturePanel({
150
+ features,
151
+ focusedSource = null,
152
+ focusedSourceTitle = null,
153
+ }) {
154
+ if (features == null) return null;
155
+
156
+ let focusedSourcePanel = null;
157
+ let filteredFeatures = features;
158
+ let title = "Features";
159
+
160
+ if (focusedSource != null) {
161
+ title = "Basemap features";
162
+ focusedSourcePanel = h(
163
+ ExpansionPanel,
164
+ {
165
+ title: "Macrostrat features",
166
+ className: "macrostrat-features",
167
+ expanded: true,
168
+ },
169
+ [
170
+ h(LoadingAwareFeatureSet, {
171
+ features,
172
+ sourceID: focusedSource,
173
+ }),
174
+ ]
175
+ );
176
+ filteredFeatures = features.filter((d) => d.source != focusedSource);
177
+ }
178
+
179
+ return h("div.feature-panel", [
180
+ focusedSourcePanel,
181
+ h(
182
+ ExpansionPanel,
183
+ { title, className: "basemap-features", expanded: focusedSource == null },
184
+ [
185
+ h(Features, {
186
+ features: filteredFeatures,
187
+ }),
188
+ ]
189
+ ),
190
+ ]);
191
+ }
192
+
193
+ function Features({ features }) {
194
+ /** Group features by source and sourceLayer */
195
+ if (features == null) return null;
196
+
197
+ const groups = group(features, (d) => `${d.source} - ${d.sourceLayer}`);
198
+
199
+ return h(
200
+ "div.features",
201
+ Array.from(groups).map(([key, features]) => {
202
+ return h("div.feature-group", [
203
+ h(FeatureHeader, { feature: features[0] }),
204
+ features.map((feature, i) => h(FeatureRecord, { key: i, feature })),
205
+ ]);
206
+ })
207
+ );
208
+ }
@@ -0,0 +1,118 @@
1
+ import { getMapboxStyle, mergeStyles } from "@macrostrat/mapbox-utils";
2
+ import chroma from "chroma-js";
3
+ import mapboxgl from "mapbox-gl";
4
+
5
+ interface XRayOptions {
6
+ color?: string;
7
+ inDarkMode?: boolean;
8
+ mapboxToken?: string;
9
+ xRaySources?: string[];
10
+ }
11
+
12
+ export async function buildXRayStyle(
13
+ baseStyle: string | object,
14
+ params: XRayOptions = null
15
+ ) {
16
+ const {
17
+ inDarkMode = false,
18
+ color = "rgb(74, 242, 161)",
19
+ mapboxToken,
20
+ xRaySources
21
+ } = params;
22
+ const style = await getMapboxStyle(baseStyle, { access_token: mapboxToken });
23
+ const sources = xRaySources ?? Object.keys(style.sources);
24
+
25
+ let layers = [];
26
+ for (let layer of style.layers) {
27
+ if (!sources.includes(layer.source)) {
28
+ layers.push(layer);
29
+ continue;
30
+ }
31
+ let newLayer = transformMapboxLayer(layer, color, inDarkMode);
32
+ if (newLayer != null) {
33
+ layers.push(newLayer);
34
+ }
35
+ }
36
+
37
+ return {
38
+ ...style,
39
+ layers,
40
+ };
41
+ }
42
+
43
+ function transformMapboxLayer(layer, color, inDarkMode) {
44
+ const c = chroma(color);
45
+ const xRayColor = (opacity = 1, darken = 0) => {
46
+ if (!inDarkMode) {
47
+ return chroma(color)
48
+ .darken(2 - darken)
49
+ .alpha(opacity)
50
+ .css();
51
+ }
52
+ return c.alpha(opacity).darken(darken).css();
53
+ };
54
+
55
+ if (layer.type == "background") {
56
+ return null;
57
+ }
58
+
59
+ let newLayer = { ...layer };
60
+
61
+ if (layer.type == "fill") {
62
+ newLayer.paint = {
63
+ "fill-color": xRayColor(0.1),
64
+ "fill-outline-color": xRayColor(0.5),
65
+ };
66
+ } else if (layer.type == "line") {
67
+ newLayer.paint = {
68
+ "line-color": xRayColor(0.5, 0),
69
+ "line-width": 1.5,
70
+ };
71
+ } else if (layer.type == "symbol") {
72
+ newLayer.paint = {
73
+ "text-color": xRayColor(1, -0.5),
74
+ "text-halo-color": "#000",
75
+ };
76
+ } else if (layer.type == "circle") {
77
+ newLayer.paint = {
78
+ "circle-color": xRayColor(0.5, 0),
79
+ "circle-stroke-color": xRayColor(0.5, 1),
80
+ "circle-radius": 2,
81
+ };
82
+ }
83
+
84
+ return newLayer;
85
+ }
86
+
87
+ type InspectorStyleOptions = XRayOptions & {
88
+ xRay?: boolean;
89
+ };
90
+
91
+ export async function buildInspectorStyle(
92
+ baseStyle: mapboxgl.Style | string,
93
+ overlayStyle: mapboxgl.Style | string | null = null,
94
+ params: InspectorStyleOptions = {}
95
+ ) {
96
+ const { mapboxToken, xRay = false, xRaySources: _xRaySources, ...rest } = params;
97
+ let xRaySources = _xRaySources;
98
+ let style = await getMapboxStyle(baseStyle, {
99
+ access_token: mapboxToken,
100
+ });
101
+
102
+ if (overlayStyle != null) {
103
+ const overlay = await getMapboxStyle(overlayStyle, {
104
+ access_token: mapboxToken,
105
+ });
106
+ style = mergeStyles(style, overlay);
107
+ xRaySources ??= Object.keys(overlay.sources);
108
+ }
109
+
110
+
111
+ if (xRay) {
112
+ // If we haven't specified sources, then we'll use all of them
113
+ xRaySources ??= Object.keys(style.sources);
114
+
115
+ style = await buildXRayStyle(style, { ...rest, mapboxToken, xRaySources });
116
+ }
117
+ return style;
118
+ }
@@ -0,0 +1,18 @@
1
+ import hyper from "@macrostrat/hyper";
2
+ import styles from "./main.module.sass";
3
+
4
+ const h = hyper.styled(styles);
5
+
6
+ export function PanelSubhead(props) {
7
+ const { title, component = "h3", children, ...rest } = props;
8
+ return h("div.panel-subhead", rest, [
9
+ h(
10
+ component,
11
+ {
12
+ className: "title",
13
+ },
14
+ title
15
+ ),
16
+ children,
17
+ ]);
18
+ }
@@ -0,0 +1,134 @@
1
+ import { useState } from "react";
2
+ import { Collapse, Icon } from "@blueprintjs/core";
3
+ import hyper from "@macrostrat/hyper";
4
+ import styles from "./main.module.sass";
5
+ import classNames from "classnames";
6
+ import { Button } from "@blueprintjs/core";
7
+ import { PanelSubhead } from "./headers";
8
+
9
+ const h = hyper.styled(styles);
10
+
11
+ function ExpansionPanelSummary(props) {
12
+ const { expanded, children, onChange, className, title, titleComponent } =
13
+ props;
14
+ const icon = expanded ? "chevron-up" : "chevron-down";
15
+ return h(
16
+ PanelSubhead,
17
+ {
18
+ className: classNames("expansion-panel-header", className),
19
+ onClick: onChange,
20
+ title,
21
+ component: titleComponent,
22
+ },
23
+ [children, h(Icon, { icon })]
24
+ );
25
+ }
26
+
27
+ function ExpansionPanelBase(props) {
28
+ let {
29
+ title,
30
+ titleComponent = "h3",
31
+ children,
32
+ expanded,
33
+ helpText,
34
+ onChange = () => {},
35
+ sideComponent = null,
36
+ className,
37
+ } = props;
38
+ const [isOpen, setOpen] = useState(expanded || false);
39
+
40
+ const onChange_ = () => {
41
+ onChange();
42
+ setOpen(!isOpen);
43
+ };
44
+
45
+ return h(
46
+ "div.expansion-panel-base",
47
+ {
48
+ className: classNames(className, {
49
+ expanded: isOpen,
50
+ collapsed: !isOpen,
51
+ }),
52
+ },
53
+ [
54
+ h(
55
+ ExpansionPanelSummary,
56
+ {
57
+ onChange: onChange_,
58
+ expanded: isOpen,
59
+ title,
60
+ titleComponent,
61
+ },
62
+ h("div.expansion-summary-title-help", [
63
+ h("span.expansion-panel-subtext", helpText),
64
+ " ",
65
+ sideComponent,
66
+ ])
67
+ ),
68
+ h(Collapse, { isOpen }, h("div.expansion-children", null, children)),
69
+ ]
70
+ );
71
+ }
72
+
73
+ export function InfoPanelSection(props) {
74
+ let { title, children, className, headerElement = null } = props;
75
+ return h("div.info-panel-section", { className }, [
76
+ h("div.panel-subhead", null, headerElement ?? h("h3", title)),
77
+ h("div.panel-content", null, children),
78
+ ]);
79
+ }
80
+
81
+ function ExpansionPanel(props) {
82
+ return h(ExpansionPanelBase, {
83
+ ...props,
84
+ className: "expansion-panel",
85
+ });
86
+ }
87
+
88
+ function SubExpansionPanel(props) {
89
+ return h(ExpansionPanelBase, {
90
+ ...props,
91
+ className: "expansion-panel sub-expansion-panel",
92
+ titleComponent: "h4",
93
+ });
94
+ }
95
+
96
+ function ExpandableDetailsPanel(props) {
97
+ let { title, children, value, headerElement, className } = props;
98
+ const [isOpen, setIsOpen] = useState(false);
99
+ headerElement ??= h([h("div.title", title), value]);
100
+ return h("div.expandable-details", { className }, [
101
+ h("div.expandable-details-main", [
102
+ h("div.expandable-details-header", headerElement),
103
+ h("div.expandable-details-toggle", [
104
+ h(Button, {
105
+ small: true,
106
+ minimal: true,
107
+ active: isOpen,
108
+ onClick: () => setIsOpen(!isOpen),
109
+ icon: "more",
110
+ }),
111
+ ]),
112
+ ]),
113
+ h(
114
+ Collapse,
115
+ { isOpen },
116
+ h("div.expandable-details-children", null, children)
117
+ ),
118
+ ]);
119
+ }
120
+
121
+ function ExpansionBody({ title, className, children }) {
122
+ return h("div.expansion-body", { className }, [
123
+ h("div.expansion-panel-detail-header", title),
124
+ h("div.expansion-panel-detail-body", null, children),
125
+ ]);
126
+ }
127
+
128
+ export {
129
+ ExpansionPanel,
130
+ ExpansionPanelSummary,
131
+ ExpandableDetailsPanel,
132
+ SubExpansionPanel,
133
+ ExpansionBody,
134
+ };
@@ -0,0 +1,143 @@
1
+
2
+ .panel-subhead
3
+ padding: 0.2em var(--box-horizontal-padding)
4
+ border-top: 1px solid var(--accent-border-color)
5
+ border-bottom: 1px solid var(--accent-border-color)
6
+ background-color: var(--accent-color)
7
+ display: flex
8
+ flex-direction: row
9
+ align-items: center
10
+ z-index: 1
11
+ gap: var(--box-horizontal-padding)
12
+ top: 0px
13
+ position: sticky
14
+ h1, h2, h3, h4
15
+ font-family: Montserrat,sans-serif
16
+ font-weight: 700
17
+ margin: 0.2em 0
18
+ h4
19
+ font-weight: 600
20
+ .title
21
+ flex-grow: 1
22
+
23
+ // :global(.bp4-dark) .panel-subhead
24
+ // margin 0 1px
25
+
26
+ .expansion-panel
27
+ padding: 0
28
+ flex-wrap: wrap
29
+ margin-top: -1px
30
+ // &.collapsed
31
+ // .expansion-panel-header
32
+ // border-bottom-width: 0;
33
+
34
+ .sub-expansion-panel
35
+ margin: -1px calc(var(--panel-padding-h) * -0.5) 0
36
+ overflow: hidden
37
+ &:first-child
38
+ .expansion-panel-header
39
+ border-top-width: 0
40
+ .panel-subhead
41
+ border-top: none
42
+ border-bottom: none
43
+
44
+ .expansion-panel-header
45
+ background-color: var(--accent-secondary-color)
46
+ cursor: pointer
47
+ &:hover
48
+ background-color: var(--accent-secondary-hover-color)
49
+ h2, h3, h4
50
+ font-weight: 500
51
+ border-bottom: 1px solid var(--tertiary-border-color)
52
+ border-top: 1px solid var(--tertiary-border-color)
53
+ margin-top: -1px
54
+ padding: 5px 1em 5px
55
+ align-items: center
56
+
57
+ .expansion-summary-title-help
58
+ margin-left: 5px
59
+ :global(.bp4-icon)
60
+ margin-left: 5px
61
+
62
+ .expansion-panel-header
63
+ cursor: pointer
64
+ &:hover
65
+ background-color: var(--accent-hover-color)
66
+ :global(.bp4-icon)
67
+ transform: translate(0,3px)
68
+
69
+ .expansion-children
70
+ padding: 5px 1em 10px
71
+ .expansion-panel
72
+ margin-left: -1em
73
+ margin-right: -1em
74
+ &:first-child
75
+ margin-top: -5px
76
+
77
+ .expansion-panel-subtext
78
+ font-size: 85%
79
+ font-weight: 400
80
+
81
+ :global
82
+ .expansion-panel-root
83
+ padding-left: 15px !important
84
+
85
+ .expansion-panel-detail
86
+ display: block !important
87
+ padding: 0 !important
88
+
89
+ .expansion-panel-detail-sub
90
+ display: block !important
91
+
92
+ // New expandable panel for details
93
+ .expandable-details-main
94
+ display: flex
95
+ flex-direction: row
96
+ align-items: center
97
+ justify-content: space-between
98
+ margin: 3px 0
99
+
100
+ .expandable-details-header
101
+ display: inline-flex
102
+ flex-direction: row
103
+ align-items: baseline
104
+ flex-grow: 1
105
+
106
+ .expandable-details-children
107
+ position: relative
108
+
109
+ .expandable-details-toggle
110
+ :global(.bp4-button)
111
+ font-size: 10px
112
+
113
+ .expandable-details
114
+ &.macrostrat-unit
115
+ .title:after
116
+ content: none
117
+ .title
118
+ margin-right: 1em
119
+ &:after
120
+ content: ":"
121
+
122
+ .expansion-body
123
+ display: inline-block
124
+ //flex-direction row
125
+ align-items: baseline
126
+ background-color: var(--tertiary-background)
127
+ padding: 2px 6px
128
+ border-radius: 4px
129
+ width: 100%
130
+ box-shadow: 0px 1px 2px 1px rgba(0,0,0,0.2)
131
+ margin-bottom: 6px
132
+
133
+ .expansion-panel-detail-header
134
+ font-size: 90%
135
+ font-style: italic
136
+ margin-right: 1em
137
+ display: inline
138
+ color: var(--secondary-color)
139
+ &:after
140
+ content: ":"
141
+
142
+ .expansion-panel-detail-body
143
+ display: inline