diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 1cb64bbd..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Build - -on: pull_request - -jobs: - build: - name: Build - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [20] - - steps: - - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - - run: cd site; npm install - - - name: Setup vars 📋 - id: vars - run: | - echo identifier=$(git rev-parse --short ${{ github.sha }})-20.x >> $GITHUB_OUTPUT - echo distdir=aria-site/aria$(git rev-parse --short ${{ github.sha }})-20.x-dist >> $GITHUB_OUTPUT - - - run: cd site; npm run build - env: - BASE_ARIA_URL: ${{ steps.vars.outputs.identifier }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d2499de2..d1167434 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,11 +1,10 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions +# This workflow will do a clean install of node dependencies and deploy to AWS. name: Deploy on: push: - branches: [ main , manifest_driven_downloads] + branches: [ main ] pull_request: branches: [ main ] @@ -37,7 +36,7 @@ jobs: run: cd site; npm install - name: Build 🏃 run: cd site; npm run build - env: + env: BASE_ARIA_URL: ${{ steps.vars.outputs.ariabaseurl }} - name: Upload artifacts 📤 uses: actions/upload-artifact@v4 @@ -50,6 +49,9 @@ jobs: name: Deploy runs-on: ubuntu-latest needs: build + environment: + name: preview + url: https://d13285jxgcxetl.cloudfront.net/aria/${{ steps.vars.outputs.distdir }}/index.html steps: - name: Checkout 📥 @@ -94,4 +96,4 @@ jobs: - name: Deployment complete! 🚀 run: | - echo "Your build is at: https://d13285jxgcxetl.cloudfront.net/aria/${{ steps.vars.outputs.distdir }}/index.html" \ No newline at end of file + echo "Your build is at: https://d13285jxgcxetl.cloudfront.net/aria/${{ steps.vars.outputs.distdir }}/index.html" >> $GITHUB_STEP_SUMMARY diff --git a/site/src/DownloadCatalog.js b/site/src/DownloadCatalog.js index 997a293c..e90cee26 100644 --- a/site/src/DownloadCatalog.js +++ b/site/src/DownloadCatalog.js @@ -1,5 +1,5 @@ // URL for the remote manifest file -const STAC_URL = 'https://labs.overturemaps.org/stac/catalog.json' +const STAC_URL = 'https://stac.overturemaps.org/catalog.json' // Cache the manifest to avoid repeated fetches let cachedManifest = null; @@ -24,7 +24,7 @@ async function fetchManifest() { }) .then(stacData => { const latest = stacData.latest; - return fetch(`https://labs.overturemaps.org/stac/${latest}/manifest.geojson`); + return fetch(`https://stac.overturemaps.org/${latest}/manifest.geojson`); }) .then(response => { if (!response.ok) { diff --git a/site/src/Map.jsx b/site/src/Map.jsx index 5e21486f..b86073e8 100644 --- a/site/src/Map.jsx +++ b/site/src/Map.jsx @@ -20,8 +20,11 @@ import { layers } from "./Layers"; import ThemeTypeLayer from "./ThemeTypeLayer"; import FeaturePopup from "./FeatureSelector"; +// Fetch the latest Overture release from Overture STAC +const LATEST_RELEASE = await fetch('https://stac.overturemaps.org/catalog.json').then(r => r.json()).then(r => r.latest.split('.')[0]) + const PMTILES_URL = - "pmtiles://https://d3c1b7bog2u1nn.cloudfront.net/2025-07-23/"; + "pmtiles://https://d3c1b7bog2u1nn.cloudfront.net/" + LATEST_RELEASE + "/"; const INITIAL_VIEW_STATE = { latitude: 38.90678, diff --git a/site/src/inspector_panel/InspectorPanel.css b/site/src/inspector_panel/InspectorPanel.css index 00f8a3b3..1cd10972 100644 --- a/site/src/inspector_panel/InspectorPanel.css +++ b/site/src/inspector_panel/InspectorPanel.css @@ -32,7 +32,7 @@ overflow-y: scroll; overflow-x: auto; max-height: 250px; - table-layout: fixed; + table-layout: auto; } table th { diff --git a/site/src/inspector_panel/InspectorPanel.jsx b/site/src/inspector_panel/InspectorPanel.jsx index ce2e0279..a671731e 100644 --- a/site/src/inspector_panel/InspectorPanel.jsx +++ b/site/src/inspector_panel/InspectorPanel.jsx @@ -32,6 +32,19 @@ function InspectorPanel({ const theme = entity["theme"]; + // Determine the panel title - use name if available, otherwise default + let panelTitle = "Inspector Panel"; + if (entity["names"]) { + try { + const names = JSON.parse(entity["names"]); + if (names["primary"]) { + panelTitle = names["primary"]; + } + } catch (e) { + // If parsing fails, keep default title + } + } + let inspectorPanel =
; if (theme === "base") { @@ -101,14 +114,47 @@ function InspectorPanel({ } else { console.log("unhandled theme type"); console.log(entity); + + // Get all keys except those starting with @ + const allKeys = Object.keys(entity).filter((key) => !key.startsWith("@")); + + // Create custom ordering for class/subclass hierarchy + const orderedKeys = []; + const processedKeys = new Set(); + + // First pass: add all keys except subclass + allKeys.forEach(key => { + if (key !== "subclass") { + orderedKeys.push({ key, indented: false }); + processedKeys.add(key); + + // If this is "class" and "subclass" exists, add subclass right after + if (key === "class" && entity.hasOwnProperty("subclass")) { + orderedKeys.push({ key: "subclass", indented: true }); + processedKeys.add("subclass"); + } + } + }); + + // Second pass: add any remaining keys that weren't processed + allKeys.forEach(key => { + if (!processedKeys.has(key)) { + orderedKeys.push({ key, indented: false }); + } + }); + inspectorPanel = ( - {Object.keys(entity) - .filter((key) => !key.startsWith("@")) - .map((key) => ( - - ))} + {orderedKeys.map(({ key, indented }) => ( + + ))}
); @@ -117,7 +163,7 @@ function InspectorPanel({ return (
-

Inspector Panel

+
{panelTitle}
)} + {source.between && ( +
+ between: + {source.between} +
+ )} {index < sources.length - 1 && (
)} diff --git a/site/src/inspector_panel/TableRow.css b/site/src/inspector_panel/TableRow.css index b4734f37..19822aeb 100644 --- a/site/src/inspector_panel/TableRow.css +++ b/site/src/inspector_panel/TableRow.css @@ -1,65 +1,76 @@ .inspector-panel { + table { + table-layout: auto !important; + width: 100% !important; + display: block; + } + + tbody { + display: block; + } + + tr { + display: flex; + border-top: none; + border-bottom: 1px solid var(--border-color-light); + width: 100%; + } + td { - padding: 5px; - vertical-align: top; + padding: 2px 0; + padding-right: 8px; border-left: 0; border-right: 0; word-break: break-all; + font-size: 13px; + display: block; } td:first-child { text-align: left; - width: 40%; - word-break: normal; - border: none; - } - - td:last-child { - overflow: hidden; - width: 60%; + word-break: keep-all; + white-space: nowrap; border: none; + padding-right: 6px; + font-size: 11px; + font-weight: 500; + color: #64748b; + flex-shrink: 0; + width: auto; } - tr { - border-top: none; - border-bottom: var(--ifm-table-border-width) solid - var(--ifm-table-border-color); - } - - td.expanded { - max-height: unset; - white-space: normal; + td:first-child strong { + font-weight: bold; + color: var(--ifm-font-color-base); + font-size: 13px; } - td.collapsed { - max-height: 26px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + td:last-child { + border: none; + word-break: break-word; + flex: 1; + min-width: 0; } -} -button.expand { - border: none; - border-radius: 50%; - padding: 0; - height: 26px; - width: 26px; - justify-content: center; - background-color: unset; } -button.expand:focus { - outline: none; +/* Nested content styling */ +.nested-content { + margin-left: 8px; + border-left: 1px solid #e2e8f0; + padding-left: 6px; + margin-top: 2px; } -div.first-child { - display: flex; - justify-content: space-between; - padding-right: 10px; +.nested-item { + margin: 1px 0; + line-height: 1.3; + font-size: 12px; } -svg.ec-icon { - padding-top: 1px; - fill: var(--ifm-color-secondary-darkest); +.nested-key { + color: #64748b; + font-size: 11px; + font-weight: 500; + margin-right: 3px; } diff --git a/site/src/inspector_panel/TableRow.jsx b/site/src/inspector_panel/TableRow.jsx index aaf4081d..d96dccee 100644 --- a/site/src/inspector_panel/TableRow.jsx +++ b/site/src/inspector_panel/TableRow.jsx @@ -1,49 +1,167 @@ import PropTypes from "prop-types"; -import { useState } from "react"; import "./TableRow.css"; -import AddIcon from "@mui/icons-material/Add"; -import RemoveIcon from "@mui/icons-material/Remove"; -import InfoToolTip from "./InfoToolTip"; -function TableRow({ mode, table_key, entity }) { - const [expanded, setExpanded] = useState(false); +function TableRow({ mode, table_key, entity, indented = false }) { + // Function to check if a value is a URL + const isURL = (value) => { + if (!value || typeof value !== 'string') return false; + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } + }; + + // Function to render a single URL as a clickable link + const renderURL = (url) => ( + + {url} + + ); + + // Function to render value content without nested-content wrapper (for recursive calls) + const renderValueContent = (value) => { + // Handle null/undefined + if (value == null) return 'null'; + + // Handle arrays first - render inline for simple arrays, items for complex ones + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + + // Check if all items are URLs + const allURLs = value.every(item => isURL(item)); + + // Check if all items are simple (strings, numbers, booleans) + const allSimple = value.every(item => + typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' + ); + + if (allURLs) { + // Render URL arrays as items + return value.map((item, index) => ( +
+ {renderURL(item)} +
+ )); + } else if (allSimple && value.length <= 3) { + // Render simple arrays inline + return `[${value.join(', ')}]`; + } + + // Render complex or long arrays as items + return value.map((item, index) => ( +
+ {renderValueContent(item)} +
+ )); + } + + // Handle objects + if (typeof value === 'object') { + const entries = Object.entries(value).filter(([key, val]) => val != null && val !== "null"); + if (entries.length === 0) return '{}'; + + return entries.map(([key, val]) => ( +
+ + {key}: + {' '} + {renderValueContent(val)} +
+ )); + } - const handleExpand = () => { - setExpanded(!expanded); + // Now handle strings - convert to string for further processing + const stringValue = value.toString(); + + // Try to parse as JSON if it looks like an array or object + if ((stringValue.startsWith('[') && stringValue.endsWith(']')) || + (stringValue.startsWith('{') && stringValue.endsWith('}'))) { + try { + const parsed = JSON.parse(stringValue); + return renderValueContent(parsed); + } catch { + // If parsing fails, fall through to regular string handling + } + } + + // Check if it's a URL + if (isURL(stringValue)) { + return renderURL(stringValue); + } + + return stringValue; + }; + + // Function to render value with nested-content wrapper (for top-level calls) + const renderValue = (value) => { + // Handle simple values directly + if (value == null || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean') { + return renderValueContent(value); + } + + // Handle arrays - check if simple first + if (Array.isArray(value)) { + if (value.length === 0) return '[]'; + + const allSimple = value.every(item => + typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean' + ); + + if (allSimple && value.length <= 3) { + return `[${value.join(', ')}]`; + } + } + + // For complex values (objects, complex arrays), wrap in nested-content + const content = renderValueContent(value); + + // If content is a string, return it directly + if (typeof content === 'string') { + return content; + } + + // Otherwise wrap in nested-content + return ( +
+ {content} +
+ ); }; return ( - -
- {table_key} - -
+ + {table_key}: {entity[table_key] != null ? ( - - {entity[table_key].toString()}{" "} + + {renderValue(entity[table_key])} ) : ( - None Found + None Found )} ); } TableRow.propTypes = { - entity: PropTypes.object, + mode: PropTypes.string, + table_key: PropTypes.string.isRequired, + entity: PropTypes.object.isRequired, + indented: PropTypes.bool, }; export default TableRow; diff --git a/site/src/inspector_panel/ThemePanel.jsx b/site/src/inspector_panel/ThemePanel.jsx index cf60a1dc..b7435e24 100644 --- a/site/src/inspector_panel/ThemePanel.jsx +++ b/site/src/inspector_panel/ThemePanel.jsx @@ -6,6 +6,7 @@ import PushPinIcon from "@mui/icons-material/PushPin"; import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined"; import ThemeIcon from "./ThemeIcon"; import SourcesRow from "./SourcesRow.jsx"; +import NestedPropertyRow from "./NestedPropertyRow.jsx"; const sharedProperties = [ "theme", @@ -13,28 +14,24 @@ const sharedProperties = [ "update_time", "id", "sources", + "names", + "categories", "subtype", + "class", "version", ]; function ThemePanel({ mode, entity, tips, activeThemes, setActiveThemes }) { return (
- {entity["names"] ? ( -
-
- {JSON.parse(entity["names"])["primary"]} -
-
- ) : ( - <> - )} {entity["id"] ? (
- id: - { - navigator.clipboard.writeText(entity["id"]); - }}>{entity["id"]} +
+ id: + { + navigator.clipboard.writeText(entity["id"]); + }}>{entity["id"]} +
@@ -99,6 +96,42 @@ function ThemePanel({ mode, entity, tips, activeThemes, setActiveThemes }) { ) : ( <> )} + {entity["class"] ? ( +
+
+ class: + {entity["class"]} + {entity["subclass"] ? ( +
+ subclass: + {entity["subclass"]} +
+ ) : ( + <> + )} +
+ +
+ ) : ( + <> + )} + + + {entity["sources"] ? ( ) : ( @@ -117,6 +150,7 @@ function ThemePanel({ mode, entity, tips, activeThemes, setActiveThemes }) { {Object.keys(entity) .filter((key) => !key.startsWith("@")) .filter((key) => !sharedProperties.includes(key)) + .filter((key) => entity[key] != null && entity[key] !== "null") .map((key) => ( ))} diff --git a/site/src/main.jsx b/site/src/main.jsx index 3fc910fc..fe0129b1 100644 --- a/site/src/main.jsx +++ b/site/src/main.jsx @@ -3,14 +3,9 @@ import ReactDOM from 'react-dom/client' import App from './App.jsx' import './index.css' import 'infima/dist/css/default/default.css'; -import initWasm from "@geoarrow/geoarrow-wasm/esm/index.js"; -import wasmUrl from "@geoarrow/geoarrow-wasm/esm/index_bg.wasm?url" - -//TODO: Make this async and parallelize with the startup of the map component, rather than blocking in. -await initWasm(wasmUrl); ReactDOM.createRoot(document.getElementById('root')).render( , -) \ No newline at end of file +) diff --git a/site/src/nav/DownloadButton.jsx b/site/src/nav/DownloadButton.jsx index 11320d9a..5d131f5c 100644 --- a/site/src/nav/DownloadButton.jsx +++ b/site/src/nav/DownloadButton.jsx @@ -12,6 +12,8 @@ import RefreshIcon from "../icons/icon-refresh.svg?react"; import "./DownloadButton.css"; import Floater from "react-floater"; import CloseIcon from "@mui/icons-material/Close"; +import initWasm from "@geoarrow/geoarrow-wasm/esm/index.js"; +import wasmUrl from "@geoarrow/geoarrow-wasm/esm/index_bg.wasm?url" const ZOOM_BOUND = 15; @@ -29,6 +31,11 @@ function DownloadButton({ mode, zoom, setZoom, visibleTypes}) { }, [myMap]); const handleDownloadClick = async () => { + + //TODO: Make this async and parallelize with the startup of the map component, rather than blocking in. + await initWasm(wasmUrl); + + setLoading(true); try { //Get current map dimensions and convert to bbox