diff --git a/.gitignore b/.gitignore index c9b2fcee..6c3e1b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ delete_old_cluster.sh **/isolation/output **/isolation/isolation.conf pg_lake_iceberg/logs/polaris.log +node_modules/ +.DS_Store +.env +.env.local diff --git a/docs/examples/overture-maps-postgis-maplibre/README.md b/docs/examples/overture-maps-postgis-maplibre/README.md new file mode 100644 index 00000000..ca81e347 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/README.md @@ -0,0 +1,235 @@ +# pg_lake for Overture Maps and PostGIS with MapLibre + +**A small sample app demonstrating pg_lake's ability to query remote Parquet data from object storage alongside local PostGIS data.** + +This example showcases a retail site selection tool that combines: + +- Local PostGIS Data: Company stores, service areas, and sales data in your PostgreSQL database +- Remote Overture Maps Data: POI and building data queried directly from S3 Parquet files via pg_lake +- Web Interface: Interactive MapLibre GL JS map with real-time spatial analysis + + +### Key Technologies + +- **PostgreSQL + PostGIS**: Core spatial database +- **pg_lake**: Query Parquet files from S3/object storage directly in Postgres +- **Overture Maps**: Global open map data (places, buildings, roads) +- **MapLibre GL JS**: Modern, performant web mapping +- **Node.js + Express**: Backend API serving GeoJSON data + +## ๐๏ธ Architecture + +``` +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +โ Frontend (MapLibre) โ +โ - Interactive map with company stores (local data) โ +โ - Competitor POIs from Overture (remote via pg_lake) โ +โ - Real-time site analysis โ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + โ๏ธ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +โ Backend API (Node.js + Express) โ +โ - Site analysis endpoints โ +โ - Spatial queries combining local + remote data โ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + โ๏ธ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +โ PostgreSQL + PostGIS + pg_lake (Remote) โ +โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ +โ โ Local Schema โ โ Overture Schema โ โ +โ โ - stores โ โ - places โ โ +โ โ - service_areasโ โ - buildings โ โ +โ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โ +โ โ๏ธ โ +โ โโโโโโโโโโโโโโโโโโโโ โ +โ โ pg_lake โ โ +โ โ (iceberg_scan) โ โ +โ โโโโโโโโโโโโโโโโโโโโ โ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ + โ๏ธ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +โ S3: Overture Maps Parquet Files โ +โ s3://overturemaps-us-west-2/release/2024-10-23.0/ โ +โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ +``` + + + +## ๐ ๏ธ Setup Instructions + +### 1. Configure Database Connection + +Copy the `.env.example` file and configure your database connection: + +```bash +cp .env.example .env +``` + +Edit `.env` and set your PostgreSQL connection string: + +```bash +DATABASE_URL=postgresql://username:password@hostname:5432/database +``` + +### 2. Initialize the Database + +First, source your environment variables: + +```bash +source .env +``` + +Then run the setup script to create schemas, tables, and load seed data: + +```bash +./setup-database.sh +``` + +**Manual Setup** (if you prefer): +```bash +source .env +psql "$DATABASE_URL" -f database/init.sql +psql "$DATABASE_URL" -f database/seed-stores.sql +``` + +### 3. Start the Backend API + +```bash +cd backend +npm install +npm run dev +``` + +The API will be available at **http://localhost:3001** + +Available Endpoints: +- `GET /health` - Health check +- `GET /api/stores` - Get all company stores (GeoJSON) +- `GET /api/service-areas` - Get service area polygons +- `POST /api/analyze-site` - Analyze a potential site location +- `GET /api/competitors` - Get nearby competitors from Overture +- `POST /api/load-overture-data` - Load Overture data via pg_lake +- `GET /api/stats` - Get data statistics + +### 4. Start the Frontend + +```bash +cd frontend +npm install +npm run dev +``` + +The app will be available at **http://localhost:5173** + +## ๐ฎ Usage + +### Basic Navigation + +1. **View Company Stores**: The map loads with your existing 24 store locations across major US cities +2. **Toggle Layers**: Use the sidebar to show/hide stores, service areas, and competitors +3. **Explore**: Click on store markers to see details (revenue, employees, location) + +### Site Analysis + +1. Click **"๐ Click Map to Analyze Site"** button +2. Click anywhere on the map +3. View instant analysis: + - Distance to nearest existing store + - Number of stores within 1 mile (cannibalization risk) + - Competitor count (from Overture Maps) + - Building density + - Average building height + +### Loading Overture Data (pg_lake) + +1. Navigate to an area of interest (e.g., zoom into a city) +2. Click **"Load Overture Data for Current View"** +3. pg_lake will query Overture Maps S3 bucket and load POIs for that area +4. Competitor locations will appear on the map + +#### Database Schema + +### Local Schema + +```sql +local.stores +โโโ id - Store ID +โโโ name - Store name +โโโ store_type - flagship | standard +โโโ annual_revenue - Annual revenue ($) +โโโ employees - Number of employees +โโโ geom - PostGIS Point (SRID 4326) +โโโ city, state - Location +โโโ opening_date - When the store opened + +local.service_areas +โโโ id - Area ID +โโโ store_id - Reference to stores +โโโ radius_miles - Service radius +โโโ geom - PostGIS Polygon (buffer) +``` + +### Overture Schema (Remote Data via pg_lake) + +```sql +overture.places +โโโ id - Overture place ID +โโโ name - Place name +โโโ category_main - Main category (restaurant, retail, etc.) +โโโ confidence - Data confidence score +โโโ geom - PostGIS Point +โโโ source_data - Raw JSONB from Overture + +overture.buildings +โโโ id - Overture building ID +โโโ height - Building height (meters) +โโโ num_floors - Number of floors +โโโ geom - PostGIS Polygon +โโโ sources - Data sources +``` + +## ๐ง Key Functions + +### `local.analyze_site(lat, lon, radius_miles)` + +Performs comprehensive site analysis combining local and remote data: + +```sql +SELECT * FROM local.analyze_site(37.7749, -122.4194, 1.0); +``` + +Returns: +- Distance to nearest store +- Store count within radius +- Competitor count within radius +- Building density +- Average building height + +### `load_overture_data(lat, lon, radius_miles)` + +Uses **pg_lake** to query Overture Maps Parquet files directly from S3 without loading them into PostgreSQL: + +```sql +SELECT load_overture_data(37.7749, -122.4194, 5.0); +``` + +**How this works with pg_lake:** + +This function demonstrates pg_lake's core capability: it uses pg_lake's foreign data wrapper to scan remote Parquet files in S3 and materialize only the relevant POIs into a local table. The key operations are: + +1. **Remote Scan**: Query Parquet files directly from `s3://overturemaps-us-west-2/` +2. **Spatial Filter**: Apply PostGIS spatial predicates during the scan to minimize data transfer +3. **Selective Materialization**: Insert only matching rows into the local `ov_places` table + +This pattern allows you to work with massive datasets in object storage while keeping your database small and queries fast. + + +## ๐ Resources + +- [pg_lake Documentation](https://github.com/paradedb/pg_lake) +- [Overture Maps](https://overturemaps.org/) +- [PostGIS Documentation](https://postgis.net/documentation/) +- [MapLibre GL JS](https://maplibre.org/maplibre-gl-js-docs/) + + + diff --git a/docs/examples/overture-maps-postgis-maplibre/backend/package-lock.json b/docs/examples/overture-maps-postgis-maplibre/backend/package-lock.json new file mode 100644 index 00000000..dbccc846 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/backend/package-lock.json @@ -0,0 +1,1003 @@ +{ + "name": "site-selection-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "site-selection-backend", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "pg": "^8.11.3" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.16.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", + "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.9.1", + "pg-pool": "^3.10.1", + "pg-protocol": "^1.10.3", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.2.7" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", + "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", + "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", + "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/docs/examples/overture-maps-postgis-maplibre/backend/package.json b/docs/examples/overture-maps-postgis-maplibre/backend/package.json new file mode 100644 index 00000000..fc933389 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/backend/package.json @@ -0,0 +1,20 @@ +{ + "name": "site-selection-backend", + "version": "1.0.0", + "description": "Backend API for Site Selection Demo", + "main": "server.js", + "type": "module", + "scripts": { + "start": "node server.js", + "dev": "node --watch server.js" + }, + "keywords": ["postgis", "pg_lake", "overture", "geospatial"], + "author": "", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2", + "pg": "^8.11.3" + } +} + diff --git a/docs/examples/overture-maps-postgis-maplibre/backend/server.js b/docs/examples/overture-maps-postgis-maplibre/backend/server.js new file mode 100644 index 00000000..72c36d8f --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/backend/server.js @@ -0,0 +1,354 @@ +import express from 'express'; +import cors from 'cors'; +import pg from 'pg'; + +const { Pool } = pg; + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Database connection +if (!process.env.DATABASE_URL) { + console.error('ERROR: DATABASE_URL environment variable is required'); + console.error('Please set DATABASE_URL to your PostgreSQL connection string'); + console.error('Example: export DATABASE_URL="postgresql://user:pass@host:5432/db"'); + process.exit(1); +} + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + max: 10, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 5000, + ssl: process.env.DATABASE_SSL === 'false' ? false : { + rejectUnauthorized: false + } +}); + +// Middleware +app.use(cors()); +app.use(express.json()); + +// Health check +app.get('/health', async (req, res) => { + try { + const result = await pool.query('SELECT NOW()'); + res.json({ + status: 'ok', + database: 'connected', + timestamp: result.rows[0].now + }); + } catch (error) { + res.status(500).json({ + status: 'error', + message: error.message + }); + } +}); + +// Get all stores +app.get('/api/stores', async (req, res) => { + try { + const result = await pool.query(` + SELECT + id, + name, + store_type, + annual_revenue, + employees, + city, + state, + ST_X(geom) as longitude, + ST_Y(geom) as latitude, + ST_AsGeoJSON(geom)::json as geometry + FROM stores + ORDER BY name + `); + + res.json({ + type: 'FeatureCollection', + features: result.rows.map(row => ({ + type: 'Feature', + id: row.id, + properties: { + name: row.name, + store_type: row.store_type, + annual_revenue: row.annual_revenue, + employees: row.employees, + city: row.city, + state: row.state + }, + geometry: row.geometry + })) + }); + } catch (error) { + console.error('Error fetching stores:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Get service areas +app.get('/api/service-areas', async (req, res) => { + try { + const result = await pool.query(` + SELECT + sa.id, + s.name as store_name, + sa.radius_miles, + ST_AsGeoJSON(sa.geom)::json as geometry + FROM service_areas sa + JOIN stores s ON sa.store_id = s.id + `); + + res.json({ + type: 'FeatureCollection', + features: result.rows.map(row => ({ + type: 'Feature', + id: row.id, + properties: { + store_name: row.store_name, + radius_miles: row.radius_miles + }, + geometry: row.geometry + })) + }); + } catch (error) { + console.error('Error fetching service areas:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Helper function to get population from Census API +async function getCensusPopulation(lat, lon, radiusMiles) { + try { + // Use Census Geocoding API to get the census tract + const geocodeUrl = `https://geocoding.geo.census.gov/geocoder/geographies/coordinates?x=${lon}&y=${lat}&benchmark=Public_AR_Current&vintage=Current_Current&format=json`; + const geocodeResponse = await fetch(geocodeUrl); + const geocodeData = await geocodeResponse.json(); + + if (!geocodeData.result?.geographies?.['Census Tracts']?.[0]) { + // Fallback: estimate based on urban density + const area1mile = Math.PI * 1 * 1; // ~3.14 sq miles + const area5mile = Math.PI * 5 * 5; // ~78.5 sq miles + const urbanDensity = 8000; // people per sq mile (moderate urban) + + return { + pop_1_mile: Math.round(area1mile * urbanDensity), + pop_5_miles: Math.round(area5mile * urbanDensity) + }; + } + + const tract = geocodeData.result.geographies['Census Tracts'][0]; + const state = tract.STATE; + const county = tract.COUNTY; + + // Get population from Census API (2020 Decennial Census) + const popUrl = `https://api.census.gov/data/2020/dec/pl?get=P1_001N,NAME&for=tract:*&in=state:${state}%20county:${county}`; + const popResponse = await fetch(popUrl); + const popData = await popResponse.json(); + + // Calculate total county population for better density estimate + let totalPop = 0; + + for (let i = 1; i < popData.length; i++) { + const pop = parseInt(popData[i][0]); + if (!isNaN(pop)) { + totalPop += pop; + } + } + + // Use higher density assumption for urban cores + // Urban areas: 15,000-25,000 people per sq mile + // We'll use a multiplier based on tract population + const avgTractPop = totalPop / (popData.length - 1); + + // Higher tract population = more urban = higher density + let densityPerSqMile; + if (avgTractPop > 6000) { + densityPerSqMile = 18000; // Dense urban (SF, NYC, Boston) + } else if (avgTractPop > 4000) { + densityPerSqMile = 12000; // Urban + } else if (avgTractPop > 2000) { + densityPerSqMile = 6000; // Suburban + } else { + densityPerSqMile = 2000; // Rural/Exurban + } + + const area1mile = Math.PI * 1 * 1; // ~3.14 sq miles + const area5mile = Math.PI * 5 * 5; // ~78.5 sq miles + + return { + pop_1_mile: Math.round(area1mile * densityPerSqMile), + pop_5_miles: Math.round(area5mile * densityPerSqMile) + }; + + } catch (error) { + console.error('Census API error:', error); + // Fallback estimates + return { + pop_1_mile: 25000, + pop_5_miles: 650000 + }; + } +} + +// Analyze a potential site +app.post('/api/analyze-site', async (req, res) => { + const { latitude, longitude, radius = 1.0 } = req.body; + + if (!latitude || !longitude) { + return res.status(400).json({ + error: 'latitude and longitude are required' + }); + } + + try { + // Get population data from Census API + const populationData = await getCensusPopulation(latitude, longitude, radius); + + // Run spatial analysis + const result = await pool.query( + 'SELECT * FROM analyze_site($1, $2, $3)', + [latitude, longitude, radius] + ); + + // Convert rows to key-value object + const analysis = {}; + result.rows.forEach(row => { + // Replace placeholder population values with real Census data + if (row.metric === 'population_1_mile') { + analysis[row.metric] = { + value: populationData.pop_1_mile, + description: 'Population within 1 mile (US Census)' + }; + } else if (row.metric === 'population_5_miles') { + analysis[row.metric] = { + value: populationData.pop_5_miles, + description: 'Population within 5 miles (US Census)' + }; + } else { + analysis[row.metric] = { + value: parseFloat(row.value), + description: row.description + }; + } + }); + + res.json({ + location: { latitude, longitude }, + radius_miles: radius, + analysis + }); + } catch (error) { + console.error('Error analyzing site:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Get nearby competitors +app.get('/api/competitors', async (req, res) => { + const { lat, lon, radius = 1.0, limit = 50 } = req.query; + + if (!lat || !lon) { + return res.status(400).json({ + error: 'lat and lon query parameters are required' + }); + } + + try { + const result = await pool.query( + `SELECT id, name, category, distance_miles, geom_json::text + FROM get_nearby_competitors($1, $2, $3, $4)`, + [parseFloat(lat), parseFloat(lon), parseFloat(radius), parseInt(limit)] + ); + + res.json({ + type: 'FeatureCollection', + features: result.rows.map(row => ({ + type: 'Feature', + id: row.id, + properties: { + name: row.name, + category: row.category, + distance_miles: parseFloat(row.distance_miles) + }, + geometry: typeof row.geom_json === 'string' ? JSON.parse(row.geom_json) : row.geom_json + })) + }); + } catch (error) { + console.error('Error fetching competitors:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Load Overture data for an area (using pg_lake) +app.post('/api/load-overture-data', async (req, res) => { + const { latitude, longitude, radius = 5.0 } = req.body; + + if (!latitude || !longitude) { + return res.status(400).json({ + error: 'latitude and longitude are required' + }); + } + + try { + const result = await pool.query( + 'SELECT load_overture_data($1, $2, $3) as message', + [latitude, longitude, radius] + ); + + res.json({ + success: true, + message: result.rows[0].message, + area: { latitude, longitude, radius_miles: radius } + }); + } catch (error) { + console.error('Error loading Overture data:', error); + res.status(500).json({ + error: error.message, + hint: 'Make sure pg_lake extension is properly configured' + }); + } +}); + +// Get statistics +app.get('/api/stats', async (req, res) => { + try { + const storeCountResult = await pool.query( + 'SELECT COUNT(*) as count FROM stores' + ); + + // Count coffee shops from ov_places (not local overture_places table) + const placeCountResult = await pool.query( + `SELECT COUNT(*) as count + FROM ov_places + WHERE basic_category = 'coffee_shop' + AND (operating_status IS NULL OR operating_status = 'open') + AND confidence > 0.7` + ); + + res.json({ + stores: parseInt(storeCountResult.rows[0].count), + overture_places: parseInt(placeCountResult.rows[0].count), + overture_buildings: 0 // Not using buildings + }); + } catch (error) { + console.error('Error fetching stats:', error); + res.status(500).json({ error: error.message }); + } +}); + +// Start server +app.listen(PORT, () => { + console.log(`๐ Site Selection API running on http://localhost:${PORT}`); + console.log(`๐ Health check: http://localhost:${PORT}/health`); + console.log(`๐ Stores endpoint: http://localhost:${PORT}/api/stores`); +}); + +// Graceful shutdown +process.on('SIGTERM', async () => { + console.log('SIGTERM received, closing database pool...'); + await pool.end(); + process.exit(0); +}); + diff --git a/docs/examples/overture-maps-postgis-maplibre/database/init.sql b/docs/examples/overture-maps-postgis-maplibre/database/init.sql new file mode 100644 index 00000000..3eb3c280 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/database/init.sql @@ -0,0 +1,336 @@ +-- Initialize PostGIS and pg_lake for Site Selection Demo +-- This script sets up the database with PostGIS, pg_lake extension, +-- and connections to Overture Maps data + +-- Enable PostGIS +CREATE EXTENSION IF NOT EXISTS postgis; +CREATE EXTENSION IF NOT EXISTS postgis_topology; + +-- Note: pg_lake installation +-- pg_lake needs to be installed separately as it's not in standard Postgres images +-- For now, we'll create placeholder functions and tables +-- In production, follow pg_lake installation: https://github.com/paradedb/pg_lake + +-- Create schema for local data +CREATE SCHEMA IF NOT EXISTS local; +CREATE SCHEMA IF NOT EXISTS overture; + +-- ============================================ +-- LOCAL DATA: Company Stores +-- ============================================ + +CREATE TABLE local.stores ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + store_type VARCHAR(50), + opening_date DATE, + annual_revenue NUMERIC(12, 2), + employees INTEGER, + geom GEOMETRY(Point, 4326), + address TEXT, + city VARCHAR(100), + state VARCHAR(50), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX stores_geom_idx ON local.stores USING GIST(geom); + +-- Create service areas (buffers around stores) +CREATE TABLE local.service_areas ( + id SERIAL PRIMARY KEY, + store_id INTEGER REFERENCES local.stores(id), + radius_miles NUMERIC(5, 2), + geom GEOMETRY(Polygon, 4326), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX service_areas_geom_idx ON local.service_areas USING GIST(geom); + +-- ============================================ +-- OVERTURE MAPS via pg_lake +-- ============================================ + +-- pg_lake allows querying Parquet files from S3 directly +-- Overture Maps 2024-10-23.0 release structure: +-- s3://overturemaps-us-west-2/release/2024-10-23.0/theme={theme}/type={type}/* + +-- Create a function to query Overture Places via pg_lake +-- Note: This uses the iceberg_scan function from pg_lake +CREATE OR REPLACE FUNCTION overture.query_places_raw( + bbox_west NUMERIC, + bbox_south NUMERIC, + bbox_east NUMERIC, + bbox_north NUMERIC +) +RETURNS TABLE( + id VARCHAR, + names JSONB, + categories JSONB, + confidence NUMERIC, + geometry JSONB, + addresses JSONB, + brands JSONB, + websites TEXT[], + phones JSONB +) AS $$ +BEGIN + -- Using pg_lake to query Overture Maps Parquet files + -- This queries the latest Overture Maps release + RETURN QUERY + SELECT + data->>'id' as id, + data->'names' as names, + data->'categories' as categories, + (data->>'confidence')::numeric as confidence, + data->'geometry' as geometry, + data->'addresses' as addresses, + data->'brands' as brands, + ARRAY[]::text[] as websites, -- placeholder + data->'phones' as phones + FROM iceberg_scan( + 's3://overturemaps-us-west-2/release/2024-10-23.0/theme=places', + aws_region => 'us-west-2', + allow_moved_paths => true + ) as data + WHERE + -- Filter by bounding box + (data->'geometry'->'coordinates'->0)::numeric BETWEEN bbox_west AND bbox_east + AND (data->'geometry'->'coordinates'->1)::numeric BETWEEN bbox_south AND bbox_north; +EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'pg_lake query failed: %. Creating fallback table.', SQLERRM; + RETURN; +END; +$$ LANGUAGE plpgsql; + +-- Create a materialized view or regular table for Places +-- We'll use a regular table and populate it from pg_lake +CREATE TABLE overture.places ( + id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255), + category_main VARCHAR(100), + category_sub VARCHAR(100), + confidence NUMERIC(3, 2), + geom GEOMETRY(Point, 4326), + addresses JSONB, + brands JSONB, + websites TEXT[], + phones TEXT[], + source_data JSONB, + loaded_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX places_geom_idx ON overture.places USING GIST(geom); +CREATE INDEX places_category_idx ON overture.places(category_main); + +-- Similar structure for Buildings +CREATE TABLE overture.buildings ( + id VARCHAR(255) PRIMARY KEY, + height NUMERIC(8, 2), + num_floors INTEGER, + building_class VARCHAR(100), + geom GEOMETRY(Polygon, 4326), + sources JSONB, + loaded_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX buildings_geom_idx ON overture.buildings USING GIST(geom); + +-- Helper function to load Overture data for a specific area +CREATE OR REPLACE FUNCTION overture.load_area_data( + center_lat NUMERIC, + center_lon NUMERIC, + radius_miles NUMERIC DEFAULT 5.0 +) +RETURNS TEXT AS $$ +DECLARE + bbox_west NUMERIC; + bbox_east NUMERIC; + bbox_south NUMERIC; + bbox_north NUMERIC; + loaded_count INTEGER := 0; +BEGIN + -- Calculate bounding box (approximate) + bbox_west := center_lon - (radius_miles / 69.0); + bbox_east := center_lon + (radius_miles / 69.0); + bbox_south := center_lat - (radius_miles / 69.0); + bbox_north := center_lat + (radius_miles / 69.0); + + -- Try to load data from pg_lake + BEGIN + INSERT INTO overture.places (id, name, category_main, geom, source_data) + SELECT + data->>'id', + data->'names'->>'primary', + data->'categories'->'main'->0->>'name', + ST_SetSRID(ST_GeomFromGeoJSON(data->'geometry'::text), 4326), + data + FROM iceberg_scan( + 's3://overturemaps-us-west-2/release/2024-10-23.0/theme=places', + aws_region => 'us-west-2', + allow_moved_paths => true + ) as data + WHERE + (data->'geometry'->'coordinates'->0)::numeric BETWEEN bbox_west AND bbox_east + AND (data->'geometry'->'coordinates'->1)::numeric BETWEEN bbox_south AND bbox_north + ON CONFLICT (id) DO NOTHING; + + GET DIAGNOSTICS loaded_count = ROW_COUNT; + + RETURN format('Loaded %s places from Overture Maps via pg_lake', loaded_count); + EXCEPTION + WHEN OTHERS THEN + RETURN format('pg_lake load failed: %s. Using sample data instead.', SQLERRM); + END; +END; +$$ LANGUAGE plpgsql; + +-- ============================================ +-- ANALYSIS FUNCTIONS +-- ============================================ + +-- Function: Analyze a potential site location +CREATE OR REPLACE FUNCTION local.analyze_site( + lat NUMERIC, + lon NUMERIC, + radius_miles NUMERIC DEFAULT 1.0 +) +RETURNS TABLE( + metric VARCHAR, + value NUMERIC, + description TEXT +) AS $$ +DECLARE + site_point GEOMETRY; + radius_meters NUMERIC; +BEGIN + -- Create point geometry + site_point := ST_SetSRID(ST_MakePoint(lon, lat), 4326); + radius_meters := radius_miles * 1609.34; -- Convert miles to meters + + -- Distance to nearest existing store + RETURN QUERY + SELECT + 'nearest_store_miles'::VARCHAR, + ROUND((ST_Distance(site_point::geography, geom::geography) / 1609.34)::numeric, 2), + 'Distance to nearest company store'::TEXT + FROM local.stores + ORDER BY site_point <-> geom + LIMIT 1; + + -- Count existing stores within radius + RETURN QUERY + SELECT + 'stores_in_radius'::VARCHAR, + COUNT(*)::NUMERIC, + format('Company stores within %s miles', radius_miles)::TEXT + FROM local.stores + WHERE ST_DWithin(site_point::geography, geom::geography, radius_meters); + + -- Count competitors (Overture POIs) within radius + RETURN QUERY + SELECT + 'competitors_in_radius'::VARCHAR, + COUNT(*)::NUMERIC, + format('Competitor POIs within %s miles', radius_miles)::TEXT + FROM overture.places + WHERE ST_DWithin(site_point::geography, geom::geography, radius_meters) + AND category_main IN ('restaurant', 'retail', 'food_and_drink'); + + -- Building density + RETURN QUERY + SELECT + 'buildings_in_radius'::VARCHAR, + COUNT(*)::NUMERIC, + format('Buildings within %s miles', radius_miles)::TEXT + FROM overture.buildings + WHERE ST_DWithin(site_point::geography, geom::geography, radius_meters); + + -- Average building height (proxy for urban density) + RETURN QUERY + SELECT + 'avg_building_height_ft'::VARCHAR, + ROUND((AVG(height) * 3.28084)::numeric, 1), + 'Average building height (feet) in area'::TEXT + FROM overture.buildings + WHERE ST_DWithin(site_point::geography, geom::geography, radius_meters) + AND height IS NOT NULL; + +END; +$$ LANGUAGE plpgsql; + +-- Function: Get competitors near a point +CREATE OR REPLACE FUNCTION local.get_nearby_competitors( + lat NUMERIC, + lon NUMERIC, + radius_miles NUMERIC DEFAULT 1.0, + limit_count INTEGER DEFAULT 50 +) +RETURNS TABLE( + id VARCHAR, + name VARCHAR, + category VARCHAR, + distance_miles NUMERIC, + geom GEOMETRY +) AS $$ +DECLARE + site_point GEOMETRY; + radius_meters NUMERIC; +BEGIN + site_point := ST_SetSRID(ST_MakePoint(lon, lat), 4326); + radius_meters := radius_miles * 1609.34; + + RETURN QUERY + SELECT + p.id, + p.name, + p.category_main, + ROUND((ST_Distance(site_point::geography, p.geom::geography) / 1609.34)::numeric, 2) as distance_miles, + p.geom + FROM overture.places p + WHERE ST_DWithin(site_point::geography, p.geom::geography, radius_meters) + AND p.category_main IN ('restaurant', 'retail', 'food_and_drink', 'commercial') + ORDER BY site_point <-> p.geom + LIMIT limit_count; +END; +$$ LANGUAGE plpgsql; + +-- ============================================ +-- VIEWS FOR MAPPING +-- ============================================ + +-- View: All stores with service areas (for vector tiles) +CREATE OR REPLACE VIEW local.stores_display AS +SELECT + s.id, + s.name, + s.store_type, + s.annual_revenue, + s.employees, + s.city || ', ' || s.state as location, + s.geom +FROM local.stores s; + +-- Store locations view for mapping +COMMENT ON VIEW local.stores_display IS 'Company store locations for mapping'; + +-- View: Service area boundaries +CREATE OR REPLACE VIEW local.service_areas_display AS +SELECT + sa.id, + s.name as store_name, + sa.radius_miles, + sa.geom +FROM local.service_areas sa +JOIN local.stores s ON sa.store_id = s.id; + +COMMENT ON VIEW local.service_areas_display IS 'Store service area boundaries'; + + +-- Grant permissions +GRANT USAGE ON SCHEMA local TO PUBLIC; +GRANT USAGE ON SCHEMA overture TO PUBLIC; +GRANT SELECT ON ALL TABLES IN SCHEMA local TO PUBLIC; +GRANT SELECT ON ALL TABLES IN SCHEMA overture TO PUBLIC; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA local TO PUBLIC; + diff --git a/docs/examples/overture-maps-postgis-maplibre/database/seed-stores.sql b/docs/examples/overture-maps-postgis-maplibre/database/seed-stores.sql new file mode 100644 index 00000000..b1e4d195 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/database/seed-stores.sql @@ -0,0 +1,130 @@ +-- Seed data for Site Selection Demo +-- Sample company stores across major US cities + +-- Insert sample stores (coffee shop chain example) +INSERT INTO local.stores (name, store_type, opening_date, annual_revenue, employees, geom, address, city, state) VALUES + -- San Francisco Bay Area + ('Downtown SF Store', 'flagship', '2018-03-15', 1250000.00, 25, ST_SetSRID(ST_MakePoint(-122.4194, 37.7749), 4326), '1 Market St', 'San Francisco', 'CA'), + ('Mission District', 'standard', '2019-06-20', 850000.00, 15, ST_SetSRID(ST_MakePoint(-122.4194, 37.7599), 4326), '3201 Mission St', 'San Francisco', 'CA'), + ('Berkeley Campus', 'standard', '2020-01-10', 680000.00, 12, ST_SetSRID(ST_MakePoint(-122.2727, 37.8716), 4326), '2600 Bancroft Way', 'Berkeley', 'CA'), + ('Palo Alto Store', 'standard', '2020-08-15', 720000.00, 14, ST_SetSRID(ST_MakePoint(-122.1430, 37.4419), 4326), '123 University Ave', 'Palo Alto', 'CA'), + ('Oakland Downtown', 'standard', '2021-03-22', 590000.00, 11, ST_SetSRID(ST_MakePoint(-122.2711, 37.8044), 4326), '456 Broadway', 'Oakland', 'CA'), + + -- Seattle + ('Seattle Pike Place', 'flagship', '2017-05-12', 1450000.00, 28, ST_SetSRID(ST_MakePoint(-122.3421, 47.6097), 4326), '1912 Pike Pl', 'Seattle', 'WA'), + ('Capitol Hill', 'standard', '2019-09-08', 780000.00, 16, ST_SetSRID(ST_MakePoint(-122.3210, 47.6205), 4326), '500 E Pine St', 'Seattle', 'WA'), + ('Fremont', 'standard', '2020-11-14', 650000.00, 13, ST_SetSRID(ST_MakePoint(-122.3493, 47.6505), 4326), '3400 Fremont Ave N', 'Seattle', 'WA'), + + -- Portland + ('Portland Downtown', 'flagship', '2018-07-20', 980000.00, 20, ST_SetSRID(ST_MakePoint(-122.6765, 45.5231), 4326), '701 SW 6th Ave', 'Portland', 'OR'), + ('Pearl District', 'standard', '2020-04-15', 720000.00, 14, ST_SetSRID(ST_MakePoint(-122.6819, 45.5266), 4326), '1201 NW Couch St', 'Portland', 'OR'), + + -- Los Angeles + ('LA Downtown', 'flagship', '2019-02-10', 1180000.00, 24, ST_SetSRID(ST_MakePoint(-118.2437, 34.0522), 4326), '444 S Flower St', 'Los Angeles', 'CA'), + ('Santa Monica', 'standard', '2019-08-25', 920000.00, 18, ST_SetSRID(ST_MakePoint(-118.4912, 34.0195), 4326), '1234 Promenade', 'Santa Monica', 'CA'), + ('Pasadena', 'standard', '2020-06-12', 670000.00, 13, ST_SetSRID(ST_MakePoint(-118.1445, 34.1478), 4326), '50 W Colorado Blvd', 'Pasadena', 'CA'), + + -- New York + ('Times Square', 'flagship', '2017-11-01', 1850000.00, 32, ST_SetSRID(ST_MakePoint(-73.9857, 40.7580), 4326), '1500 Broadway', 'New York', 'NY'), + ('Brooklyn Heights', 'standard', '2019-05-18', 890000.00, 17, ST_SetSRID(ST_MakePoint(-73.9936, 40.6955), 4326), '200 Montague St', 'Brooklyn', 'NY'), + ('Upper West Side', 'standard', '2020-03-07', 820000.00, 16, ST_SetSRID(ST_MakePoint(-73.9754, 40.7870), 4326), '2200 Broadway', 'New York', 'NY'), + + -- Boston + ('Boston Common', 'flagship', '2018-09-15', 1120000.00, 22, ST_SetSRID(ST_MakePoint(-71.0636, 42.3551), 4326), '1 Tremont St', 'Boston', 'MA'), + ('Cambridge MIT', 'standard', '2019-11-22', 750000.00, 15, ST_SetSRID(ST_MakePoint(-71.0942, 42.3601), 4326), '77 Massachusetts Ave', 'Cambridge', 'MA'), + + -- Chicago + ('Chicago Loop', 'flagship', '2018-04-20', 1280000.00, 26, ST_SetSRID(ST_MakePoint(-87.6298, 41.8781), 4326), '200 N State St', 'Chicago', 'IL'), + ('Wicker Park', 'standard', '2020-02-14', 710000.00, 14, ST_SetSRID(ST_MakePoint(-87.6771, 41.9090), 4326), '1425 N Milwaukee Ave', 'Chicago', 'IL'), + + -- Denver + ('Denver Downtown', 'flagship', '2019-07-08', 890000.00, 18, ST_SetSRID(ST_MakePoint(-104.9903, 39.7392), 4326), '1600 16th St', 'Denver', 'CO'), + ('RiNo District', 'standard', '2021-01-20', 580000.00, 12, ST_SetSRID(ST_MakePoint(-104.9823, 39.7643), 4326), '3600 Walnut St', 'Denver', 'CO'), + + -- Austin + ('Austin Downtown', 'flagship', '2019-03-30', 950000.00, 19, ST_SetSRID(ST_MakePoint(-97.7431, 30.2672), 4326), '600 Congress Ave', 'Austin', 'TX'), + ('South Congress', 'standard', '2020-09-10', 680000.00, 14, ST_SetSRID(ST_MakePoint(-97.7474, 30.2548), 4326), '1500 S Congress Ave', 'Austin', 'TX'); + +-- Create service areas (1-mile radius around each store) +INSERT INTO local.service_areas (store_id, radius_miles, geom) +SELECT + id, + 1.0, + ST_Buffer(geom::geography, 1609.34)::geometry -- 1 mile in meters +FROM local.stores; + +-- ============================================ +-- SAMPLE OVERTURE DATA +-- ============================================ +-- In production, this would come from pg_lake foreign tables +-- For demo, we'll insert some sample competitor POIs + +-- Sample competitors near San Francisco stores +INSERT INTO overture.places (id, name, category_main, category_sub, confidence, geom) VALUES + ('comp_sf_001', 'Starbucks - Market St', 'food_and_drink', 'coffee_shop', 0.95, ST_SetSRID(ST_MakePoint(-122.4184, 37.7750), 4326)), + ('comp_sf_002', 'Blue Bottle Coffee', 'food_and_drink', 'coffee_shop', 0.92, ST_SetSRID(ST_MakePoint(-122.4200, 37.7755), 4326)), + ('comp_sf_003', 'Peets Coffee', 'food_and_drink', 'coffee_shop', 0.90, ST_SetSRID(ST_MakePoint(-122.4180, 37.7745), 4326)), + ('comp_sf_004', 'Philz Coffee', 'food_and_drink', 'coffee_shop', 0.88, ST_SetSRID(ST_MakePoint(-122.4205, 37.7760), 4326)), + ('comp_sf_005', 'Starbucks - Mission', 'food_and_drink', 'coffee_shop', 0.95, ST_SetSRID(ST_MakePoint(-122.4200, 37.7605), 4326)), + ('comp_sf_006', 'Ritual Coffee', 'food_and_drink', 'coffee_shop', 0.87, ST_SetSRID(ST_MakePoint(-122.4190, 37.7595), 4326)), + + -- Seattle competitors + ('comp_sea_001', 'Starbucks Reserve', 'food_and_drink', 'coffee_shop', 0.98, ST_SetSRID(ST_MakePoint(-122.3420, 47.6105), 4326)), + ('comp_sea_002', 'Victrola Coffee', 'food_and_drink', 'coffee_shop', 0.89, ST_SetSRID(ST_MakePoint(-122.3215, 47.6210), 4326)), + ('comp_sea_003', 'Espresso Vivace', 'food_and_drink', 'coffee_shop', 0.86, ST_SetSRID(ST_MakePoint(-122.3205, 47.6200), 4326)), + + -- Portland competitors + ('comp_pdx_001', 'Stumptown Coffee', 'food_and_drink', 'coffee_shop', 0.92, ST_SetSRID(ST_MakePoint(-122.6770, 45.5235), 4326)), + ('comp_pdx_002', 'Coava Coffee', 'food_and_drink', 'coffee_shop', 0.88, ST_SetSRID(ST_MakePoint(-122.6822, 45.5270), 4326)), + + -- NYC competitors + ('comp_nyc_001', 'Starbucks - Times Sq', 'food_and_drink', 'coffee_shop', 0.96, ST_SetSRID(ST_MakePoint(-73.9860, 40.7585), 4326)), + ('comp_nyc_002', 'Bluestone Lane', 'food_and_drink', 'coffee_shop', 0.87, ST_SetSRID(ST_MakePoint(-73.9850, 40.7575), 4326)), + ('comp_nyc_003', 'Joe Coffee', 'food_and_drink', 'coffee_shop', 0.85, ST_SetSRID(ST_MakePoint(-73.9755, 40.7875), 4326)); + +-- Sample buildings (in production, these come from Overture via pg_lake) +-- Buildings need to be polygons, not points +INSERT INTO overture.buildings (id, height, num_floors, building_class, geom) VALUES + -- SF Financial District buildings (small square footprints) + ('bldg_sf_001', 235.0, 52, 'commercial', ST_Buffer(ST_SetSRID(ST_MakePoint(-122.4195, 37.7751), 4326)::geography, 20)::geometry), + ('bldg_sf_002', 180.0, 40, 'commercial', ST_Buffer(ST_SetSRID(ST_MakePoint(-122.4185, 37.7748), 4326)::geography, 20)::geometry), + ('bldg_sf_003', 25.0, 3, 'residential', ST_Buffer(ST_SetSRID(ST_MakePoint(-122.4200, 37.7752), 4326)::geography, 15)::geometry), + ('bldg_sf_004', 35.0, 4, 'mixed_use', ST_Buffer(ST_SetSRID(ST_MakePoint(-122.4190, 37.7746), 4326)::geography, 15)::geometry), + + -- Seattle downtown + ('bldg_sea_001', 290.0, 76, 'commercial', ST_Buffer(ST_SetSRID(ST_MakePoint(-122.3422, 47.6098), 4326)::geography, 25)::geometry), + ('bldg_sea_002', 45.0, 5, 'residential', ST_Buffer(ST_SetSRID(ST_MakePoint(-122.3211, 47.6206), 4326)::geography, 12)::geometry); + +-- Create some useful indexes +CREATE INDEX IF NOT EXISTS stores_city_idx ON local.stores(city); +CREATE INDEX IF NOT EXISTS stores_type_idx ON local.stores(store_type); +CREATE INDEX IF NOT EXISTS places_name_idx ON overture.places(name); + +-- Vacuum and analyze +VACUUM ANALYZE local.stores; +VACUUM ANALYZE local.service_areas; +VACUUM ANALYZE overture.places; +VACUUM ANALYZE overture.buildings; + +-- Summary output +DO $$ +DECLARE + store_count INTEGER; + place_count INTEGER; + building_count INTEGER; +BEGIN + SELECT COUNT(*) INTO store_count FROM local.stores; + SELECT COUNT(*) INTO place_count FROM overture.places; + SELECT COUNT(*) INTO building_count FROM overture.buildings; + + RAISE NOTICE '==============================================='; + RAISE NOTICE 'Site Selection Demo - Database Initialized'; + RAISE NOTICE '==============================================='; + RAISE NOTICE 'Company Stores: %', store_count; + RAISE NOTICE 'Competitor POIs: %', place_count; + RAISE NOTICE 'Buildings: %', building_count; + RAISE NOTICE '==============================================='; + RAISE NOTICE 'Run analysis: SELECT * FROM local.analyze_site(37.7749, -122.4194, 1.0);'; + RAISE NOTICE '==============================================='; +END $$; + diff --git a/docs/examples/overture-maps-postgis-maplibre/example-hybrid-query.sql b/docs/examples/overture-maps-postgis-maplibre/example-hybrid-query.sql new file mode 100644 index 00000000..ced0a094 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/example-hybrid-query.sql @@ -0,0 +1,166 @@ +-- ============================================ +-- HYBRID QUERY EXAMPLE: Local + Remote Data +-- ============================================ +-- This query joins: +-- - LOCAL DATA: Company stores (in PostGIS) +-- - REMOTE DATA: Overture Maps coffee shops (via pg_lake from S3) + +-- Example 1: Find competitor coffee shops within 1 mile of each store +SELECT + -- Local store info + s.name AS store_name, + s.city, + s.state, + + -- Remote competitor info from Overture (via pg_lake) + (competitor.names).primary AS competitor_name, + competitor.basic_category, + + -- Distance calculation + ROUND( + (ST_Distance( + s.geom::geography, + ST_SetSRID(competitor.geometry, 4326)::geography + ) / 1609.34)::numeric, + 2 + ) AS distance_miles + +FROM stores s +CROSS JOIN LATERAL ( + -- Query ov_places (remote S3 data via pg_lake) + SELECT + ov.names, + ov.basic_category, + ov.geometry + FROM ov_places ov + WHERE + -- Spatial filter: within 1 mile of this store + ST_DWithin( + ST_SetSRID(ov.geometry, 4326)::geography, + s.geom::geography, + 1609.34 -- 1 mile in meters + ) + -- Only coffee shops + AND ov.basic_category = 'coffee_shop' + -- Only operating places + AND (ov.operating_status IS NULL OR ov.operating_status = 'open') + -- High confidence data + AND ov.confidence > 0.7 + ORDER BY ST_SetSRID(ov.geometry, 4326) <-> s.geom + LIMIT 5 -- Top 5 nearest competitors per store +) competitor + +ORDER BY s.name, distance_miles; + + +-- ============================================ +-- Example 2: Count competitors by store location +-- ============================================ +SELECT + s.name AS store_name, + s.city, + s.state, + + -- Count coffee shops from Overture within 1 mile + COUNT(ov.*) AS competitors_within_1_mile + +FROM stores s +LEFT JOIN LATERAL ( + SELECT 1 + FROM ov_places ov + WHERE + ST_DWithin( + ST_SetSRID(ov.geometry, 4326)::geography, + s.geom::geography, + 1609.34 + ) + AND ov.basic_category = 'coffee_shop' + AND (ov.operating_status IS NULL OR ov.operating_status = 'open') + AND ov.confidence > 0.7 +) ov ON true + +GROUP BY s.id, s.name, s.city, s.state +ORDER BY competitors_within_1_mile DESC; + + +-- ============================================ +-- Example 3: Market saturation analysis +-- ============================================ +-- Which stores have the most competition? + +WITH competition_analysis AS ( + SELECT + s.id, + s.name, + s.city, + s.annual_revenue, + + -- Count Overture coffee shops within different radii + (SELECT COUNT(*) + FROM ov_places ov + WHERE ST_DWithin( + ST_SetSRID(ov.geometry, 4326)::geography, + s.geom::geography, + 804.67 -- 0.5 mile + ) + AND ov.basic_category = 'coffee_shop' + AND ov.confidence > 0.7 + ) AS competitors_0_5_miles, + + (SELECT COUNT(*) + FROM ov_places ov + WHERE ST_DWithin( + ST_SetSRID(ov.geometry, 4326)::geography, + s.geom::geography, + 1609.34 -- 1 mile + ) + AND ov.basic_category = 'coffee_shop' + AND ov.confidence > 0.7 + ) AS competitors_1_mile + + FROM stores s +) +SELECT + name, + city, + annual_revenue, + competitors_0_5_miles, + competitors_1_mile, + + -- Calculate market saturation score + CASE + WHEN competitors_1_mile = 0 THEN 'Low Competition' + WHEN competitors_1_mile < 5 THEN 'Moderate Competition' + WHEN competitors_1_mile < 10 THEN 'High Competition' + ELSE 'Very High Competition' + END AS market_saturation + +FROM competition_analysis +ORDER BY competitors_1_mile DESC; + + +-- ============================================ +-- KEY CONCEPTS DEMONSTRATED: +-- ============================================ +-- +-- 1. HYBRID DATA: +-- - stores (local PostGIS table) +-- - ov_places (foreign table via pg_lake โ S3 Parquet files) +-- +-- 2. SPATIAL JOINS: +-- - ST_DWithin() for radius queries +-- - ST_Distance() for exact distances +-- - CROSS JOIN LATERAL for correlated queries +-- +-- 3. REAL-TIME S3 QUERIES: +-- - Every query to ov_places hits S3 object storage +-- - pg_lake handles the Parquet โ PostGIS conversion +-- - Filters pushed down for performance +-- +-- 4. NO DATA COPYING: +-- - Overture data stays in S3 +-- - Only query what you need +-- - Always fresh data +-- +-- ============================================ + diff --git a/docs/examples/overture-maps-postgis-maplibre/frontend/index.html b/docs/examples/overture-maps-postgis-maplibre/frontend/index.html new file mode 100644 index 00000000..ab54154c --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/frontend/index.html @@ -0,0 +1,94 @@ + + +
+ + +PostGIS + pg_lake + Overture Maps + MapLibre
+Type: ${properties.store_type}
+Location: ${properties.city}, ${properties.state}
+Employees: ${properties.employees}
+Annual Revenue: $${(properties.annual_revenue / 1000000).toFixed(2)}M
+ `) + .addTo(map); + } + }); + + // Change cursor on hover + map.on('mouseenter', 'stores-layer', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + + map.on('mouseleave', 'stores-layer', () => { + map.getCanvas().style.cursor = analyzeMode ? 'crosshair' : ''; + }); + + console.log(`Loaded ${stores.length} stores`); + } catch (error) { + console.error('Error loading stores:', error); + showMapMessage('Failed to load stores', 'error'); + } +} + +// Load service areas +async function loadServiceAreas() { + try { + const response = await fetch(`${API_URL}/api/service-areas`); + const data = await response.json(); + + map.addSource('service-areas', { + type: 'geojson', + data: data + }); + + map.addLayer({ + id: 'service-areas-layer', + type: 'fill', + source: 'service-areas', + paint: { + 'fill-color': '#667eea', + 'fill-opacity': 0.1 + } + }); + + map.addLayer({ + id: 'service-areas-outline', + type: 'line', + source: 'service-areas', + paint: { + 'line-color': '#667eea', + 'line-width': 2, + 'line-dasharray': [2, 2] + } + }); + + console.log('Loaded service areas'); + } catch (error) { + console.error('Error loading service areas:', error); + } +} + +// Analyze a potential site +async function analyzeSite(lat, lng) { + try { + // Show loading message + showMapMessage('Analyzing site...', 'info'); + + // Place candidate marker + if (candidateMarker) { + candidateMarker.remove(); + } + + const el = document.createElement('div'); + el.className = 'marker marker-candidate'; + candidateMarker = new maplibregl.Marker(el) + .setLngLat([lng, lat]) + .addTo(map); + + // Fetch analysis + const response = await fetch(`${API_URL}/api/analyze-site`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ latitude: lat, longitude: lng, radius: 1.0 }) + }); + + const data = await response.json(); + displayAnalysisResults(data); + + // Load nearby competitors (0.5 mile radius for faster queries) + await loadCompetitors(lat, lng, 0.5); + + // Hide message + hideMapMessage(); + + } catch (error) { + console.error('Error analyzing site:', error); + showMapMessage('Failed to analyze site', 'error'); + } +} + +// Display analysis results +function displayAnalysisResults(data) { + const resultsDiv = document.getElementById('analysis-results'); + const contentDiv = document.getElementById('analysis-content'); + + resultsDiv.style.display = 'block'; + + let html = ''; + for (const [key, metric] of Object.entries(data.analysis)) { + html += ` +Category: ${properties.category}
+Distance: ${properties.distance_miles} miles
+ `) + .addTo(map); + } + }); + + console.log(`Loaded ${competitors.length} competitors in ${loadTime}s`); + showMapMessage(`Loaded ${competitors.length} competitors in ${loadTime}s via pg_lake`, 'info'); + setTimeout(hideMapMessage, 3000); + } catch (error) { + console.error('Error loading competitors:', error); + } +} + +// Load Overture data via pg_lake +async function loadOvertureData() { + const btn = document.getElementById('load-overture-btn'); + const statusDiv = document.getElementById('load-status'); + + btn.disabled = true; + btn.textContent = 'Loading...'; + statusDiv.className = 'status-message info'; + statusDiv.textContent = 'Querying Overture Maps via pg_lake...'; + + try { + const center = map.getCenter(); + const response = await fetch(`${API_URL}/api/load-overture-data`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + latitude: center.lat, + longitude: center.lng, + radius: 5.0 + }) + }); + + const data = await response.json(); + + if (data.success) { + statusDiv.className = 'status-message success'; + statusDiv.textContent = data.message; + + // Reload stats + await loadStats(); + } else { + throw new Error(data.error || 'Failed to load data'); + } + } catch (error) { + console.error('Error loading Overture data:', error); + statusDiv.className = 'status-message error'; + statusDiv.textContent = `Error: ${error.message}`; + } finally { + btn.disabled = false; + btn.textContent = 'Load Overture Data for Current View'; + } +} + +// Show map message +function showMapMessage(text, type = 'info') { + const messageDiv = document.getElementById('map-message'); + const textDiv = document.getElementById('map-message-text'); + textDiv.textContent = text; + messageDiv.style.display = 'block'; +} + +// Hide map message +function hideMapMessage() { + const messageDiv = document.getElementById('map-message'); + messageDiv.style.display = 'none'; +} + +// Setup event listeners +function setupEventListeners() { + // Analyze mode toggle + document.getElementById('analyze-mode-btn').addEventListener('click', () => { + analyzeMode = !analyzeMode; + const btn = document.getElementById('analyze-mode-btn'); + + if (analyzeMode) { + btn.classList.add('active'); + btn.textContent = 'โ Analysis Mode Active'; + map.getCanvas().style.cursor = 'crosshair'; + showMapMessage('Click anywhere to analyze potential site location'); + } else { + btn.classList.remove('active'); + btn.textContent = '๐ Click Map to Analyze Site'; + map.getCanvas().style.cursor = ''; + hideMapMessage(); + } + }); + + // Layer toggles + document.getElementById('toggle-stores').addEventListener('change', (e) => { + const visibility = e.target.checked ? 'visible' : 'none'; + map.setLayoutProperty('stores-layer', 'visibility', visibility); + // Labels removed - no glyphs support + // map.setLayoutProperty('stores-labels', 'visibility', visibility); + }); + + document.getElementById('toggle-service-areas').addEventListener('change', (e) => { + const visibility = e.target.checked ? 'visible' : 'none'; + map.setLayoutProperty('service-areas-layer', 'visibility', visibility); + map.setLayoutProperty('service-areas-outline', 'visibility', visibility); + }); + + document.getElementById('toggle-competitors').addEventListener('change', (e) => { + const visibility = e.target.checked ? 'visible' : 'none'; + console.log('Toggle competitors checkbox:', e.target.checked, 'visibility:', visibility); + if (map.getLayer('competitors-layer')) { + map.setLayoutProperty('competitors-layer', 'visibility', visibility); + console.log('Set competitors-layer visibility to:', visibility); + } else { + console.log('competitors-layer does not exist yet'); + } + }); + + // Load Overture data button + document.getElementById('load-overture-btn').addEventListener('click', loadOvertureData); +} + +// Start the application +init().catch(error => { + console.error('Failed to initialize application:', error); + showMapMessage('Failed to initialize application', 'error'); +}); + diff --git a/docs/examples/overture-maps-postgis-maplibre/frontend/package-lock.json b/docs/examples/overture-maps-postgis-maplibre/frontend/package-lock.json new file mode 100644 index 00000000..a91c4fba --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/frontend/package-lock.json @@ -0,0 +1,1321 @@ +{ + "name": "site-selection-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "site-selection-frontend", + "version": "1.0.0", + "dependencies": { + "maplibre-gl": "^4.1.0" + }, + "devDependencies": { + "vite": "^5.1.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "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": "0.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", + "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", + "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": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "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": "20.4.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "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": "^2.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/maplibre-gl-style-spec/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "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/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" + }, + "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/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/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/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/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ini": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "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/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "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/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.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pbf": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "ieee754": "^1.1.12", + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "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/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/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/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/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "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/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + } + } +} diff --git a/docs/examples/overture-maps-postgis-maplibre/frontend/package.json b/docs/examples/overture-maps-postgis-maplibre/frontend/package.json new file mode 100644 index 00000000..8cf9de79 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/frontend/package.json @@ -0,0 +1,18 @@ +{ + "name": "site-selection-frontend", + "version": "1.0.0", + "description": "Frontend map interface for Site Selection Demo", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "maplibre-gl": "^4.1.0" + }, + "devDependencies": { + "vite": "^5.1.0" + } +} + diff --git a/docs/examples/overture-maps-postgis-maplibre/frontend/style.css b/docs/examples/overture-maps-postgis-maplibre/frontend/style.css new file mode 100644 index 00000000..18bc81bc --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/frontend/style.css @@ -0,0 +1,388 @@ +/* Reset and base styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Layout */ +#app { + display: grid; + grid-template-areas: + "header header" + "sidebar map"; + grid-template-rows: auto 1fr; + grid-template-columns: 360px 1fr; + height: 100vh; + overflow: hidden; +} + +/* Header */ +.header { + grid-area: header; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 1.5rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); +} + +.header-content h1 { + font-size: 1.75rem; + font-weight: 700; + margin-bottom: 0.25rem; +} + +.subtitle { + font-size: 0.875rem; + opacity: 0.9; +} + +.header-stats { + display: flex; + gap: 2rem; +} + +.stat { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.stat-label { + font-size: 0.75rem; + opacity: 0.8; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.stat-value { + font-size: 1.5rem; + font-weight: 700; +} + +/* Sidebar */ +.sidebar { + grid-area: sidebar; + background: #f8f9fa; + border-right: 1px solid #e0e0e0; + overflow-y: auto; + padding: 1.5rem; +} + +.sidebar-section { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e0e0e0; +} + +.sidebar-section:last-child { + border-bottom: none; +} + +.sidebar-section h2 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; + color: #333; +} + +/* Layer toggles */ +.layer-toggle { + display: flex; + align-items: center; + padding: 0.75rem; + margin-bottom: 0.5rem; + background: white; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s; + border: 1px solid #e0e0e0; +} + +.layer-toggle:hover { + border-color: #667eea; + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.1); +} + +.layer-toggle input[type="checkbox"] { + margin-right: 0.75rem; + width: 18px; + height: 18px; + cursor: pointer; +} + +.layer-toggle span { + font-size: 0.95rem; + font-weight: 500; +} + +/* Buttons */ +.btn { + width: 100%; + padding: 0.875rem 1.25rem; + border: none; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-primary.active { + background: #28a745; +} + +.btn-secondary { + background: #4CAF50; + color: white; +} + +.btn-secondary:hover { + background: #45a049; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4); +} + +.btn-secondary:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; +} + +/* Hints and messages */ +.hint { + font-size: 0.85rem; + color: #666; + margin-top: 0.75rem; + line-height: 1.4; +} + +.status-message { + margin-top: 0.75rem; + padding: 0.75rem; + border-radius: 6px; + font-size: 0.875rem; + display: none; +} + +.status-message.success { + display: block; + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; +} + +.status-message.error { + display: block; + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; +} + +.status-message.info { + display: block; + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; +} + +/* Analysis results */ +#analysis-content { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.analysis-metric { + background: white; + padding: 1rem; + border-radius: 8px; + border-left: 4px solid #667eea; +} + +.analysis-metric-label { + font-size: 0.8rem; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.25rem; +} + +.analysis-metric-value { + font-size: 1.5rem; + font-weight: 700; + color: #333; +} + +.analysis-metric-desc { + font-size: 0.85rem; + color: #666; + margin-top: 0.25rem; +} + +/* About section */ +.about-text { + font-size: 0.875rem; + line-height: 1.6; + color: #555; +} + +.about-text ul { + margin-top: 0.75rem; + padding-left: 1.25rem; +} + +.about-text li { + margin-bottom: 0.5rem; +} + +.about-text strong { + color: #333; +} + +/* Map */ +.map-container { + grid-area: map; + position: relative; + background: #e5e3df; +} + +#map { + width: 100%; + height: 100%; +} + +.map-overlay { + position: absolute; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: rgba(255, 255, 255, 0.95); + padding: 1rem 1.5rem; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 10; + font-weight: 600; +} + +/* MapLibre GL JS styles */ +@import 'maplibre-gl/dist/maplibre-gl.css'; + +/* Custom popup styles */ +.maplibregl-popup-content { + padding: 1rem; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); +} + +.maplibregl-popup-content h3 { + margin: 0 0 0.5rem 0; + font-size: 1rem; + color: #333; +} + +.maplibregl-popup-content p { + margin: 0.25rem 0; + font-size: 0.875rem; + color: #666; +} + +.popup-revenue { + color: #28a745; + font-weight: 600; +} + +/* Marker styles */ +.marker { + background-size: cover; + width: 30px; + height: 30px; + cursor: pointer; + transition: transform 0.2s; +} + +.marker:hover { + transform: scale(1.2); +} + +.marker-store { + background-color: #667eea; + border: 3px solid white; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); +} + +.marker-competitor { + background-color: #ff6b6b; + border: 3px solid white; + border-radius: 50%; + box-shadow: 0 2px 8px rgba(0,0,0,0.3); +} + +.marker-candidate { + background-color: #feca57; + border: 3px solid white; + border-radius: 50%; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +/* Responsive design */ +@media (max-width: 1024px) { + #app { + grid-template-columns: 300px 1fr; + } +} + +@media (max-width: 768px) { + #app { + grid-template-areas: + "header" + "map"; + grid-template-columns: 1fr; + } + + .sidebar { + display: none; /* Could be toggled on mobile */ + } + + .header-stats { + display: none; + } +} + diff --git a/docs/examples/overture-maps-postgis-maplibre/frontend/vite.config.js b/docs/examples/overture-maps-postgis-maplibre/frontend/vite.config.js new file mode 100644 index 00000000..2aa30b97 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/frontend/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + server: { + port: 5173, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +}); + diff --git a/docs/examples/overture-maps-postgis-maplibre/setup-database.sh b/docs/examples/overture-maps-postgis-maplibre/setup-database.sh new file mode 100755 index 00000000..c3eed437 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/setup-database.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Setup script for Site Selection Demo database +# This script initializes the database schema and loads seed data + +set -e + +# Get database URL from environment variable +DB_URL="${DATABASE_URL}" + +if [ -z "$DB_URL" ]; then + echo "โ ERROR: DATABASE_URL environment variable is not set" + echo "" + echo "Please set DATABASE_URL to your PostgreSQL connection string:" + echo " export DATABASE_URL='postgresql://user:password@host:5432/database'" + echo "" + echo "Or copy .env.example to .env and configure it, then run:" + echo " source .env" + exit 1 +fi + +echo "==========================================" +echo "Site Selection Demo - Database Setup" +echo "==========================================" +echo "" + +# Check if psql is installed +if ! command -v psql &> /dev/null; then + echo "โ psql not found. Please install PostgreSQL client." + echo " macOS: brew install postgresql" + echo " Ubuntu: sudo apt-get install postgresql-client" + exit 1 +fi + +echo "โ psql found" +echo "" + +# Test database connection +echo "Testing database connection..." +if psql "$DB_URL" -c "SELECT version();" > /dev/null 2>&1; then + echo "โ Database connection successful" +else + echo "โ Cannot connect to database" + exit 1 +fi +echo "" + +# Check for pg_lake extension +echo "Checking for pg_lake extension..." +if psql "$DB_URL" -tAc "SELECT 1 FROM pg_extension WHERE extname = 'pg_lake';" | grep -q 1; then + echo "โ pg_lake extension is installed" +else + echo "โ ๏ธ pg_lake extension not found" + echo " The database might not have pg_lake installed yet." + echo " Some features may not work until pg_lake is available." +fi +echo "" + +# Run initialization script +echo "Running database initialization..." +psql "$DB_URL" -f database/init.sql +if [ $? -eq 0 ]; then + echo "โ Database schema created" +else + echo "โ Failed to create database schema" + exit 1 +fi +echo "" + +# Load seed data +echo "Loading seed data..." +psql "$DB_URL" -f database/seed-stores.sql +if [ $? -eq 0 ]; then + echo "โ Seed data loaded" +else + echo "โ Failed to load seed data" + exit 1 +fi +echo "" + +# Summary +echo "==========================================" +echo "โ Database setup complete!" +echo "==========================================" +echo "" +echo "Quick test queries:" +echo "" +echo "1. List all stores:" +echo " psql \"$DB_URL\" -c \"SELECT name, city, state FROM local.stores;\"" +echo "" +echo "2. Run site analysis (San Francisco):" +echo " psql \"$DB_URL\" -c \"SELECT * FROM local.analyze_site(37.7749, -122.4194, 1.0);\"" +echo "" +echo "3. Find nearby competitors:" +echo " psql \"$DB_URL\" -c \"SELECT name, category, distance_miles FROM local.get_nearby_competitors(37.7749, -122.4194, 1.0);\"" +echo "" +echo "==========================================" + diff --git a/docs/examples/overture-maps-postgis-maplibre/start-demo.sh b/docs/examples/overture-maps-postgis-maplibre/start-demo.sh new file mode 100755 index 00000000..48efb47b --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/start-demo.sh @@ -0,0 +1,115 @@ +#!/bin/bash + +# Quick start script for Site Selection Demo + +set -e + +echo "==========================================" +echo "๐ฏ Site Selection Demo - Quick Start" +echo "==========================================" +echo "" + +# Check prerequisites +echo "Checking prerequisites..." + +if ! command -v node &> /dev/null; then + echo "โ Node.js not found. Please install Node.js 18+" + exit 1 +fi +echo "โ Node.js found: $(node --version)" + +if ! command -v npm &> /dev/null; then + echo "โ npm not found" + exit 1 +fi +echo "โ npm found: $(npm --version)" + +if ! command -v psql &> /dev/null; then + echo "โ ๏ธ psql not found. Database setup will be skipped." + echo " Install PostgreSQL client: brew install postgresql" + SKIP_DB=true +else + echo "โ psql found" + SKIP_DB=false +fi + +echo "" + +# Database setup +if [ "$SKIP_DB" = false ]; then + echo "==========================================" + echo "๐ Setting up database..." + echo "==========================================" + + if [ ! -f ".db-initialized" ]; then + ./setup-database.sh + touch .db-initialized + echo "โ Database initialized" + else + echo "โ Database already initialized (delete .db-initialized to reinit)" + fi + echo "" +fi + +# Install and start backend +echo "==========================================" +echo "๐ง Starting backend API..." +echo "==========================================" +cd backend +if [ ! -d "node_modules" ]; then + echo "Installing backend dependencies..." + npm install +fi +echo "โ Backend ready" + +# Start backend in background +echo "Starting backend server..." +npm start > ../backend.log 2>&1 & +BACKEND_PID=$! +echo $BACKEND_PID > ../backend.pid +echo "โ Backend running on http://localhost:3001 (PID: $BACKEND_PID)" +cd .. +echo "" + +# Install and start frontend +echo "==========================================" +echo "๐จ Starting frontend..." +echo "==========================================" +cd frontend +if [ ! -d "node_modules" ]; then + echo "Installing frontend dependencies..." + npm install +fi +echo "โ Frontend ready" +cd .. +echo "" + +# Wait for backend to be ready +echo "Waiting for backend to be ready..." +for i in {1..30}; do + if curl -s http://localhost:3001/health > /dev/null 2>&1; then + echo "โ Backend is healthy" + break + fi + if [ $i -eq 30 ]; then + echo "โ ๏ธ Backend health check timeout" + fi + sleep 1 +done + +echo "" +echo "==========================================" +echo "โ Demo is ready!" +echo "==========================================" +echo "" +echo "๐ Frontend: http://localhost:5173" +echo "๐ง Backend API: http://localhost:3001" +echo "" +echo "To start the frontend (in a new terminal):" +echo " cd frontend && npm run dev" +echo "" +echo "To stop everything:" +echo " ./stop-demo.sh" +echo "" +echo "==========================================" + diff --git a/docs/examples/overture-maps-postgis-maplibre/stop-demo.sh b/docs/examples/overture-maps-postgis-maplibre/stop-demo.sh new file mode 100755 index 00000000..7d7be915 --- /dev/null +++ b/docs/examples/overture-maps-postgis-maplibre/stop-demo.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +# Stop script for Site Selection Demo + +echo "==========================================" +echo "๐ Stopping Site Selection Demo" +echo "==========================================" +echo "" + +# Stop backend +if [ -f "backend.pid" ]; then + BACKEND_PID=$(cat backend.pid) + if ps -p $BACKEND_PID > /dev/null 2>&1; then + echo "Stopping backend (PID: $BACKEND_PID)..." + kill $BACKEND_PID + rm backend.pid + echo "โ Backend stopped" + else + echo "Backend process not running" + rm backend.pid + fi +else + echo "No backend PID file found" +fi + +echo "" +echo "==========================================" +echo "โ Demo stopped" +echo "==========================================" +