diff --git a/components/constants.js b/components/constants.js index 50d79ec..3930cb4 100644 --- a/components/constants.js +++ b/components/constants.js @@ -25,6 +25,15 @@ export const LABELS = { }, } +export const BOUNDARY_PMTILES_URL = + 'https://carbonplan-offsets-db.s3.us-west-2.amazonaws.com/miscellaneous/project-boundaries.pmtiles' + +export const BOUNDARY_LAYER_ID = 'boundaries' +export const CENTROIDS_LAYER_ID = 'centroids' + +export const BASEMAP_PMTILES_URL = + 'https://carbonplan-maps.s3.us-west-2.amazonaws.com/basemaps/pmtiles/global.pmtiles' + export const COLORS = { agriculture: 'orange', forest: 'green', diff --git a/components/map.js b/components/map.js new file mode 100644 index 0000000..4bdb719 --- /dev/null +++ b/components/map.js @@ -0,0 +1,447 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' +import { createPortal } from 'react-dom' +import { Box, get, useThemeUI } from 'theme-ui' +import { useRouter } from 'next/router' +import maplibregl from 'maplibre-gl' +import 'maplibre-gl/dist/maplibre-gl.css' +import { Protocol } from 'pmtiles' +import { Arrow } from '@carbonplan/icons' +import { + BOUNDARY_PMTILES_URL, + BASEMAP_PMTILES_URL, + BOUNDARY_LAYER_ID, + CENTROIDS_LAYER_ID, + COLORS, +} from './constants' +import { useMapTheme } from './use-map-theme' +import { getProjectCategory } from './utils' + +const Map = ({ project }) => { + const mapContainer = useRef(null) + const map = useRef(null) + const hoveredFeatureId = useRef(null) + const hoveredFeature = useRef(null) + const markerRef = useRef(null) + const [markerEl, setMarkerEl] = useState(null) + const [markerVisible, setMarkerVisible] = useState(false) + const mapLayers = useMapTheme() + const { theme } = useThemeUI() + const router = useRouter() + + const colorName = COLORS[getProjectCategory(project)] ?? COLORS.other + const color = get(theme, `rawColors.${colorName}`, colorName) + const secondary = get(theme, 'rawColors.secondary') + const background = get(theme, 'rawColors.background') + const hinted = get(theme, 'rawColors.hinted') + const primary = get(theme, 'rawColors.primary') + + const bounds = useMemo( + () => [ + [project.bbox.xmin, project.bbox.ymin], + [project.bbox.xmax, project.bbox.ymax], + ], + [project.bbox] + ) + + const projectLayers = useMemo(() => { + const isProject = ['==', ['get', 'project_id'], project.project_id] + return [ + { + id: 'project-boundaries-fill', + type: 'fill', + source: 'project-boundaries', + 'source-layer': BOUNDARY_LAYER_ID, + paint: { + 'fill-color': ['case', isProject, color, secondary], + 'fill-opacity': 0.2, + }, + }, + { + id: 'project-boundaries-outline', + type: 'line', + source: 'project-boundaries', + 'source-layer': BOUNDARY_LAYER_ID, + paint: { + 'line-color': ['case', isProject, color, secondary], + 'line-width': [ + 'case', + isProject, + 1, + ['case', ['==', ['feature-state', 'hover'], true], 1, 0.5], + ], + }, + }, + { + id: 'project-centroids-label', + type: 'symbol', + source: 'project-boundaries', + 'source-layer': CENTROIDS_LAYER_ID, + layout: { + 'text-field': ['get', 'project_id'], + 'text-size': 16, + 'text-font': ['Relative Mono Pro 11 Pitch'], + 'text-letter-spacing': 0.02, + 'text-anchor': 'center', + 'symbol-sort-key': ['case', isProject, 0, 1], + }, + paint: { + 'text-color': [ + 'case', + isProject, + color, + [ + 'case', + ['==', ['feature-state', 'hover'], true], + primary, + secondary, + ], + ], + 'text-halo-color': [ + 'case', + isProject, + background, + [ + 'case', + ['==', ['feature-state', 'hover'], true], + hinted, + background, + ], + ], + 'text-halo-width': 2, + }, + }, + ] + }, [project.project_id, color, secondary, primary, background, hinted]) + + const mapControlStyles = useMemo( + () => ({ + '& .maplibregl-control-container': { + fontSize: [0, 0, 0, 0], + fontFamily: 'faux', + letterSpacing: 'faux', + '& [class*="maplibregl-ctrl-"]': { zIndex: 0 }, + '& .maplibregl-ctrl-attrib': { + bg: 'hinted', + alignItems: 'center', + border: `1px solid`, + borderColor: 'muted', + color: 'secondary', + display: 'flex', + '& a': { color: 'secondary' }, + '& .maplibregl-ctrl-attrib-button': { + bg: 'hinted', + backgroundImage: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath fill='${encodeURIComponent( + primary + )}' d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")`, + '&:hover, &:focus-visible': { + backgroundImage: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath fill='${encodeURIComponent( + secondary + )}' d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")`, + }, + }, + }, + '& .maplibregl-ctrl-group': { + marginBottom: 0, + bg: 'hinted', + border: `1px solid`, + borderColor: 'muted', + borderRadius: '20px', + boxShadow: 'none', + overflow: 'hidden', + '& button': { + bg: 'hinted', + border: 'none', + borderBottom: `1px solid`, + borderColor: 'muted', + width: '24px', + height: '24px', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', + '&:last-child': { + borderBottom: 'none', + }, + '& .maplibregl-ctrl-icon': { + backgroundSize: '20px', + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center', + }, + }, + '& .maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon': { + backgroundImage: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath stroke='${encodeURIComponent( + primary + )}' stroke-width='2' stroke-linecap='round' fill='none' d='M10 6v8M6 10h8'/%3E%3C/svg%3E")`, + }, + '& .maplibregl-ctrl-zoom-in:hover .maplibregl-ctrl-icon, & .maplibregl-ctrl-zoom-in:focus-visible .maplibregl-ctrl-icon': + { + backgroundImage: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath stroke='${encodeURIComponent( + secondary + )}' stroke-width='2' stroke-linecap='round' fill='none' d='M10 6v8M6 10h8'/%3E%3C/svg%3E")`, + }, + '& .maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon': { + backgroundImage: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath stroke='${encodeURIComponent( + primary + )}' stroke-width='2' stroke-linecap='round' fill='none' d='M6 10h8'/%3E%3C/svg%3E")`, + }, + '& .maplibregl-ctrl-zoom-out:hover .maplibregl-ctrl-icon, & .maplibregl-ctrl-zoom-out:focus-visible .maplibregl-ctrl-icon': + { + backgroundImage: `url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath stroke='${encodeURIComponent( + secondary + )}' stroke-width='2' stroke-linecap='round' fill='none' d='M6 10h8'/%3E%3C/svg%3E")`, + }, + }, + }, + }), + [primary, secondary] + ) + + useEffect(() => { + if (!mapContainer.current || map.current) return + + const setFeatureHover = (featureId, hover) => { + if (!featureId || !map.current) return + const layers = [BOUNDARY_LAYER_ID, CENTROIDS_LAYER_ID] + layers.forEach((sourceLayer) => { + map.current.setFeatureState( + { source: 'project-boundaries', sourceLayer, id: featureId }, + { hover } + ) + }) + } + + const clearHover = () => { + if (hoveredFeatureId.current) { + setFeatureHover(hoveredFeatureId.current, false) + hoveredFeatureId.current = null + } + hoveredFeature.current = null + if (map.current) { + map.current.getCanvas().style.cursor = '' + } + setMarkerVisible(false) + } + + const updateHover = (feature) => { + const featureId = feature.id || feature.properties.project_id + const featureProjectId = feature.properties?.project_id + if (featureId !== hoveredFeatureId.current) { + clearHover() + hoveredFeatureId.current = featureId + setFeatureHover(featureId, true) + } + if (featureProjectId !== project.project_id) { + map.current.getCanvas().style.cursor = 'pointer' + } + } + + const updateMarkerPosition = (feature) => { + if (!markerRef.current || !feature) { + hoveredFeature.current = null + return + } + + const projectId = feature.properties?.project_id + if (projectId === project.project_id) { + setMarkerVisible(false) + hoveredFeature.current = null + return + } + + hoveredFeature.current = feature + const coords = feature.geometry.coordinates + const point = map.current.project(coords) + + const fontSize = 16 + const letterSpacing = 0.02 // em + const charWidth = fontSize * (0.55 + letterSpacing) + const textWidth = projectId.length * charWidth + const offsetX = textWidth / 2 + 8 + + const offsetPoint = map.current.unproject([point.x + offsetX, point.y]) + markerRef.current.setLngLat(offsetPoint) + setMarkerVisible(true) + } + + const handleZoom = () => { + if (hoveredFeature.current) { + updateMarkerPosition(hoveredFeature.current) + } + } + + const handleMouseMove = (event) => { + // Query both layers, prioritize labels over fills + const labels = map.current.queryRenderedFeatures(event.point, { + layers: ['project-centroids-label'], + }) + if (labels[0]) { + updateHover(labels[0]) + updateMarkerPosition(labels[0]) + return + } + + const tolerance = 3 + const bbox = [ + [event.point.x - tolerance, event.point.y - tolerance], + [event.point.x + tolerance, event.point.y + tolerance], + ] + const fills = map.current.queryRenderedFeatures(bbox, { + layers: ['project-boundaries-fill'], + }) + if (fills[0]) { + updateHover(fills[0]) + // Find the visible centroid label for this project to position the marker + const projectId = fills[0].properties?.project_id + const centroids = map.current.queryRenderedFeatures(undefined, { + layers: ['project-centroids-label'], + filter: ['==', ['get', 'project_id'], projectId], + }) + if (centroids[0]) { + updateMarkerPosition(centroids[0]) + } else { + setMarkerVisible(false) + } + } else { + clearHover() + } + } + + const handleClick = (event) => { + const clickedProjectId = event.features?.[0]?.properties?.project_id + if (clickedProjectId && clickedProjectId !== project.project_id) { + router.push(`/projects/${clickedProjectId}`) + } + } + + const protocol = new Protocol() + maplibregl.addProtocol('pmtiles', protocol.tile) + + map.current = new maplibregl.Map({ + container: mapContainer.current, + bounds, + fitBoundsOptions: { padding: 10 }, + scrollZoom: false, + style: { + version: 8, + glyphs: + 'https://carbonplan-maps.s3.us-west-2.amazonaws.com/basemaps/fonts/{fontstack}/{range}.pbf', + sources: { + basemap: { + type: 'vector', + url: `pmtiles://${BASEMAP_PMTILES_URL}`, + attribution: + 'Protomaps © OpenStreetMap', + }, + 'project-boundaries': { + type: 'vector', + url: `pmtiles://${BOUNDARY_PMTILES_URL}`, + promoteId: 'project_id', + attribution: + 'Karnik et al. (2025)', + }, + }, + layers: mapLayers, + }, + attributionControl: false, + }) + + map.current.addControl( + new maplibregl.AttributionControl({ compact: true }), + 'bottom-right' + ) + + map.current.addControl( + new maplibregl.NavigationControl({ showCompass: false }), + 'bottom-right' + ) + + map.current.on('load', () => { + projectLayers + .filter((layer) => layer.id !== 'project-centroids-label') + .forEach((layer) => map.current.addLayer(layer, 'address_label')) + + const labelLayer = projectLayers.find( + (layer) => layer.id === 'project-centroids-label' + ) + if (labelLayer) map.current.addLayer(labelLayer) + + const el = document.createElement('div') + markerRef.current = new maplibregl.Marker({ + element: el, + anchor: 'center', + }) + .setLngLat([0, 0]) + .addTo(map.current) + setMarkerEl(el) + + map.current.on('mousemove', handleMouseMove) + map.current.on('zoom', handleZoom) + map.current.on('click', 'project-boundaries-fill', handleClick) + map.current.on('click', 'project-centroids-label', handleClick) + }) + + const handleKeyDown = (e) => { + if ((e.metaKey || e.ctrlKey) && map.current) { + map.current.scrollZoom.enable() + } + } + + const handleKeyUp = (e) => { + if (!e.metaKey && !e.ctrlKey && map.current) { + map.current.scrollZoom.disable() + } + } + + window.addEventListener('keydown', handleKeyDown) + window.addEventListener('keyup', handleKeyUp) + + return () => { + window.removeEventListener('keydown', handleKeyDown) + window.removeEventListener('keyup', handleKeyUp) + if (markerRef.current) { + markerRef.current.remove() + markerRef.current = null + } + setMarkerEl(null) + if (map.current) { + clearHover() + map.current.remove() + map.current = null + } + maplibregl.removeProtocol?.('pmtiles') + } + }, [bounds, projectLayers, mapLayers, project.project_id, router]) + + return ( + <> + + {markerEl && + createPortal( + , + markerEl + )} + + ) +} + +export default Map diff --git a/components/project.js b/components/project.js index 6be73cd..aa0cd3b 100644 --- a/components/project.js +++ b/components/project.js @@ -8,6 +8,7 @@ import ProjectOverview from './project-overview' import Timeline from './timeline' import BackButton from './back-button' import Quantity from './quantity' +import Map from './map' import { getProjectCategory } from './utils' const Project = ({ project }) => { @@ -115,6 +116,17 @@ const Project = ({ project }) => { + {project.bbox && ( + <> + + Boundary + + + + + + )} + Transactions diff --git a/components/queries.js b/components/queries.js index 79c35cd..985c21c 100644 --- a/components/queries.js +++ b/components/queries.js @@ -1,4 +1,4 @@ -import { Column, Filter, Input, Link, Row } from '@carbonplan/components' +import { Column, Filter, Input, Link, Row, Tag } from '@carbonplan/components' import { useRouter } from 'next/router' import { createContext, useContext, useEffect, useState } from 'react' import { Box, Flex } from 'theme-ui' @@ -30,6 +30,7 @@ export const QueryProvider = ({ children }) => { ) const [projectType, setProjectType] = useState(null) const [complianceOnly, setComplianceOnly] = useState(null) + const [hasGeography, setHasGeography] = useState(null) const [search, setSearch] = useState('') const [beneficiarySearch, setBeneficiarySearch] = useState('') const [listingBounds, setListingBounds] = useState(null) @@ -59,6 +60,8 @@ export const QueryProvider = ({ children }) => { setRegistry, category, setCategory, + hasGeography, + setHasGeography, projectType, setProjectType, complianceOnly, @@ -118,6 +121,8 @@ const Queries = () => { setRegistry, complianceOnly, setComplianceOnly, + hasGeography, + setHasGeography, search, setSearch, beneficiarySearch, @@ -141,7 +146,7 @@ const Queries = () => { return ( {view === 'projects' && ( - + Project @@ -175,7 +180,7 @@ const Queries = () => { )} {view === 'transactions' && ( - + User @@ -208,7 +213,7 @@ const Queries = () => { )} - + Registry @@ -227,7 +232,7 @@ const Queries = () => { - + Category @@ -240,7 +245,7 @@ const Queries = () => { - + Type @@ -253,7 +258,7 @@ const Queries = () => { - + Country @@ -269,7 +274,7 @@ const Queries = () => { - + Protocol @@ -285,7 +290,7 @@ const Queries = () => { - + Program @@ -316,6 +321,39 @@ const Queries = () => { + + + Geography + + + + { + let value + if (obj.available && obj.missing) { + value = null + } else if (obj.available) { + value = true + } else if (obj.missing) { + value = false + } else { + return + } + setHasGeography(value) + }} + multiSelect + /> + + + ) } diff --git a/components/use-fetcher.js b/components/use-fetcher.js index b3df724..ad3c5aa 100644 --- a/components/use-fetcher.js +++ b/components/use-fetcher.js @@ -22,6 +22,7 @@ const fetcher = ([ category, projectType, complianceOnly, + hasGeography, search, listingBounds, transactionBounds, @@ -129,6 +130,10 @@ const fetcher = ([ protocols.forEach((protocol) => params.append('protocol', protocol)) } + if (typeof hasGeography === 'boolean') { + params.append('geography', hasGeography ? 'true' : 'false') + } + const reqUrl = new URL( '/research/offsets-db/api/query', window.location.origin @@ -162,6 +167,7 @@ const useFetcher = ( category, projectType, complianceOnly, + hasGeography, search, listingBounds, transactionBounds, @@ -176,6 +182,7 @@ const useFetcher = ( useDebounce(category), useDebounce(projectType), complianceOnly, + hasGeography, useDebounce(search), useDebounce(listingBounds), useDebounce(transactionBounds), diff --git a/components/use-map-theme.js b/components/use-map-theme.js new file mode 100644 index 0000000..b1c8c4b --- /dev/null +++ b/components/use-map-theme.js @@ -0,0 +1,174 @@ +import { useMemo } from 'react' +import { useColorMode, useThemeUI, get } from 'theme-ui' +import { layers, namedFlavor } from '@protomaps/basemaps' + +const language = 'en' + +export const useMapTheme = () => { + const [colorMode] = useColorMode() + const { theme } = useThemeUI() + const isDark = colorMode === 'dark' + const flavorName = isDark ? 'black' : 'white' + const transparent = 'transparent' + const hinted = get(theme, 'rawColors.hinted') + const primary = get(theme, 'rawColors.primary') + const muted = get(theme, 'rawColors.muted') + const background = get(theme, 'rawColors.background') + const secondary = get(theme, 'rawColors.secondary') + + const mapTheme = useMemo( + () => ({ + ...namedFlavor(flavorName), + buildings: muted, + background: transparent, + park_a: transparent, + park_b: transparent, + hospital: transparent, + industrial: transparent, + school: transparent, + wood_a: transparent, + wood_b: transparent, + pedestrian: transparent, + scrub_a: transparent, + scrub_b: transparent, + glacier: transparent, + sand: transparent, + beach: transparent, + aerodrome: transparent, + runway: transparent, + earth: transparent, + zoo: transparent, + military: transparent, + + landcover: { + barren: transparent, + farmland: transparent, + forest: transparent, + glacier: transparent, + grassland: transparent, + scrub: transparent, + urban_area: transparent, + }, + + water: hinted, + + bridges_other_casing: background, + bridges_minor_casing: background, + bridges_link_casing: background, + bridges_major_casing: background, + bridges_highway_casing: background, + bridges_other: muted, + bridges_minor: muted, + bridges_link: muted, + bridges_major: muted, + bridges_highway: muted, + + minor_service_casing: background, + minor_casing: background, + link_casing: background, + major_casing_late: background, + highway_casing_late: background, + other: muted, + minor_service: muted, + minor_a: muted, + minor_b: muted, + link: muted, + major_casing_early: background, + major: muted, + highway_casing_early: background, + highway: muted, + pier: muted, + + railway: muted, + boundaries: muted, + + roads_label_minor: muted, + roads_label_minor_halo: background, + roads_label_major: muted, + roads_label_major_halo: background, + ocean_label: muted, + subplace_label: [ + 'interpolate', + ['linear'], + ['zoom'], + 12, + muted, + 22, + primary, + ], + subplace_label_halo: background, + city_label: [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + muted, + 12, + secondary, + ], + city_label_halo: background, + state_label: [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + muted, + 12, + secondary, + ], + state_label_halo: background, + country_label: [ + 'interpolate', + ['linear'], + ['zoom'], + 0, + muted, + 12, + secondary, + ], + + address_label: muted, + address_label_halo: background, + + regular: 'Relative Faux Pro Book', + bold: 'Relative Faux Pro Book', + italic: 'Relative Faux Pro Book', + }), + [flavorName, hinted, muted, background, primary] + ) + + const mapLayers = useMemo(() => { + const baseLayers = layers('basemap', mapTheme, { + lang: language, + }) + + return baseLayers.map((layer) => { + if (layer.id === 'places_locality' && layer.type === 'symbol') { + const { 'text-variable-anchor': _, ...restLayout } = layer.layout + return { + ...layer, + layout: { + ...restLayout, + 'text-anchor': 'center', + 'text-justify': 'center', + 'text-letter-spacing': 0.05, + }, + } + } + if (layer.type === 'symbol' && layer.layout?.['text-field']) { + return { + ...layer, + layout: { + ...layer.layout, + 'text-letter-spacing': 0.05, + }, + } + } + return layer + }) + }, [mapTheme]) + + return mapLayers +} + +export default useMapTheme diff --git a/package-lock.json b/package-lock.json index f0ba3b6..fc822a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,12 +20,15 @@ "@mdx-js/loader": "^2.1.5", "@mdx-js/react": "^2.1.5", "@next/mdx": "^12.3.1", + "@protomaps/basemaps": "^5.7.0", "@theme-ui/color": "^0.15.3", "@theme-ui/match-media": "^0.15.3", "d3-brush": "^3.0.0", "d3-format": "^3.1.0", "d3-selection": "^3.0.0", + "maplibre-gl": "^5.14.0", "next": "^14.2.23", + "pmtiles": "^4.3.0", "polished": "^4.2.2", "react": "^18.2.0", "react-animate-height": "^3.2.2", @@ -400,6 +403,109 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "license": "ISC", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", + "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", + "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", + "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^4.0.1" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "license": "ISC", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.3.1.tgz", + "integrity": "sha512-TUM5JD40H2mgtVXl5IwWz03BuQabw8oZQLJTmPpJA0YTYF+B+oZppy5lNMO6bMvHzB+/5mxqW9VLG3wFdeqtOw==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "rw": "^1.3.3", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/mlt": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.1.2.tgz", + "integrity": "sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.1.0.tgz", + "integrity": "sha512-9LjFAoWtxdGRns8RK9vG3Fcw/fb3eHMxvAn2jffwn3jnVO1k49VOv6+FEza70rK7WzF8GnBiKa0K39RyfevKUw==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/vector-tile": "^2.0.4", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "geojson-vt": "^4.0.2", + "pbf": "^4.0.1", + "supercluster": "^8.0.1" + } + }, "node_modules/@mdx-js/loader": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@mdx-js/loader/-/loader-2.1.5.tgz", @@ -627,6 +733,15 @@ "node": ">= 10" } }, + "node_modules/@protomaps/basemaps": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@protomaps/basemaps/-/basemaps-5.7.0.tgz", + "integrity": "sha512-vIInnzVSxHuOcvj1BFGkCjlFxG/9a1GV23t98kGEVcPUM7aEqTnf6loUHTRJYX5eCz+WCO16N0aibr1SLg830Q==", + "license": "BSD-3-Clause", + "bin": { + "generate_style": "src/cli.ts" + } + }, "node_modules/@styled-system/background": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@styled-system/background/-/background-5.1.2.tgz", @@ -896,6 +1011,21 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/hast": { "version": "2.3.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz", @@ -967,6 +1097,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/unist": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", @@ -1636,6 +1775,12 @@ "node": ">=0.3.1" } }, + "node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "license": "ISC" + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -1833,6 +1978,12 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "peer": true }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -1843,11 +1994,35 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/geojson-vt": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/github-slugger": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==" }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -2125,6 +2300,18 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "peer": true }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -2167,6 +2354,44 @@ "loose-envify": "cli.js" } }, + "node_modules/maplibre-gl": { + "version": "5.14.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.14.0.tgz", + "integrity": "sha512-O2ok6N/bQ9NA9nJ22r/PRQQYkUe9JwfDMjBPkQ+8OwsVH4TpA5skIAM2wc0k+rni5lVbAVONVyBvgi1rF2vEPA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.0.7", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^2.0.4", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^24.3.1", + "@maplibre/mlt": "^1.1.2", + "@maplibre/vt-pbf": "^4.1.0", + "@types/geojson": "^7946.0.16", + "@types/geojson-vt": "3.2.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.2", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.4", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^4.0.1", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, "node_modules/markdown-extensions": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-1.1.1.tgz", @@ -2918,6 +3143,15 @@ "node": ">= 0.6" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -2931,6 +3165,12 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.8", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", @@ -3079,6 +3319,18 @@ "node": ">=8" } }, + "node_modules/pbf": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", + "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, "node_modules/periscopic": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.0.4.tgz", @@ -3093,6 +3345,15 @@ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, + "node_modules/pmtiles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.3.0.tgz", + "integrity": "sha512-wnzQeSiYT/MyO63o7AVxwt7+uKqU0QUy2lHrivM7GvecNy0m1A4voVyGey7bujnEW5Hn+ZzLdvHPoFaqrOzbPA==", + "license": "BSD-3-Clause", + "dependencies": { + "fflate": "^0.8.2" + } + }, "node_modules/polished": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/polished/-/polished-4.2.2.tgz", @@ -3132,6 +3393,12 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, "node_modules/prettier": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.2.tgz", @@ -3153,6 +3420,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", + "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -3162,6 +3435,12 @@ "node": ">=6" } }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3334,6 +3613,21 @@ "node": ">=4" } }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -3522,6 +3816,15 @@ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz", "integrity": "sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA==" }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -3631,6 +3934,12 @@ "react": ">=18" } }, + "node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index cdc874d..a914727 100644 --- a/package.json +++ b/package.json @@ -32,12 +32,15 @@ "@mdx-js/loader": "^2.1.5", "@mdx-js/react": "^2.1.5", "@next/mdx": "^12.3.1", + "@protomaps/basemaps": "^5.7.0", "@theme-ui/color": "^0.15.3", "@theme-ui/match-media": "^0.15.3", "d3-brush": "^3.0.0", "d3-format": "^3.1.0", "d3-selection": "^3.0.0", + "maplibre-gl": "^5.14.0", "next": "^14.2.23", + "pmtiles": "^4.3.0", "polished": "^4.2.2", "react": "^18.2.0", "react-animate-height": "^3.2.2",