diff --git a/pages/dev/map/outcrop/+Page.client.ts b/pages/dev/map/outcrop/+Page.client.ts new file mode 100644 index 000000000..93ff77215 --- /dev/null +++ b/pages/dev/map/outcrop/+Page.client.ts @@ -0,0 +1,65 @@ +import h from "@macrostrat/hyper"; + +import { + MapAreaContainer, + MapView, + buildInspectorStyle +} from "@macrostrat/map-interface"; +import { mapboxAccessToken } from "@macrostrat-web/settings"; +import { useEffect, useState } from "react"; +import { useDarkMode, FlexRow } 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 { fetchAPIData, fetchPGData } from "~/_utils"; +import { Measurement } from "./measurement"; +import { usePageContext } from "vike-react/usePageContext"; +import { Loading } from "~/components"; +import { buildMacrostratStyle } from "@macrostrat/map-styles"; + +export function Page() { + return h(FullscreenPage, h(Map)) +} + +function Map() { + + + + const style = buildMacrostratStyle({ + tileserverDomain, + fillOpacity: 0.3, + strokeOpacity: 0.1, + }) as mapboxgl.Style; + + if(style == null) return null; + + const mapPosition = { + camera: { + lat: 39, + lng: -98, + altitude: 6000000, + }, + }; + + + return h( + "div.map-container", + [ + // The Map Area Container + h( + MapAreaContainer, + { + className: 'map-area-container', + }, + [ + h(MapView, { + style, + mapboxToken: mapboxAccessToken, + mapPosition, + }), + ] + ), + ] + ); +} \ No newline at end of file diff --git a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts index 929e72382..a2b525c6a 100644 --- a/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts +++ b/pages/integrations/xdd/feedback/@sourceTextID/+Page.client.ts @@ -194,7 +194,7 @@ function FeedbackInterface({ data, models, entityTypes, autoSelect, customFeedba concept: "/lex/strat-concepts", }, lineHeight: 3, - // view: user === null, + // view: user === null, TODO: Enable view mode for non-logged in users autoSelect, onSave: wrapWithToaster( async (tree) => { diff --git a/pages/lex/legends/+Page.client.ts b/pages/lex/legends/+Page.client.ts new file mode 100644 index 000000000..f1fdf7ba4 --- /dev/null +++ b/pages/lex/legends/+Page.client.ts @@ -0,0 +1,111 @@ +import h from "./main.module.sass"; +import { StickyHeader, LinkCard, PageBreadcrumbs, Footer, BetaTag } from "~/components"; +import { ContentPage } from "~/layouts"; +import { usePageContext } from "vike-react/usePageContext"; +import { PostgRESTInfiniteScrollView } from "@macrostrat/ui-components"; +import { postgrestPrefix, apiDomain } from "@macrostrat-web/settings"; +import { LithologyTag, FlexRow, ExpansionPanel } from "~/components/lex/tag"; + +export function Page() { + const url = usePageContext().urlOriginal.split("?")[1]; + + if (!url) { + return h(Base); + } + + const params = getUrlParams(url); + const idType = params.idType; + const id = params[idType]; + const color = params.color; + const name = params.name; + + + return h(ContentPage, [ + h(Header, { name, color, idType, id }), + h(FilterData), + ]); +} + +function Header({ name, color, idType, id }) { + const map = { + 'int_id': "intervals", + 'lith_id': "lithologies", + 'econ_id': "economics", + 'environ_id': "environments", + 'strat_name_id': "strat-names", + } + + return h(StickyHeader, { className: "header" }, [ + h(PageBreadcrumbs, { + title: h(FlexRow, { gap: ".5em", alignItems: "center" }, [ + h('p.title', 'Legends for '), + h(LithologyTag, { data: { name, color }, href: `/lex/${map[idType]}/${id}` }), + ]), + }), + h(BetaTag) + ]); +} + +function getUrlParams(urlString) { + const params = new URLSearchParams(urlString); + const result = {}; + + for (const [key, value] of params.entries()) { + result[key] = value; + + if (key.toLowerCase().includes('id')) { + result.idType = key; + } + } + + return result; +} + +function Base() { + return h(ContentPage, { className: 'page' }, [ + h(StickyHeader, { className: "header" }, h(PageBreadcrumbs, { title: "Legends" })), + h(PostgRESTInfiniteScrollView, { + route: postgrestPrefix + '/legend_liths', + id_key: 'legend_id', + limit: 20, + itemComponent: LegendItem, + filterable: true, + searchColumns: [{value: "map_unit_name", label: "Map unit name"}], + }), + ]); +} + +function BaseUnitItem({ data }) { + const { id, col_id, strat_name } = data; + + return h(LinkCard, { + href: `/columns/${col_id}#unit=${id}`, + title: strat_name, + }) +} + +function FilterData() { + const params = usePageContext().urlParsed.href.split("?")[1].split("=") + const id = params[1].split("&")[0] + + return h(PostgRESTInfiniteScrollView, { + route: postgrestPrefix + `/legend_liths`, + id_key: "legend_id", + limit: 20, + extraParams: { + lith_ids: `cs.{${id}}`, + }, + filterable: true, + searchColumns: [{value: "map_unit_name", label: "Map unit name"}], + itemComponent: LegendItem, + }); +} + +function LegendItem({ data }) { + const { map_unit_name, legend_id, source_id } = data; + + return h(LinkCard, { + href: `/maps/${source_id}?legend_id=${legend_id}`, + title: h("div.title", map_unit_name), + }); +} \ No newline at end of file diff --git a/pages/lex/legends/@id/+Page.client.ts b/pages/lex/legends/@id/+Page.client.ts new file mode 100644 index 000000000..8eebca108 --- /dev/null +++ b/pages/lex/legends/@id/+Page.client.ts @@ -0,0 +1,13 @@ +import h from "@macrostrat/hyper"; +import { usePageContext } from "vike-react/usePageContext"; +import { fetchAPIData } from "~/_utils"; +import { navigate } from "vike/client/router"; + +export function Page() { + const unit_id = usePageContext()?.urlPathname.split("/")?.[3] || []; + + fetchAPIData("/units", { unit_id }) + .then(data => navigate(`/columns/${data[0].col_id}#unit=${unit_id}`)) + + return null +} diff --git a/pages/lex/legends/main.module.sass b/pages/lex/legends/main.module.sass new file mode 100644 index 000000000..6a16a1f68 --- /dev/null +++ b/pages/lex/legends/main.module.sass @@ -0,0 +1,23 @@ +.title + margin: .5em 0 + +.page + display: flex + flex-direction: column + gap: 1em + +.header + display: flex + justify-content: space-between + +.units + margin: 1em 0 + +.unit-title + display: flex + justify-content: space-between + align-items: center + +.count + margin: 0 + font-size: 15px \ No newline at end of file diff --git a/pages/lex/lithologies/@id/+Page.client.ts b/pages/lex/lithologies/@id/+Page.client.ts index dc31c8381..a7730d6b0 100644 --- a/pages/lex/lithologies/@id/+Page.client.ts +++ b/pages/lex/lithologies/@id/+Page.client.ts @@ -32,10 +32,10 @@ export function Page() { h(Charts, { features }), h(PrevalentTaxa, { taxaData }), h(Timescales, { timescales }), - h(Maps, { mapsData }), h(TextExtractions, { lith_id: id, href: "autoselect=" + resData?.name + "&lith_id=" + id + "&color=" + resData?.color }), - h.if(fossilsData.features.length > 0)(Fossils, { href: "lith_id=" + id + "&color=" + resData?.color + "&name=" + resData?.name }), - h.if(unitsData.length > 0)(Units, { href: "lith_id=" + id + "&color=" + resData?.color + "&name=" + resData?.name }), + h.if(unitsData?.length > 0)(Units, { href: "lith_id=" + id + "&color=" + resData?.color + "&name=" + resData?.name }), + h.if(mapsData?.length > 0)(Maps, { href: "lith_id=" + id + "&color=" + resData?.color + "&name=" + resData?.name }), + h.if(fossilsData?.features.length > 0)(Fossils, { href: "lith_id=" + id + "&color=" + resData?.color + "&name=" + resData?.name }), ]; return LexItemPage({ children, id, refs, resData, siftLink: "lithology" }); diff --git a/pages/lex/lithologies/@id/+data.ts b/pages/lex/lithologies/@id/+data.ts index 514578b4a..a7c22762e 100644 --- a/pages/lex/lithologies/@id/+data.ts +++ b/pages/lex/lithologies/@id/+data.ts @@ -31,7 +31,7 @@ export async function data(pageContext) { }), "colData" ), - safeFetch(() => fetchAPIData("/geologic_units/map/legend", { lith_id }), "mapsData"), + safeFetch(() => fetchAPIData("/geologic_units/map/legend", { lith_id, sample: "true" }), "mapsData"), safeFetch(() => fetchAPIData("/fossils", { lith_id, format: "geojson" }), "fossilsData"), safeFetch(() => fetchAPIRefs("/fossils", { lith_id }), "refs1"), safeFetch(() => fetchAPIRefs("/columns", { lith_id }), "refs2"), diff --git a/pages/lex/strat-names/@id/+Page.client.ts b/pages/lex/strat-names/@id/+Page.client.ts index 450a2ca35..944bfd723 100644 --- a/pages/lex/strat-names/@id/+Page.client.ts +++ b/pages/lex/strat-names/@id/+Page.client.ts @@ -35,13 +35,13 @@ export function Page() { h(Charts, { features }), h(PrevalentTaxa, { taxaData }), h(Timescales, { timescales }), - h.if(unitsData.length > 0)(Units, { href: "strat_name_id=" + id + "&name=" + resData?.strat_name }), - h.if(fossilsData.features.length > 0)(Fossils, { href: "strat_name_id=" + id + "&name=" + resData?.name }), - h(Maps, { mapsData }), h(TextExtractions, { strat_name_id: id, href: "autoselect=" + resData?.strat_name_long + "&strat_name_id=" + id, }), + h.if(unitsData.length > 0)(Units, { href: "strat_name_id=" + id + "&name=" + resData?.strat_name }), + // h.if(mapsData?.length > 0)(Maps, { href: "strat_name_id=" + id + "&name=" + resData?.name }), (add strat names to legends view first) + h.if(fossilsData.features.length > 0)(Fossils, { href: "strat_name_id=" + id + "&name=" + resData?.name }), h(StratNameHierarchy, { id }), h(ConceptInfo, { concept_id: resData?.concept_id, showHeader: true }), ]; diff --git a/pages/lex/strat-names/@id/+data.ts b/pages/lex/strat-names/@id/+data.ts index f9e01e008..f689c1ee4 100644 --- a/pages/lex/strat-names/@id/+data.ts +++ b/pages/lex/strat-names/@id/+data.ts @@ -14,7 +14,7 @@ export async function data(pageContext) { format: "geojson", }), fetchAPIData("/fossils", { strat_name_id, format: "geojson" }), - fetchAPIData("/geologic_units/map/legend", { strat_name_id }), + fetchAPIData("/geologic_units/map/legend", { strat_name_id, sample: "true" }), fetchAPIRefs("/fossils", { strat_name_id }), fetchAPIRefs("/columns", { strat_name_id }), fetchAPIData("/units", { strat_name_id }), diff --git a/pages/maps/+Page.ts b/pages/maps/+Page.client.ts similarity index 99% rename from pages/maps/+Page.ts rename to pages/maps/+Page.client.ts index 46114c5df..f93b62ea6 100644 --- a/pages/maps/+Page.ts +++ b/pages/maps/+Page.client.ts @@ -119,4 +119,4 @@ function SourceItem({ data }) { ]), ] ); -} +} \ No newline at end of file diff --git a/pages/maps/main.module.sass b/pages/maps/main.module.sass index 55aa84de9..f8cd27ff2 100644 --- a/pages/maps/main.module.sass +++ b/pages/maps/main.module.sass @@ -186,3 +186,7 @@ h3 .assistant-links flex: 1 + +.top-row + display: flex + gap: .5em \ No newline at end of file diff --git a/src/components/lex/filter-helper.ts b/src/components/lex/filter-helper.ts new file mode 100644 index 000000000..dfa116b55 --- /dev/null +++ b/src/components/lex/filter-helper.ts @@ -0,0 +1,189 @@ +import { SETTINGS } from "@macrostrat-web/settings"; +import { FeatureCollection, Point } from "geojson"; +import { + FilterData, + IntervalFilterData, +} from "#/map/map-interface/app-state/handlers/filters"; + +export function getExpressionForFilters( + filters: FilterData[] +): mapboxgl.Expression { + // Separate time filters and other filters for different rules + // i.e. time filters are OR and all others are AND + // Keep track of name: index values of time filters for easier removing + let expr: mapboxgl.Expression = ["all", ["!=", "color", ""]]; + + const timeFilters = filters + .filter((f) => f.type === "intervals") + .map(buildFilterExpression); + if (timeFilters.length > 0) { + expr.push(["any", ...timeFilters]); + } + + const otherFilters = filters + .filter((f) => f.type !== "intervals") + .map(buildFilterExpression); + if (otherFilters.length > 0) { + expr.push(["any", ...otherFilters]); + } + return expr; +} + +function buildFilterClasses( + type: string, + name: string | number +): mapboxgl.Expression { + /* This function implements filtering over numbered classes. + It is used to provide filtering over the complex structure created for + MVT tiles of the 'carto' style. */ + let filter: mapboxgl.Expression = ["any"]; + for (let i = 1; i < 14; i++) { + filter.push(["==", `${type}${i}`, name]); + } + return filter; +} + +function buildFilterExpression(filter: FilterData): mapboxgl.Expression { + // Check which kind of filter it is + switch (filter.type) { + case "intervals": + // These should be added to the timeFilters array + // Everything else goes in normal filters + return [ + "all", + [">", "best_age_bottom", filter.t_age], + ["<", "best_age_top", filter.b_age], + ]; + case "lithology_classes": + return buildFilterClasses("lith_class", filter.name ?? filter.id); + case "lithology_types": + return buildFilterClasses("lith_type", filter.name ?? filter.id); + case "lithologies": + case "all_lithologies": + case "all_lithology_types": + case "all_lithology_classes": + return ["in", "legend_id", ...filter.legend_ids]; + case "strat_name_orphans": + case "strat_name_concepts": + return ["in", "legend_id", ...filter.legend_ids]; + } +} + +export async function getPBDBData( + filters: FilterData[], + bounds: mapboxgl.LngLatBounds, + zoom: number, + maxClusterZoom: number = 7 +): Promise> { + // One for time, one for everything else because + // time filters require a separate request for each filter + let timeQuery = []; + let queryString = []; + + const timeFilters = filters.filter( + (f) => f.type === "intervals" + ) as IntervalFilterData[]; + const stratNameFilters = filters.filter( + (f) => f.type === "strat_name_concepts" || f.type === "strat_name_orphans" + ); + + if (timeFilters.length > 0) { + for (const f of timeFilters) { + timeQuery.push(`max_ma=${f.b_age}`, `min_ma=${f.t_age}`); + } + } + // lith filters broken on pbdb (500 error returned) + // if (map.lithFilters.length) { + // let filters = map.lithFilters.filter((f) => f != "sedimentary"); + // if (filters.length) { + // queryString.push(`lithology=${filters.join(",")}`); + // } + // } + if (stratNameFilters.length > 0) { + const names = stratNameFilters.map((f) => f.name); + queryString.push(`strat=${names.join(",")}`); + } + + // Define the pbdb cluster level + let level = zoom < 3 ? "&level=2" : "&level=3"; + + let urls = []; + // Make sure lngs are between -180 and 180 + const lngMin = bounds._sw.lng < -180 ? -180 : bounds._sw.lng; + const lngMax = bounds._ne.lng > 180 ? 180 : bounds._ne.lng; + // If more than one time filter is present, multiple requests are needed + + /* Currently there is a limitation in the globe for the getBounds function that + resolves incorrect latitude ranges for low zoom levels. + - https://docs.mapbox.com/mapbox-gl-js/guides/globe/#limitations-of-globe + - https://github.com/mapbox/mapbox-gl-js/issues/11795 + - https://github.com/UW-Macrostrat/web/issues/68 + + This is a workaround for that issue. + */ + let latMin = bounds._sw.lat; + let latMax = bounds._ne.lat; + + if (zoom < 5) { + latMin = Math.max(Math.min(latMin, latMin * 5), -85); + latMax = Math.min(Math.max(latMax, latMax * 5), 85); + } + + if (timeFilters.length && timeFilters.length > 1) { + urls = timeFilters.map((f) => { + let url = `${SETTINGS.pbdbDomain}/data1.2/colls/${ + zoom < maxClusterZoom ? "summary" : "list" + }.json?lngmin=${lngMin}&lngmax=${lngMax}&latmin=${latMin}&latmax=${latMax}&max_ma=${ + f.b_age + }&min_ma=${f.t_age}${zoom < maxClusterZoom ? level : ""}`; + if (queryString.length) { + url += `&${queryString.join("&")}`; + } + return url; + }); + } else { + let url = `${SETTINGS.pbdbDomain}/data1.2/colls/${ + zoom < maxClusterZoom ? "summary" : "list" + }.json?lngmin=${lngMin}&lngmax=${lngMax}&latmin=${latMin}&latmax=${latMax}${ + zoom < maxClusterZoom ? level : "" + }`; + if (timeQuery.length) { + url += `&${timeQuery.join("&")}`; + } + if (queryString.length) { + url += `&${queryString.join("&")}`; + } + urls = [url]; + } + + // Fetch the data + return await Promise.all( + urls.map((url) => fetch(url).then((response) => response.json())) + ).then((responses) => { + // Ignore data that comes with warnings, as it means nothing was + // found under most conditions + let data = responses + .filter((res) => { + if (!res.warnings) return res; + }) + .map((res) => res.records) + .reduce((a, b) => { + return [...a, ...b]; + }, []); + + return { + type: "FeatureCollection", + features: data.map((f, i) => { + return { + type: "Feature", + properties: f, + id: i, + geometry: { + type: "Point", + coordinates: [f.lng, f.lat], + }, + }; + }), + }; + }); +} diff --git a/src/components/lex/index.ts b/src/components/lex/index.ts index ef82f9df0..24c925e9f 100644 --- a/src/components/lex/index.ts +++ b/src/components/lex/index.ts @@ -36,7 +36,7 @@ function ColumnMapContainer(props) { { load: () => import("./map.client").then((d) => d.ColumnsMapContainer), fallback: h("div.loading", "Loading map..."), - deps: [props.columns, props.projectID, props.fossilData], + deps: [props.columns, props.projectID, props.fossilData, props.filters], }, (component) => h(component, props) ); @@ -104,6 +104,50 @@ export function ColumnsTable({ resData, colData, fossilsData }) { if (!colData || !colData.features || colData.features.length === 0) return; const summary = summarize(colData.features || []); + let filters = [] + + if (resData?.lith_id) { + const legend_ids = useAPIResult(apiV2Prefix + "/mobile/map_filter?lith_id=" + resData.lith_id) + + if (legend_ids) { + filters.push({ + category: "lithology", + type: "lithologies", + id: resData.lith_id, + name: resData.name, + legend_ids + }) + } + } + + if (resData?.concept_id) { + const legend_ids = useAPIResult(apiV2Prefix + "/mobile/map_filter?concept_id=" + resData.concept_id) + + if (legend_ids) { + filters.push({ + category: "strat_name", + type: "strat_name_concepts", + id: resData.concept_id, + name: resData.name, + legend_ids + }) + } + } + + if (resData?.int_id) { + filters.push( + { + ...resData, + category: "interval", + type: "intervals", + id: resData.int_id, + name: resData.name, + } + ) + } + + console.log("Filters in ColumnsTable:", filters) + const { b_age, t_age } = resData; const { @@ -153,6 +197,7 @@ export function ColumnsTable({ resData, colData, fossilsData }) { h("div.collections", pbdb_collections.toLocaleString() + " collections"), ]), h(ColumnMapContainer, { + filters, columns: colData, className: "column-map-container", fossilsData, @@ -938,41 +983,20 @@ export function Units({ href }) { }); } -export function Maps({ mapsData }) { - const ITEMS_PER_PAGE = 20; - const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE); - const data = useMemo(() => { - return mapsData.slice(0, visibleCount); - }, [mapsData, visibleCount]); - - const visibleItems = data.map((item) => - h( - "a.maps-item", - { - key: item.map_unit_name, - href: "/maps/" + item.source_id + "?legend=" + item.legend_id, - }, - `Map #${item.source_id}: ${item.map_unit_name} (#${item.legend_id})` - ) - ); - - const handleLoadMore = () => { - setVisibleCount((prev) => Math.min(prev + ITEMS_PER_PAGE, mapsData.length)); - }; - - const showLoadMore = visibleCount < mapsData.length; - - return h.if(mapsData?.length > 0)("div.maps-container", [ - h(ExpansionPanel, { title: "Maps", className: "maps-panel" }, [ - h("div.maps-list", [...visibleItems]), - h.if(showLoadMore)( - "div.load-more-wrapper", - h("button.load-more-btn", { onClick: handleLoadMore }, "Load More") - ), - ]), - ]); +export function Maps({ href }) { + return h(LinkCard, { + title: h(FlexRow, { justifyContent: "space-between" }, [ + h(FlexRow, { alignItems: "center", gap: ".5em"}, [ + h('h4', "Map Legends"), + h(BetaTag), + ]), + ]), + href: '/lex/legends?' + href, + className: "maps-card" + }); } + export function Fossils({ href }) { return h(LinkCard, { title: h(FlexRow, { justifyContent: "space-between" }, [ diff --git a/src/components/lex/map.client.ts b/src/components/lex/map.client.ts index 033a7c109..e6e454698 100644 --- a/src/components/lex/map.client.ts +++ b/src/components/lex/map.client.ts @@ -4,15 +4,23 @@ import { } from "@macrostrat/column-views"; import h from "./map.module.sass"; import { mapboxAccessToken } from "@macrostrat-web/settings"; -import { ErrorBoundary } from "@macrostrat/ui-components"; -import { ExpansionPanel } from "@macrostrat/map-interface"; +import { ErrorBoundary, useDarkMode } from "@macrostrat/ui-components"; +import { ExpansionPanel, buildInspectorStyle } from "@macrostrat/map-interface"; import { Icon } from "@blueprintjs/core" -import { useState, useRef } from "react"; +import { useState, useRef, useEffect } from "react"; import { useMapStyleOperator } from "@macrostrat/mapbox-react" import { satelliteMapURL } from "@macrostrat-web/settings"; import { setGeoJSON } from "@macrostrat/mapbox-utils"; import mapboxgl from "mapbox-gl" -import { pbdbDomain } from "@macrostrat-web/settings"; +import { pbdbDomain, tileserverDomain } from "@macrostrat-web/settings"; +import { buildMacrostratStyle } from "@macrostrat/map-styles"; +import { getExpressionForFilters } from "./filter-helper"; + +const _macrostratStyle = buildMacrostratStyle({ + tileserverDomain, + fillOpacity: 0.3, + strokeOpacity: 0.1, +}) as mapboxgl.Style; export function ColumnsMapContainer(props) { /* TODO: integrate this with shared web components */ @@ -28,14 +36,14 @@ function ColumnsMapInner({ className = "map-container", columns = null, lex = false, - fossilsData = [] + fossilsData = [], + filters = [], }) { const [showSatellite, setShowSatellite] = useState(true); const [showFossils, setShowFossils] = useState(false); - const [showOutcrop, setShowOutcrop] = useState(true); + const [showOutcrop, setShowOutcrop] = useState(false); const fossilClickRef = useRef(false); const hasFitted = useRef(false); - const fossilsExist = fossilsData?.features?.length > 0; function LexControls() { @@ -54,7 +62,7 @@ function ColumnsMapInner({ return h('div.lex-controls', [ h.if(fossilsExist)('div.btn', { onClick: handleFossils }, h(Icon, { icon: "mountain", className: 'icon' })), - // h('div.btn', { onClick: handleOutcrop }, h(Icon, { icon: "excavator", className: 'icon' })), + h.if(filters.length > 0)('div.btn', { onClick: handleOutcrop }, h(Icon, { icon: "excavator", className: 'icon' })), h('div.btn', { onClick: handleSatellite }, h(Icon, { icon: "satellite", className: 'icon' })), ]) } @@ -75,7 +83,7 @@ function ColumnsMapInner({ { columns, accessToken: mapboxAccessToken, - style: {height: "100%"}, + style: { ..._macrostratStyle, height: "100%" }, onSelectColumn: (id) => { setTimeout(() => { console.log("fossilClicked", fossilClickRef.current) @@ -93,13 +101,53 @@ function ColumnsMapInner({ [ fossilsExist ? h(FossilsLayer, { fossilsData, showFossils, fossilClickRef }) : null, h(LexControls), - !hasFitted.current ? h(FitBounds, { columnData: columns, hasFitted }) : null + !hasFitted.current ? h(FitBounds, { columnData: columns, hasFitted }) : null, + h(OutcropLayer, {showOutcrop, filters}) ] ), ] ); } +function OutcropLayer({showOutcrop, filters}) { + useMapStyleOperator((map) => { + if (map == null || filters.length === 0) return; + + const macrostratLayers = _macrostratStyle.layers + const macrostratSources = _macrostratStyle.sources + + if(!showOutcrop) { + macrostratLayers?.forEach((lyr) => { + if (map.getLayer(lyr.id)) { + map.removeLayer(lyr.id); + } + }); + return; + } + + macrostratLayers?.forEach((lyr) => { + if (!map.getLayer(lyr.id) && lyr.source) { + if (!map.getSource(lyr.source)) { + map.addSource(lyr.source, (_macrostratStyle.sources as any)[lyr.source]); + } + map.addLayer(lyr); + } + }); + + Object.keys(macrostratSources).forEach((src) => { + if (!map.getSource(src)) { + map.addSource(src, (macrostratSources as any)[src]); + } + }); + + const expr = getExpressionForFilters(filters); + map.setFilter("burwell_fill", expr); + }, [showOutcrop, filters]); + + return null; +} + + function FossilsLayer({ fossilsData, showFossils, fossilClickRef }) { useMapStyleOperator( (map) => {