first map setup

This commit is contained in:
2026-01-20 22:00:49 +01:00
commit 1f2601a1ed
12 changed files with 1518 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use nix

52
.gitignore vendored Normal file
View File

@@ -0,0 +1,52 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Temporary files, for example, from tests.
/tmp/
# Ignore package tarball (built via "mix hex.build").
docmark-*.tar
docs/*.pdf
data
# Ignore assets that are produced by build tools.
/priv/static/assets/
/priv/static/*.js
/priv/static/*.html
/priv/static/*.css
# Ignore digested assets cache.
/priv/static/cache_manifest.json
# In case you use Node.js/npm, you want to ignore these.
npm-debug.log
/assets/node_modules/
/ui/node_modules/
/ui/.vite/
*.sublime*
postgres
.direnv
# Generated code:
ui/src/types/api.ts

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
laspy>=2.0.0
scipy>=1.9.0
numpy>=1.21.0
pyproj>=3.0.0

71
shell.nix Normal file
View File

@@ -0,0 +1,71 @@
# shell.nix
/*
# Dev env for elixir
## Environment Variables
- MIX_HOME: Local Mix installation
- HEX_HOME: Local Hex package manager
- PGDIR: PostgreSQL data directory
- PGHOST: Unix domain socket location
- DATABASE_URL: PostgreSQL connection string
*/
let
# Import nixos-unstable for most packages
pkgs = import <nixpkgs> {};
# Import nixpkgs-unstable for outliers
pkgs-unstable = import <nixpkgs-unstable> {};
extraPackages = [
pkgs.beam.packages.erlang_27.elixir_1_18
pkgs.beam.packages.erlang_27.erlang
pkgs.elixir_ls
pkgs.inotify-tools
# DB
pkgs.postgresql
# JS
pkgs-unstable.deno
# PY
pkgs.python3
pkgs.python3Packages.laspy
pkgs.python3Packages.scipy
pkgs.python3Packages.numpy
pkgs.python3Packages.pyproj
];
mkShell = pkgs.mkShell;
PROJECT_ROOT = builtins.toString ./.;
hooks = ''
# Set up environment variables
# PostgreSQL configuration
export PGDIR=${PROJECT_ROOT}/postgres
export PGHOST=$PGDIR
export PGDATA=$PGDIR/data
export PGLOG=$PGDIR/log
export DATABASE_URL="postgresql:///postgres?host=$PGDIR"
# Create PostgreSQL directories if they don't exist
if test ! -d $PGDIR; then
mkdir $PGDIR
fi
if [ ! -d $PGDATA ]; then
echo 'Initializing postgresql database...'
initdb $PGDATA --auth=trust >/dev/null
fi
# Print setup instructions
echo "Elixir development environment ready!"
echo "To start PostgreSQL, run: postgres -h \'\' -k \'$PGHOST\'"
echo "To start the project, run : iex -S mix phx.server"
'';
in mkShell {
buildInputs = extraPackages;
shellHook = hooks;
}

159
tooling/las2mound.py Normal file
View File

@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
Convert LAS lidar files to .mound binary format for Three.js rendering.
Usage:
python las_to_mound.py input.las output.mound
"""
import sys
import struct
import numpy as np
from pathlib import Path
try:
import laspy
except ImportError:
print("Error: laspy not installed. Run: pip install laspy")
sys.exit(1)
try:
from scipy.spatial import Delaunay
except ImportError:
print("Error: scipy not installed. Run: pip install scipy")
sys.exit(1)
try:
from pyproj import Transformer
except ImportError:
print("Error: pyproj not installed. Run: pip install pyproj")
sys.exit(1)
def transform_to_latlon(x, y, z):
"""Transform Ohio State Plane coordinates to lat/lon."""
print("Transforming coordinates to lat/lon...")
# Ohio State Plane South (EPSG:3735) in US Survey Feet to WGS84 (EPSG:4326)
# Note: Ohio has two zones - North (3734) and South (3735)
# Newark is in the North zone
transformer = Transformer.from_crs("EPSG:3734", "EPSG:4326", always_xy=True)
# Transform x,y (easting, northing) to lon, lat
lon, lat = transformer.transform(x, y)
# Convert elevation from US Survey Feet to meters
z_meters = z * 0.3048006096012192
print(f"Transformed to lat/lon bounds: lon[{lon.min():.6f}, {lon.max():.6f}] lat[{lat.min():.6f}, {lat.max():.6f}]")
return lon, lat, z_meters
def read_las(filepath):
"""Read LAS file and extract ground points."""
print(f"Reading {filepath}...")
las = laspy.read(filepath)
# Extract coordinates
x = las.x
y = las.y
z = las.z
# Filter for ground points (classification 2) if available
if hasattr(las, 'classification'):
ground_mask = las.classification == 2
if ground_mask.any():
print(f"Filtering {ground_mask.sum()} ground points from {len(x)} total points")
x = x[ground_mask]
y = y[ground_mask]
z = z[ground_mask]
print(f"Loaded {len(x)} points")
return x, y, z
def triangulate_points(x, y, z):
"""Perform Delaunay triangulation on XY coordinates."""
print("Performing Delaunay triangulation...")
# Create 2D point array for triangulation
points_2d = np.column_stack([x, y])
# Triangulate
tri = Delaunay(points_2d)
print(f"Generated {len(tri.simplices)} triangles")
return tri.simplices
def write_mound(filepath, x, y, z, indices):
"""Write .mound binary format."""
print(f"Writing {filepath}...")
# Convert to float32
positions = np.column_stack([x, y, z]).astype(np.float32)
indices = indices.astype(np.uint32)
# Calculate bounds
min_x, min_y, min_z = positions.min(axis=0)
max_x, max_y, max_z = positions.max(axis=0)
point_count = len(positions)
triangle_count = len(indices)
with open(filepath, 'wb') as f:
# Header (64 bytes)
f.write(b'LIDR') # Magic number (4 bytes)
f.write(struct.pack('I', 1)) # Version (4 bytes)
f.write(struct.pack('I', point_count)) # Point count (4 bytes)
f.write(struct.pack('I', triangle_count)) # Triangle count (4 bytes)
f.write(struct.pack('f', min_x)) # Min X (4 bytes)
f.write(struct.pack('f', min_y)) # Min Y (4 bytes)
f.write(struct.pack('f', min_z)) # Min Z (4 bytes)
f.write(struct.pack('f', max_x)) # Max X (4 bytes)
f.write(struct.pack('f', max_y)) # Max Y (4 bytes)
f.write(struct.pack('f', max_z)) # Max Z (4 bytes)
f.write(b'\x00' * 24) # Reserved (24 bytes)
# Position data
f.write(positions.tobytes())
# Index data
f.write(indices.tobytes())
file_size = Path(filepath).stat().st_size / (1024 * 1024)
print(f"Wrote {point_count} points, {triangle_count} triangles ({file_size:.2f} MB)")
print(f"Bounds: X[{min_x:.2f}, {max_x:.2f}] Y[{min_y:.2f}, {max_y:.2f}] Z[{min_z:.2f}, {max_z:.2f}]")
def main():
if len(sys.argv) != 3:
print("Usage: python las_to_mound.py input.las output.mound")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2]
if not Path(input_file).exists():
print(f"Error: Input file '{input_file}' not found")
sys.exit(1)
# Read LAS
x, y, z = read_las(input_file)
# Transform to lat/lon
lon, lat, z_meters = transform_to_latlon(x, y, z)
# Triangulate (using lon/lat as x/y)
indices = triangulate_points(lon, lat, z_meters)
# Write output (lon as x, lat as y, elevation as z)
write_mound(output_file, lon, lat, z_meters, indices)
print("Done!")
if __name__ == '__main__':
main()

17
ui/deno.json Normal file
View File

@@ -0,0 +1,17 @@
{
"imports": {
"@vitejs/plugin-vue": "npm:@vitejs/plugin-vue@^5",
"maplibre-gl": "npm:maplibre-gl@^5.16.0",
"three": "npm:three@^0.182.0",
"vite": "npm:vite@^7.2.1",
"vue": "npm:vue@^3.5.23",
"pinia": "npm:pinia@^3.0.4",
},
"tasks": {
"dev": "deno run -A --node-modules-dir npm:vite",
"build": "deno run -A --node-modules-dir npm:vite build",
"watch": "deno run -A --node-modules-dir npm:vite build --watch",
"preview": "deno run -A --node-modules-dir npm:vite preview",
},
"nodeModulesDir": "auto"
}

745
ui/deno.lock generated Normal file
View File

@@ -0,0 +1,745 @@
{
"version": "5",
"specifiers": {
"npm:@vitejs/plugin-vue@5": "5.2.4_vite@7.2.1__picomatch@4.0.3_vue@3.5.23",
"npm:maplibre-gl@^5.16.0": "5.16.0",
"npm:pinia@^3.0.4": "3.0.4_vue@3.5.23",
"npm:three@0.182": "0.182.0",
"npm:vite@*": "7.2.1_picomatch@4.0.3",
"npm:vite@^7.2.1": "7.2.1_picomatch@4.0.3",
"npm:vue@^3.5.23": "3.5.23"
},
"npm": {
"@babel/helper-string-parser@7.27.1": {
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
},
"@babel/helper-validator-identifier@7.28.5": {
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="
},
"@babel/parser@7.28.5": {
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dependencies": [
"@babel/types"
],
"bin": true
},
"@babel/types@7.28.5": {
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dependencies": [
"@babel/helper-string-parser",
"@babel/helper-validator-identifier"
]
},
"@esbuild/aix-ppc64@0.25.12": {
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"os": ["aix"],
"cpu": ["ppc64"]
},
"@esbuild/android-arm64@0.25.12": {
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"os": ["android"],
"cpu": ["arm64"]
},
"@esbuild/android-arm@0.25.12": {
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"os": ["android"],
"cpu": ["arm"]
},
"@esbuild/android-x64@0.25.12": {
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"os": ["android"],
"cpu": ["x64"]
},
"@esbuild/darwin-arm64@0.25.12": {
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"os": ["darwin"],
"cpu": ["arm64"]
},
"@esbuild/darwin-x64@0.25.12": {
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@esbuild/freebsd-arm64@0.25.12": {
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"os": ["freebsd"],
"cpu": ["arm64"]
},
"@esbuild/freebsd-x64@0.25.12": {
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@esbuild/linux-arm64@0.25.12": {
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@esbuild/linux-arm@0.25.12": {
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"os": ["linux"],
"cpu": ["arm"]
},
"@esbuild/linux-ia32@0.25.12": {
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"os": ["linux"],
"cpu": ["ia32"]
},
"@esbuild/linux-loong64@0.25.12": {
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"os": ["linux"],
"cpu": ["loong64"]
},
"@esbuild/linux-mips64el@0.25.12": {
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"os": ["linux"],
"cpu": ["mips64el"]
},
"@esbuild/linux-ppc64@0.25.12": {
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"os": ["linux"],
"cpu": ["ppc64"]
},
"@esbuild/linux-riscv64@0.25.12": {
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@esbuild/linux-s390x@0.25.12": {
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"os": ["linux"],
"cpu": ["s390x"]
},
"@esbuild/linux-x64@0.25.12": {
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"os": ["linux"],
"cpu": ["x64"]
},
"@esbuild/netbsd-arm64@0.25.12": {
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"os": ["netbsd"],
"cpu": ["arm64"]
},
"@esbuild/netbsd-x64@0.25.12": {
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"os": ["netbsd"],
"cpu": ["x64"]
},
"@esbuild/openbsd-arm64@0.25.12": {
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"os": ["openbsd"],
"cpu": ["arm64"]
},
"@esbuild/openbsd-x64@0.25.12": {
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"os": ["openbsd"],
"cpu": ["x64"]
},
"@esbuild/openharmony-arm64@0.25.12": {
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"os": ["openharmony"],
"cpu": ["arm64"]
},
"@esbuild/sunos-x64@0.25.12": {
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"os": ["sunos"],
"cpu": ["x64"]
},
"@esbuild/win32-arm64@0.25.12": {
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@esbuild/win32-ia32@0.25.12": {
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@esbuild/win32-x64@0.25.12": {
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"os": ["win32"],
"cpu": ["x64"]
},
"@jridgewell/sourcemap-codec@1.5.5": {
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="
},
"@mapbox/geojson-rewind@0.5.2": {
"integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
"dependencies": [
"get-stream",
"minimist"
],
"bin": true
},
"@mapbox/jsonlint-lines-primitives@2.0.2": {
"integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ=="
},
"@mapbox/point-geometry@1.1.0": {
"integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ=="
},
"@mapbox/tiny-sdf@2.0.7": {
"integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug=="
},
"@mapbox/unitbezier@0.0.1": {
"integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw=="
},
"@mapbox/vector-tile@2.0.4": {
"integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==",
"dependencies": [
"@mapbox/point-geometry",
"@types/geojson",
"pbf"
]
},
"@mapbox/whoots-js@3.1.0": {
"integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="
},
"@maplibre/maplibre-gl-style-spec@24.4.1": {
"integrity": "sha512-UKhA4qv1h30XT768ccSv5NjNCX+dgfoq2qlLVmKejspPcSQTYD4SrVucgqegmYcKcmwf06wcNAa/kRd0NHWbUg==",
"dependencies": [
"@mapbox/jsonlint-lines-primitives",
"@mapbox/unitbezier",
"json-stringify-pretty-compact",
"minimist",
"quickselect",
"rw",
"tinyqueue"
],
"bin": true
},
"@maplibre/mlt@1.1.2": {
"integrity": "sha512-SQKdJ909VGROkA6ovJgtHNs9YXV4YXUPS+VaZ50I2Mt951SLlUm2Cv34x5Xwc1HiFlsd3h2Yrs5cn7xzqBmENw==",
"dependencies": [
"@mapbox/point-geometry"
]
},
"@maplibre/vt-pbf@4.2.0": {
"integrity": "sha512-bxrk/kQUwWXZgmqYgwOCnZCMONCRi3MJMqJdza4T3E4AeR5i+VyMnaJ8iDWtWxdfEAJRtrzIOeJtxZSy5mFrFA==",
"dependencies": [
"@mapbox/point-geometry",
"@mapbox/vector-tile",
"@types/geojson-vt",
"@types/supercluster",
"geojson-vt",
"pbf",
"supercluster"
]
},
"@rollup/rollup-android-arm-eabi@4.52.5": {
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
"os": ["android"],
"cpu": ["arm"]
},
"@rollup/rollup-android-arm64@4.52.5": {
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
"os": ["android"],
"cpu": ["arm64"]
},
"@rollup/rollup-darwin-arm64@4.52.5": {
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
"os": ["darwin"],
"cpu": ["arm64"]
},
"@rollup/rollup-darwin-x64@4.52.5": {
"integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
"os": ["darwin"],
"cpu": ["x64"]
},
"@rollup/rollup-freebsd-arm64@4.52.5": {
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
"os": ["freebsd"],
"cpu": ["arm64"]
},
"@rollup/rollup-freebsd-x64@4.52.5": {
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
"os": ["freebsd"],
"cpu": ["x64"]
},
"@rollup/rollup-linux-arm-gnueabihf@4.52.5": {
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
"os": ["linux"],
"cpu": ["arm"]
},
"@rollup/rollup-linux-arm-musleabihf@4.52.5": {
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
"os": ["linux"],
"cpu": ["arm"]
},
"@rollup/rollup-linux-arm64-gnu@4.52.5": {
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@rollup/rollup-linux-arm64-musl@4.52.5": {
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
"os": ["linux"],
"cpu": ["arm64"]
},
"@rollup/rollup-linux-loong64-gnu@4.52.5": {
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
"os": ["linux"],
"cpu": ["loong64"]
},
"@rollup/rollup-linux-ppc64-gnu@4.52.5": {
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
"os": ["linux"],
"cpu": ["ppc64"]
},
"@rollup/rollup-linux-riscv64-gnu@4.52.5": {
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@rollup/rollup-linux-riscv64-musl@4.52.5": {
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
"os": ["linux"],
"cpu": ["riscv64"]
},
"@rollup/rollup-linux-s390x-gnu@4.52.5": {
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
"os": ["linux"],
"cpu": ["s390x"]
},
"@rollup/rollup-linux-x64-gnu@4.52.5": {
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
"os": ["linux"],
"cpu": ["x64"]
},
"@rollup/rollup-linux-x64-musl@4.52.5": {
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
"os": ["linux"],
"cpu": ["x64"]
},
"@rollup/rollup-openharmony-arm64@4.52.5": {
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
"os": ["openharmony"],
"cpu": ["arm64"]
},
"@rollup/rollup-win32-arm64-msvc@4.52.5": {
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
"os": ["win32"],
"cpu": ["arm64"]
},
"@rollup/rollup-win32-ia32-msvc@4.52.5": {
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
"os": ["win32"],
"cpu": ["ia32"]
},
"@rollup/rollup-win32-x64-gnu@4.52.5": {
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
"os": ["win32"],
"cpu": ["x64"]
},
"@rollup/rollup-win32-x64-msvc@4.52.5": {
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
"os": ["win32"],
"cpu": ["x64"]
},
"@types/estree@1.0.8": {
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="
},
"@types/geojson-vt@3.2.5": {
"integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
"dependencies": [
"@types/geojson"
]
},
"@types/geojson@7946.0.16": {
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
},
"@types/supercluster@7.1.3": {
"integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
"dependencies": [
"@types/geojson"
]
},
"@vitejs/plugin-vue@5.2.4_vite@7.2.1__picomatch@4.0.3_vue@3.5.23": {
"integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==",
"dependencies": [
"vite",
"vue"
]
},
"@vue/compiler-core@3.5.23": {
"integrity": "sha512-nW7THWj5HOp085ROk65LwaoxuzDsjIxr485F4iu63BoxsXoSqKqmsUUoP4A7Gl67DgIgi0zJ8JFgHfvny/74MA==",
"dependencies": [
"@babel/parser",
"@vue/shared",
"entities",
"estree-walker",
"source-map-js"
]
},
"@vue/compiler-dom@3.5.23": {
"integrity": "sha512-AT8RMw0vEzzzO0JU5gY0F6iCzaWUIh/aaRVordzMBKXRpoTllTT4kocHDssByPsvodNCfump/Lkdow2mT/O5KQ==",
"dependencies": [
"@vue/compiler-core",
"@vue/shared"
]
},
"@vue/compiler-sfc@3.5.23": {
"integrity": "sha512-3QTEUo4qg7FtQwaDJa8ou1CUikx5WTtZlY61rRRDu3lK2ZKrGoAGG8mvDgOpDsQ4A1bez9s+WtBB6DS2KuFCPw==",
"dependencies": [
"@babel/parser",
"@vue/compiler-core",
"@vue/compiler-dom",
"@vue/compiler-ssr",
"@vue/shared",
"estree-walker",
"magic-string",
"postcss",
"source-map-js"
]
},
"@vue/compiler-ssr@3.5.23": {
"integrity": "sha512-Hld2xphbMjXs9Q9WKxPf2EqmE+Rq/FEDnK/wUBtmYq74HCV4XDdSCheAaB823OQXIIFGq9ig/RbAZkF9s4U0Ow==",
"dependencies": [
"@vue/compiler-dom",
"@vue/shared"
]
},
"@vue/devtools-api@7.7.7": {
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"dependencies": [
"@vue/devtools-kit"
]
},
"@vue/devtools-kit@7.7.7": {
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"dependencies": [
"@vue/devtools-shared",
"birpc",
"hookable",
"mitt",
"perfect-debounce",
"speakingurl",
"superjson"
]
},
"@vue/devtools-shared@7.7.7": {
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"dependencies": [
"rfdc"
]
},
"@vue/reactivity@3.5.23": {
"integrity": "sha512-ji5w0qvrPyBmBx5Ldv4QGNsw0phgRreEvjt0iUf1lei2Sm8//9ZAi78uM2ZjsT5gk0YZilLuoRCIMvtuZlHMJw==",
"dependencies": [
"@vue/shared"
]
},
"@vue/runtime-core@3.5.23": {
"integrity": "sha512-LMB0S6/G7mFJcpQeQaZrbsthFbWrIX8FVTzu5x9U3Ec8YW5MY1CGAnBBHNj+TPOBu3pIbtPpjrXtcaN04X+aBw==",
"dependencies": [
"@vue/reactivity",
"@vue/shared"
]
},
"@vue/runtime-dom@3.5.23": {
"integrity": "sha512-r/PYc8W9THzEL0UExpTkV+d31zO+Jid/RMZIDG6aS/NekOEUHuCJkJgftySWZw7JTJO/+q9Kxkg8p+i7Q7Q+ew==",
"dependencies": [
"@vue/reactivity",
"@vue/runtime-core",
"@vue/shared",
"csstype"
]
},
"@vue/server-renderer@3.5.23_vue@3.5.23": {
"integrity": "sha512-NiWZsNCsXA20/VufcrW5u+Trt/PyFlpMmxaB2KERYM8eZgUoKUjXxJQb9ypq+LZ0Sp3XHJGNBR8DkhRnkKAMUw==",
"dependencies": [
"@vue/compiler-ssr",
"@vue/shared",
"vue"
]
},
"@vue/shared@3.5.23": {
"integrity": "sha512-0YZ1DYuC5o/YJPf6pFdt2KYxVGDxkDbH/1NYJnVJWUkzr8ituBEmFVQRNX2gCaAsFEjEDnLkWpgqlZA7htgS/g=="
},
"birpc@2.7.0": {
"integrity": "sha512-tub/wFGH49vNCm0xraykcY3TcRgX/3JsALYq/Lwrtti+bTyFHkCUAWF5wgYoie8P41wYwig2mIKiqoocr1EkEQ=="
},
"copy-anything@4.0.5": {
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
"dependencies": [
"is-what"
]
},
"csstype@3.1.3": {
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
},
"earcut@3.0.2": {
"integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ=="
},
"entities@4.5.0": {
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="
},
"esbuild@0.25.12": {
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"optionalDependencies": [
"@esbuild/aix-ppc64",
"@esbuild/android-arm",
"@esbuild/android-arm64",
"@esbuild/android-x64",
"@esbuild/darwin-arm64",
"@esbuild/darwin-x64",
"@esbuild/freebsd-arm64",
"@esbuild/freebsd-x64",
"@esbuild/linux-arm",
"@esbuild/linux-arm64",
"@esbuild/linux-ia32",
"@esbuild/linux-loong64",
"@esbuild/linux-mips64el",
"@esbuild/linux-ppc64",
"@esbuild/linux-riscv64",
"@esbuild/linux-s390x",
"@esbuild/linux-x64",
"@esbuild/netbsd-arm64",
"@esbuild/netbsd-x64",
"@esbuild/openbsd-arm64",
"@esbuild/openbsd-x64",
"@esbuild/openharmony-arm64",
"@esbuild/sunos-x64",
"@esbuild/win32-arm64",
"@esbuild/win32-ia32",
"@esbuild/win32-x64"
],
"scripts": true,
"bin": true
},
"estree-walker@2.0.2": {
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
},
"fdir@6.5.0_picomatch@4.0.3": {
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dependencies": [
"picomatch"
],
"optionalPeers": [
"picomatch"
]
},
"fsevents@2.3.3": {
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"os": ["darwin"],
"scripts": true
},
"geojson-vt@4.0.2": {
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
},
"get-stream@6.0.1": {
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
},
"gl-matrix@3.4.4": {
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="
},
"hookable@5.5.3": {
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="
},
"is-what@5.5.0": {
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="
},
"json-stringify-pretty-compact@4.0.0": {
"integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q=="
},
"kdbush@4.0.2": {
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA=="
},
"magic-string@0.30.21": {
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dependencies": [
"@jridgewell/sourcemap-codec"
]
},
"maplibre-gl@5.16.0": {
"integrity": "sha512-/VDY89nr4jgLJyzmhy325cG6VUI02WkZ/UfVuDbG/piXzo6ODnM+omDFIwWY8tsEsBG26DNDmNMn3Y2ikHsBiA==",
"dependencies": [
"@mapbox/geojson-rewind",
"@mapbox/jsonlint-lines-primitives",
"@mapbox/point-geometry",
"@mapbox/tiny-sdf",
"@mapbox/unitbezier",
"@mapbox/vector-tile",
"@mapbox/whoots-js",
"@maplibre/maplibre-gl-style-spec",
"@maplibre/mlt",
"@maplibre/vt-pbf",
"@types/geojson",
"@types/geojson-vt",
"@types/supercluster",
"earcut",
"geojson-vt",
"gl-matrix",
"kdbush",
"murmurhash-js",
"pbf",
"potpack",
"quickselect",
"supercluster",
"tinyqueue"
]
},
"minimist@1.2.8": {
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mitt@3.0.1": {
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
},
"murmurhash-js@1.0.0": {
"integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw=="
},
"nanoid@3.3.11": {
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"bin": true
},
"pbf@4.0.1": {
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
"dependencies": [
"resolve-protobuf-schema"
],
"bin": true
},
"perfect-debounce@1.0.0": {
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="
},
"picocolors@1.1.1": {
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"picomatch@4.0.3": {
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="
},
"pinia@3.0.4_vue@3.5.23": {
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
"dependencies": [
"@vue/devtools-api",
"vue"
]
},
"postcss@8.5.6": {
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dependencies": [
"nanoid",
"picocolors",
"source-map-js"
]
},
"potpack@2.1.0": {
"integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ=="
},
"protocol-buffers-schema@3.6.0": {
"integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="
},
"quickselect@3.0.0": {
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g=="
},
"resolve-protobuf-schema@2.1.0": {
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
"dependencies": [
"protocol-buffers-schema"
]
},
"rfdc@1.4.1": {
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
},
"rollup@4.52.5": {
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"dependencies": [
"@types/estree"
],
"optionalDependencies": [
"@rollup/rollup-android-arm-eabi",
"@rollup/rollup-android-arm64",
"@rollup/rollup-darwin-arm64",
"@rollup/rollup-darwin-x64",
"@rollup/rollup-freebsd-arm64",
"@rollup/rollup-freebsd-x64",
"@rollup/rollup-linux-arm-gnueabihf",
"@rollup/rollup-linux-arm-musleabihf",
"@rollup/rollup-linux-arm64-gnu",
"@rollup/rollup-linux-arm64-musl",
"@rollup/rollup-linux-loong64-gnu",
"@rollup/rollup-linux-ppc64-gnu",
"@rollup/rollup-linux-riscv64-gnu",
"@rollup/rollup-linux-riscv64-musl",
"@rollup/rollup-linux-s390x-gnu",
"@rollup/rollup-linux-x64-gnu",
"@rollup/rollup-linux-x64-musl",
"@rollup/rollup-openharmony-arm64",
"@rollup/rollup-win32-arm64-msvc",
"@rollup/rollup-win32-ia32-msvc",
"@rollup/rollup-win32-x64-gnu",
"@rollup/rollup-win32-x64-msvc",
"fsevents"
],
"bin": true
},
"rw@1.3.3": {
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
},
"source-map-js@1.2.1": {
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"speakingurl@14.0.1": {
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="
},
"supercluster@8.0.1": {
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"dependencies": [
"kdbush"
]
},
"superjson@2.2.5": {
"integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
"dependencies": [
"copy-anything"
]
},
"three@0.182.0": {
"integrity": "sha512-GbHabT+Irv+ihI1/f5kIIsZ+Ef9Sl5A1Y7imvS5RQjWgtTPfPnZ43JmlYI7NtCRDK9zir20lQpfg8/9Yd02OvQ=="
},
"tinyglobby@0.2.15_picomatch@4.0.3": {
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dependencies": [
"fdir",
"picomatch"
]
},
"tinyqueue@3.0.0": {
"integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g=="
},
"vite@7.2.1_picomatch@4.0.3": {
"integrity": "sha512-qTl3VF7BvOupTR85Zc561sPEgxyUSNSvTQ9fit7DEMP7yPgvvIGm5Zfa1dOM+kOwWGNviK9uFM9ra77+OjK7lQ==",
"dependencies": [
"esbuild",
"fdir",
"picomatch",
"postcss",
"rollup",
"tinyglobby"
],
"optionalDependencies": [
"fsevents"
],
"bin": true
},
"vue@3.5.23": {
"integrity": "sha512-CfvZv/vI52xUhumUvHtD6iFIS78nGWfX4IJnHfBGhpqMI0CwDq2YEngXOeaBFMRmiArcqczuVrLxurvesTYT9w==",
"dependencies": [
"@vue/compiler-dom",
"@vue/compiler-sfc",
"@vue/runtime-dom",
"@vue/server-renderer",
"@vue/shared"
]
}
},
"workspace": {
"dependencies": [
"npm:@vitejs/plugin-vue@5",
"npm:maplibre-gl@^5.16.0",
"npm:pinia@^3.0.4",
"npm:three@0.182",
"npm:vite@^7.2.1",
"npm:vue@^3.5.23"
]
}
}

12
ui/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MoundHunters</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1
ui/public/tiles Symbolic link
View File

@@ -0,0 +1 @@
/home/mark/projects/moundhunters/data/MOUND

395
ui/src/App.vue Normal file
View File

@@ -0,0 +1,395 @@
<template>
<div id="app">
<div ref="mapContainer" class="map-container"></div>
<canvas ref="threeCanvas" class="three-canvas"></canvas>
<div class="layer-controls">
<div class="control-section">
<label>Base Map:</label>
<label><input type="radio" value="osm" v-model="baseLayer" @change="updateBaseLayer"> Street</label>
<label><input type="radio" value="satellite" v-model="baseLayer" @change="updateBaseLayer"> Satellite</label>
</div>
<div class="control-section">
<label>
<input type="checkbox" v-model="showOctagon" @change="toggleOctagon">
Show Octagon
</label>
<label>
<input type="checkbox" v-model="showLidar" @change="toggleLidar">
Show Lidar
</label>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import * as THREE from 'three';
export default {
name: 'App',
setup() {
const mapContainer = ref(null);
const threeCanvas = ref(null);
let map = null;
let scene = null;
let camera = null;
let renderer = null;
let lidarMeshes = [];
const baseLayer = ref('osm');
const showOctagon = ref(true);
const showLidar = ref(true);
// Newark Octagon coordinates (converted from DMS to decimal)
const octagonCoords = [
[-82.44133, 40.05431], // 40°03'15.5"N 82°26'28.8"W
[-82.44264, 40.05311], // 40°03'11.2"N 82°26'33.5"W
[-82.44464, 40.05242], // 40°03'08.7"N 82°26'40.7"W
[-82.44631, 40.05342], // 40°03'12.3"N 82°26'46.7"W
[-82.44728, 40.05500], // 40°03'18.0"N 82°26'50.2"W
[-82.44589, 40.05633], // 40°03'22.8"N 82°26'45.2"W
[-82.44389, 40.05697], // 40°03'25.1"N 82°26'38.0"W
[-82.44192, 40.05589], // 40°03'21.2"N 82°26'30.9"W
[-82.44133, 40.05431], // Close the polygon
];
const octagonCenter = [-82.44383, 40.05469];
onMounted(() => {
map = new maplibregl.Map({
container: mapContainer.value,
style: {
version: 8,
sources: {
'osm': {
type: 'raster',
tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
tileSize: 256,
attribution: '© OpenStreetMap contributors'
},
'satellite': {
type: 'raster',
tiles: [
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'
],
tileSize: 256,
attribution: 'Esri, Maxar, Earthstar Geographics, USDA FSA, USGS, Aerogrid, IGN, IGP, and the GIS User Community'
}
},
layers: [
{
id: 'osm-layer',
type: 'raster',
source: 'osm',
layout: { visibility: 'visible' }
},
{
id: 'satellite-layer',
type: 'raster',
source: 'satellite',
layout: { visibility: 'none' }
}
]
},
center: octagonCenter,
zoom: 15
});
map.on('load', () => {
// Add octagon source and layer
map.addSource('octagon', {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [octagonCoords]
}
}
});
map.addLayer({
id: 'octagon-fill',
type: 'fill',
source: 'octagon',
paint: {
'fill-color': '#ff0000',
'fill-opacity': 0.2
}
});
map.addLayer({
id: 'octagon-outline',
type: 'line',
source: 'octagon',
paint: {
'line-color': '#ff0000',
'line-width': 2
}
});
// Initialize Three.js
initThreeJS();
loadLidarTiles();
// Update Three.js on map move
map.on('move', updateThreeCamera);
map.on('zoom', updateThreeCamera);
});
});
const updateBaseLayer = () => {
if (!map) return;
if (baseLayer.value === 'osm') {
map.setLayoutProperty('osm-layer', 'visibility', 'visible');
map.setLayoutProperty('satellite-layer', 'visibility', 'none');
} else {
map.setLayoutProperty('osm-layer', 'visibility', 'none');
map.setLayoutProperty('satellite-layer', 'visibility', 'visible');
}
};
const toggleOctagon = () => {
if (!map) return;
const visibility = showOctagon.value ? 'visible' : 'none';
map.setLayoutProperty('octagon-fill', 'visibility', visibility);
map.setLayoutProperty('octagon-outline', 'visibility', visibility);
};
const toggleLidar = () => {
lidarMeshes.forEach(mesh => {
mesh.visible = showLidar.value;
});
if (renderer) renderer.render(scene, camera);
};
const initThreeJS = () => {
scene = new THREE.Scene();
camera = new THREE.OrthographicCamera(
-1, 1, 1, -1, 0.1, 10000
);
camera.position.z = 1;
renderer = new THREE.WebGLRenderer({
canvas: threeCanvas.value,
alpha: true,
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0x000000, 0);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(1, 1, 1);
scene.add(directionalLight);
window.addEventListener('resize', handleResize);
updateThreeCamera();
};
const handleResize = () => {
if (!renderer || !camera) return;
renderer.setSize(window.innerWidth, window.innerHeight);
updateThreeCamera();
};
const updateThreeCamera = () => {
if (!map || !camera || !renderer) return;
const bounds = map.getBounds();
const ne = bounds.getNorthEast();
const sw = bounds.getSouthWest();
camera.left = sw.lng;
camera.right = ne.lng;
camera.top = ne.lat;
camera.bottom = sw.lat;
camera.updateProjectionMatrix();
renderer.render(scene, camera);
};
const parseMoundFile = async (url) => {
const response = await fetch(url);
const buffer = await response.arrayBuffer();
const view = new DataView(buffer);
let offset = 0;
const magic = String.fromCharCode(
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++),
view.getUint8(offset++)
);
if (magic !== 'LIDR') {
throw new Error('Invalid .mound file');
}
const version = view.getUint32(offset, true); offset += 4;
const pointCount = view.getUint32(offset, true); offset += 4;
const triangleCount = view.getUint32(offset, true); offset += 4;
const minX = view.getFloat32(offset, true); offset += 4;
const minY = view.getFloat32(offset, true); offset += 4;
const minZ = view.getFloat32(offset, true); offset += 4;
const maxX = view.getFloat32(offset, true); offset += 4;
const maxY = view.getFloat32(offset, true); offset += 4;
const maxZ = view.getFloat32(offset, true); offset += 4;
offset += 24; // Skip reserved
const positions = new Float32Array(buffer, offset, pointCount * 3);
offset += pointCount * 3 * 4;
const indices = new Uint32Array(buffer, offset, triangleCount * 3);
return {
pointCount,
triangleCount,
bounds: { minX, minY, minZ, maxX, maxY, maxZ },
positions,
indices
};
};
const loadLidarTiles = async () => {
const tiles = [
'BS19820747',
'BS19820748',
'BS19830747',
'BS19830748'
];
for (const tileName of tiles) {
try {
console.log(`Loading ${tileName}...`);
const data = await parseMoundFile(`/tiles/${tileName}.mound`);
console.log(`${tileName} bounds:`, data.bounds);
console.log(`First few positions:`, data.positions.slice(0, 15));
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(data.positions, 3));
geometry.setIndex(new THREE.BufferAttribute(data.indices, 1));
geometry.computeVertexNormals();
const material = new THREE.MeshLambertMaterial({
color: 0x8B7355,
side: THREE.DoubleSide,
wireframe: false
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
lidarMeshes.push(mesh);
console.log(`Loaded ${tileName}: ${data.pointCount} points, ${data.triangleCount} triangles`);
console.log(`Mesh position:`, mesh.position);
console.log(`Mesh in scene:`, scene.children.length);
} catch (err) {
console.error(`Failed to load ${tileName}:`, err);
}
}
console.log('Map bounds:', map.getBounds().toArray());
console.log('Camera:', camera);
updateThreeCamera();
};
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (renderer) {
renderer.dispose();
}
});
return {
mapContainer,
threeCanvas,
baseLayer,
showOctagon,
showLidar,
updateBaseLayer,
toggleOctagon,
toggleLidar
};
}
};
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
.map-container {
width: 100%;
height: 100%;
}
.three-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.layer-controls {
position: absolute;
bottom: 20px;
left: 20px;
background: white;
padding: 15px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
font-family: Arial, sans-serif;
font-size: 14px;
}
.control-section {
margin-bottom: 10px;
}
.control-section:last-child {
margin-bottom: 0;
}
.control-section label {
display: block;
margin: 5px 0;
cursor: pointer;
}
.control-section label:first-child {
font-weight: bold;
margin-bottom: 8px;
cursor: default;
}
.control-section input[type="radio"],
.control-section input[type="checkbox"] {
margin-right: 8px;
cursor: pointer;
}
</style>

6
ui/src/main.js Normal file
View File

@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

55
ui/vite.config.js Normal file
View File

@@ -0,0 +1,55 @@
import { defineConfig } from 'vite'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue'
const __dirname = dirname(fileURLToPath(import.meta.url))
export default defineConfig({
plugins: [
vue(),
],
optimizeDeps: {
include: ['vue']
},
build: {
outDir: '../priv/static',
emptyOutDir: false,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
},
output: {
entryFileNames: 'app.js',
chunkFileNames: 'chunks/[name].js',
assetFileNames: 'assets/[name].[ext]'
}
}
},
resolve: {
alias: {
"@": fileURLToPath(new URL('./src', import.meta.url)),
"@stores": fileURLToPath(new URL('./src/stores', import.meta.url)),
},
},
server: {
proxy: {
'/socket': {
target: 'http://localhost:4000',
changeOrigin: true,
secure: false
},
'/api': {
target: 'http://localhost:4000',
changeOrigin: true,
secure: false
},
'/admin': {
target: 'http://localhost:4000',
changeOrigin: true,
secure: false
},
}
}
})