-
Notifications
You must be signed in to change notification settings - Fork 4
Measurements tile #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Measurements tile #348
Changes from 9 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
4867d4c
Side panel works
davidsklar99 0804d34
Clsutering works
davidsklar99 1e1000b
Move map
davidsklar99 58f7f07
Add selected measurements panel
davidsklar99 b563814
Better styling
davidsklar99 c9509b9
Base map finished
davidsklar99 14333c5
Update styling
davidsklar99 714fae3
Use new colors
davidsklar99 0eea7b9
More error handling
davidsklar99 f0a5cd7
Move measurement fcn
davidsklar99 ba518de
Better fetch error handle
davidsklar99 58a6d90
Break apart top rows
davidsklar99 7b6cf28
Use href for links
davidsklar99 0b19e32
Better filtering
davidsklar99 ed53d22
Better filter list
davidsklar99 c574eb8
New page
davidsklar99 df1f3ad
Merge branch 'main' into measurements
davidsklar99 50fc20e
Make lex heirarchy
davidsklar99 d8b1d39
Need to fix filters page
davidsklar99 335c11f
Update econ and environ apge
davidsklar99 3124b01
Use new domain
davidsklar99 e5bd7e6
Better tree styling
davidsklar99 24968d0
Merge branch 'main' into measurements
davenquinn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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]); | ||
| } | ||
| }); | ||
davidsklar99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, [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); }; | ||
davidsklar99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // 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) }), | ||
davidsklar99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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"), | ||
| ]); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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"; | ||
davidsklar99 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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)]), | ||
| }); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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] }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.