Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions pages/lex/measurements/+Page.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
import h from "./main.module.sass";

import {
MapAreaContainer,
MapView,
buildInspectorStyle
} from "@macrostrat/map-interface";
import { mapboxAccessToken } from "@macrostrat-web/settings";
import { LithologyTag } from "@macrostrat/data-components";
import { useEffect, useState } from "react";
import { useDarkMode } from "@macrostrat/ui-components";
import { FullscreenPage } from "~/layouts";
import { MultiSelect } from "@blueprintjs/select"
import { MenuItem, Switch, Divider, Icon } from "@blueprintjs/core";
import { tileserverDomain } from "@macrostrat-web/settings";
import { fetchPGData } from "~/_utils";
import { DataField } from "~/components/unit-details";
import { FlexRow } from "@macrostrat/ui-components";


export function Page() {
return h(FullscreenPage, h(Map))
}

function Map() {
const [selectedTypes, setSelectedTypes] = useState([]);
const [clustered, setClustered] = useState(true);
const [selectedMeasurement, setSelectedMeasurement] = useState(null);

const style = useMapStyle({ selectedTypes, clustered });

if(style == null) return null;

const mapPosition = {
camera: {
lat: 39,
lng: -98,
altitude: 6000000,
},
};

const handleClick = (map, e) => {
const cluster = map.queryRenderedFeatures(e.point, {
layers: ['clusters']
});

if(cluster.length > 0) {
const zoom = cluster[0].properties.expansion_zoom;

map.flyTo({
center: cluster[0].geometry.coordinates,
zoom: zoom + 2,
speed: 10,
curve: .5,
});
}

const features = map.queryRenderedFeatures(e.point, {
layers: ['unclustered-point']
});

if (features.length > 0) {
const properties = features[0].properties;
setSelectedMeasurement(properties.id);
}
};

return h(
"div.map-container",
[
// The Map Area Container
h(
MapAreaContainer,
{
className: 'map-area-container',
contextPanel: h(Panel, { selectedTypes, setSelectedTypes, clustered, setClustered, selectedMeasurement, setSelectedMeasurement }),
key: selectedTypes.join(",") + clustered,
},
[
h(MapView, {
style,
mapboxToken: mapboxAccessToken,
mapPosition,
onMapLoaded: (map) => {
map.on('click', (e) => handleClick(map, e));
}
}),
]
),
]
);
}

function useMapStyle({selectedTypes, clustered}) {
const dark = useDarkMode();
const isEnabled = dark?.isEnabled;

const baseStyle = isEnabled
? "mapbox://styles/mapbox/dark-v10"
: "mapbox://styles/mapbox/light-v10";

const [actualStyle, setActualStyle] = useState(null);

const baseURL = tileserverDomain + "/measurements/tile/{z}/{x}/{y}"
const params = "cluster=" + clustered + (selectedTypes.length > 0 ? "&type=" + selectedTypes.map(encodeURIComponent).join(",") : "");

const url = baseURL + "?" + params;

const baseColor = "#868aa2";
const endColor = "#212435";

const clusteredLayers = [
{
id: "clusters",
type: "circle",
source: "measurements",
"source-layer": "default",
filter: ['>', ['get', 'n'], 1],
paint: {
"circle-radius": [
'step',
['get', 'n'],
7, 50,
9, 100,
11, 150,
13, 200,
15,
],
"circle-color": [
'step',
['get', 'n'],
"#7b7fa0", 50,
'#636b8d', 100,
'#4a546e', 150,
'#353b49', 200,
endColor
],
"circle-stroke-color": [
'step',
['get', 'n'],
"#8b8eab", 50,
'#7a7e96', 100,
'#5d5f7c', 150,
'#484b63',
],
"circle-stroke-width": 3,
"circle-stroke-opacity": 1,
},
},
{
id: 'cluster-count',
type: 'symbol',
source: 'measurements',
"source-layer": "default",
filter: ['has', 'n'],
layout: {
'text-field': ['get', 'n'],
'text-size': 10,
'text-allow-overlap': true,
'text-ignore-placement': true,
},
paint: {
"text-color": "#fff"
},
},
{
id: 'unclustered-point',
type: 'circle',
source: 'measurements',
"source-layer": "default",
filter: ['<=', ['get', 'n'], 1],
paint: {
'circle-color': baseColor,
'circle-radius': 4,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff'
}
},
];

const unclusteredLayers = [
{
id: 'points',
type: 'circle',
source: 'measurements',
"source-layer": "default",
paint: {
'circle-color': "#373ec4",
'circle-radius': 4,
}
},
];

console.log("Using URL: ", url);

const overlayStyle = {
sources: {
measurements: {
type: "vector",
tiles: [ url ],
}
},
layers: clustered ? clusteredLayers : unclusteredLayers,
}

// Auto select sample type
useEffect(() => {
buildInspectorStyle(baseStyle, overlayStyle, {
mapboxToken: mapboxAccessToken,
inDarkMode: isEnabled,
}).then((s) => {
setActualStyle(s);
});
}, [isEnabled, clustered, selectedTypes]);

return actualStyle;
}

function Panel({selectedTypes, setSelectedTypes, clustered, setClustered, selectedMeasurement, setSelectedMeasurement}) {
const types = [
"petrologic",
"environmental",
"stable isotopes",
"minor elements",
"major elements",
"material properties",
"radiogenic isotopes",
"geochronological"
]

const isItemSelected = (item) => selectedTypes.includes(item);

const handleItemSelect = (item) => {
if (!isItemSelected(item)) {
setSelectedTypes([...selectedTypes, item]);
}
};

const handleItemDelete = (itemToDelete) => {
const next = selectedTypes.filter((item) => item !== itemToDelete);
setSelectedTypes(next);
};

const itemPredicate = (query, item) =>
item.toLowerCase().includes(query.toLowerCase());

const itemRenderer = (item, { handleClick, modifiers }) => {
if (!modifiers.matchesPredicate) return null;

return h(MenuItem, {
key: item,
text: item,
onClick: handleClick,
active: modifiers.active,
shouldDismissPopover: false,
});
};

const items = types.filter((f) => !isItemSelected(f))

return h('div.panel', [
h.if(!selectedMeasurement)('div.filter', [
h("h3", "Filter Measurements"),
h(Divider),
h('div.filter-select', [
h(MultiSelect, {
items,
itemRenderer,
itemPredicate,
selectedItems: selectedTypes,
onItemSelect: handleItemSelect,
onRemove: handleItemDelete,
tagRenderer: (item) => item,
popoverProps: { minimal: true },
fill: true,
}),
h(
Switch,
{
checked: clustered,
label: "Clustered",
onChange: () => setClustered(!clustered),
}
),
]),
]),
h.if(selectedMeasurement)(SelectedMeasurement, { selectedMeasurement, setSelectedMeasurement }),
]);
}

function SelectedMeasurement({ selectedMeasurement, setSelectedMeasurement }) {
const [data, setData] = useState(null);

useEffect(() => {
fetchPGData(
"/measurements_with_type",
{
id: "eq." + selectedMeasurement,
}
).then((data) => {
if(selectedMeasurement != null) {
setData(data[0]);
}
});
}, [selectedMeasurement]);

if (selectedMeasurement == null || data == null) {
return null;
}

return h(Measurement, { data, setSelectedMeasurement });
}

export function Measurement({data, setSelectedMeasurement}) {
const { sample_name, sample_geo_unit, sample_lith, lith_id, lith_color, int_name, int_id, int_color, sample_description, ref, type, id } = data;

// Lithology tag component
let lithProps = {
data: { name: sample_lith, color: lith_color }
};

if (lith_id !== 0) {
lithProps.onClick = () => { window.open('/lex/lithologies/' + lith_id); };
}

// Interval tag component
let ageProps = {
data: { name: int_name, color: int_color }
};

if (int_id !== 0) {
ageProps.onClick = () => { window.open('/lex/intervals/' + int_id); };
}

return h("div.selected-measurement", [
h.if(setSelectedMeasurement)(FlexRow, { justifyContent: 'space-between' }, [
h("h3", "Selected Measurement"),
h(Icon, { icon: "cross", className: 'close-btn', onClick: () => setSelectedMeasurement(null) }),
]),
h.if(setSelectedMeasurement)(Divider),
h.if(setSelectedMeasurement)(DataField, { label: "Name", value: h('a.ref', { href: '/lex/measurements/' + id, target: "_blank" }, sample_name) }),
h(DataField, { label: "Type", value: type }),
h(DataField, { label: "Geological Unit", value: sample_geo_unit }),
h.if(sample_lith)(DataField, { label: "Lithology", value: h(LithologyTag, lithProps) }),
h.if(int_id)(DataField, { label: "Age", value: h(LithologyTag, ageProps) }),
h(DataField, { label: "Description", value: sample_description }),
h.if(ref.includes("http"))('a.ref', { href: ref, target: "_blank" }, "View Reference"),
]);
}
25 changes: 25 additions & 0 deletions pages/lex/measurements/@id/+Page.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useData } from "vike-react/useData";
import h from "./main.module.sass";
import { LexItemPage } from "~/components/lex";
import { usePageContext } from "vike-react/usePageContext";
import { DataField } from "~/components/unit-details";
import { LithologyTag } from "@macrostrat/data-components";
import { fetchAPIData } from "~/_utils";
import { useEffect, useState } from "react";
import { Measurement } from "../+Page.client";

export function Page() {
const { resData } = useData();

const id = usePageContext().urlParsed.pathname.split("/")[3];

const children = [h(Measurement, { data: resData })];

return LexItemPage({
children,
id,
resData,
siftLink: "measurements",
header: h("div.strat-header", [h("h1.strat-title", resData?.sample_name)]),
});
}
13 changes: 13 additions & 0 deletions pages/lex/measurements/@id/+data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { pbdbDomain } from "@macrostrat-web/settings";
import { fetchAPIData, fetchAPIRefs, fetchPGData } from "~/_utils";

export async function data(pageContext) {
const id = parseInt(pageContext.urlParsed.pathname.split("/")[3]);

// Await all API calls
const [resData] = await Promise.all([
fetchPGData("/measurements_with_type", { id: "eq." + id }),
]);

return { resData: resData[0] };
}
Loading