add a bunch of (false) information to the map

This commit is contained in:
2026-01-21 23:25:04 +01:00
parent 08fe8ebc7f
commit 9f591b1bdc
4 changed files with 955 additions and 74 deletions

View File

@@ -34,6 +34,7 @@ let
pkgs.python3Packages.scipy
pkgs.python3Packages.numpy
pkgs.python3Packages.pyproj
pkgs.python3Packages.requests
];
mkShell = pkgs.mkShell;

View File

@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""
Process Hopewell Earthworks Sites
Acquires lidar tiles, processes them to mound format, and generates metadata.
"""
import subprocess
import json
import os
from pathlib import Path
from typing import List, Dict, Tuple
# Define all sites of interest
SITES = [
{
"name": "Newark Octagon Earthworks",
"coordinates": [(-82.4463745, 40.0519828)],
"description": "Part of the Newark Earthworks complex, the Octagon is precisely aligned to the 18.6-year lunar cycle. Connected to a 50-acre circle, this geometric earthwork demonstrates sophisticated astronomical knowledge. The Octagon's eight walls and Observatory Circle form one of only two known circle-octagon pairs in the Hopewell world.",
"type": "earthwork",
"significance": "unesco_world_heritage",
"azimuth_alignment": 211,
},
{
"name": "Great Circle Earthworks",
"coordinates": [(-82.4277555, 40.0402671)],
"description": "A nearly perfect circle 1,200 feet in diameter, enclosing 30 acres. The earthen wall is lined by a deep interior ditch. Located in Heath, Ohio, this is one of the largest circular earthworks in the Americas and serves as the site of the Newark Earthworks Museum.",
"type": "earthwork",
"significance": "unesco_world_heritage",
},
{
"name": "Van Voorhis Walls",
"coordinates": [
(-82.446375, 40.051983),
(-82.447, 40.048),
(-82.448, 40.045),
(-82.450, 40.040),
],
"description": "The best-preserved section of the Great Hopewell Road, extending 2.5 miles south from the Newark Octagon to Ramp Creek. This confirmed earthwork consists of parallel walls approximately 50 meters (150-200 feet) apart, aligned on an azimuth of 211°. Still visible above ground in woodland areas too swampy to farm.",
"type": "road_confirmed",
"significance": "confirmed_road_section",
"azimuth_alignment": 211,
},
{
"name": "Mound City Group",
"coordinates": [(-83.0065767, 39.3744923)],
"description": "The headquarters of Hopewell Culture National Historical Park. Contains 23 burial mounds within a nearly square earthen enclosure along the Scioto River. Each mound covered a charnel house where the dead were cremated. Excavations revealed spectacular artifacts including effigy pipes, mica, and copper.",
"type": "earthwork",
"significance": "unesco_world_heritage",
},
{
"name": "Hopeton Earthworks",
"coordinates": [(-82.9809185, 39.3790743)],
"description": "A geometric earthwork complex featuring a circle (320m diameter) and square of similar size, connected by parallel earthen lines aligned to the winter solstice. Located on a high terrace on the east side of the Scioto River, northeast from Mound City.",
"type": "earthwork",
"significance": "unesco_world_heritage",
"astronomical_alignment": "winter_solstice",
},
{
"name": "Hopewell Mound Group",
"coordinates": [(-83.0844809, 39.3608166)],
"description": "The type site for the Hopewell culture, named after former landowner M. Cloud Hopewell. Contains 29 mounds including the largest known Hopewell mound—500 feet long and consisting of three conjoined circles. A semicircular earthwork encloses the main mound and four additional mounds.",
"type": "earthwork",
"significance": "type_site",
},
{
"name": "Seip Earthworks",
"coordinates": [(-83.2214086, 39.2416867)],
"description": "One of the largest Hopewell complexes, featuring two circles and a square enclosing 121 acres. The Seip-Pricer Mound stands 30 feet high. The square measures exactly 27 acres, matching four other nearby Hopewell sites, suggesting a common unit of measurement. Contains evidence of elaborate burials and exotic trade goods.",
"type": "earthwork",
"significance": "unesco_world_heritage",
},
{
"name": "High Bank Works",
"coordinates": [(-82.94, 39.26)],
"description": "Features a circle-octagon pair identical to the Newark Earthworks—both circles are exactly 1,050 feet in diameter. Located 60 miles from Newark on a terrace 75-80 feet above the Scioto River. The octagon is precisely aligned to the northernmost moonrise of the 18.6-year lunar cycle. Currently a research preserve, not open to the public.",
"type": "earthwork",
"significance": "unesco_world_heritage",
"astronomical_alignment": "lunar_standstill",
"paired_with": "Newark Octagon Earthworks",
},
]
def run_command(cmd: List[str], description: str) -> subprocess.CompletedProcess:
"""Run a shell command and return the result."""
print(f"\n{'='*60}")
print(f"{description}")
print(f"{'='*60}")
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"ERROR: {result.stderr}")
else:
print(f"SUCCESS: {result.stdout}")
return result
def acquire_tiles(site: Dict, output_dir: str) -> List[str]:
"""Acquire lidar tiles for a site."""
coords = site["coordinates"]
# Build the request_tiles.py command
cmd = ["python", "./tooling/request_tiles.py"]
for lon, lat in coords:
cmd.extend(["--coords", str(lon), str(lat)])
cmd.extend(["--output", output_dir])
result = run_command(cmd, f"Acquiring tiles for {site['name']}")
# Parse the output JSON to get tile names
if result.returncode == 0:
try:
data = json.loads(result.stdout)
tiles = [
tile["tile_name"]
for tile in data.get("tiles", [])
if tile.get("status") in ["downloaded", "already_exists"]
]
return tiles
except json.JSONDecodeError:
print(f"Warning: Could not parse JSON output for {site['name']}")
return []
return []
def unzip_tiles(tiles: List[str], lidar_dir: str) -> None:
"""Unzip all downloaded tiles."""
for tile in tiles:
zip_path = os.path.join(lidar_dir, f"{tile}.zip")
extract_dir = os.path.join(lidar_dir, tile)
if not os.path.exists(zip_path):
print(f"Warning: {zip_path} does not exist, skipping")
continue
cmd = ["unzip", "-o", zip_path, "-d", extract_dir]
run_command(cmd, f"Unzipping {tile}")
def convert_to_mound(tiles: List[str], lidar_dir: str, mound_dir: str) -> None:
"""Convert LAS files to mound format."""
for tile in tiles:
las_path = os.path.join(lidar_dir, tile, f"{tile}.las")
mound_path = os.path.join(mound_dir, f"{tile}.mound")
if not os.path.exists(las_path):
print(f"Warning: {las_path} does not exist, skipping")
continue
cmd = ["python", "tooling/las2mound.py", las_path, mound_path]
run_command(cmd, f"Converting {tile} to mound format")
def generate_metadata(sites: List[Dict], output_path: str) -> None:
"""Generate a JSON metadata file with all site information."""
metadata = {
"generated": "2025-01-21",
"description": "Hopewell Ceremonial Earthworks and Great Hopewell Road Sites",
"sites": sites,
"summary": {
"total_sites": len(sites),
"unesco_sites": len([s for s in sites if s.get("significance") == "unesco_world_heritage"]),
"confirmed_road_sections": len([s for s in sites if s.get("type") == "road_confirmed"]),
}
}
with open(output_path, 'w') as f:
json.dump(metadata, f, indent=2)
print(f"\n{'='*60}")
print(f"Metadata written to {output_path}")
print(f"{'='*60}")
def main():
"""Main processing pipeline."""
# Setup directories
base_dir = Path("./data")
lidar_dir = base_dir / "LIDAR"
mound_dir = base_dir / "MOUND"
lidar_dir.mkdir(parents=True, exist_ok=True)
mound_dir.mkdir(parents=True, exist_ok=True)
print(f"""
╔══════════════════════════════════════════════════════════════════════════╗
║ ║
║ HOPEWELL EARTHWORKS LIDAR PROCESSING PIPELINE ║
║ ║
║ Processing {len(SITES)} sites of interest: ║
║ - UNESCO World Heritage Sites ║
║ - Confirmed Great Hopewell Road sections ║
║ - Type sites and major earthwork complexes ║
║ ║
╚══════════════════════════════════════════════════════════════════════════╝
""")
# Process each site
for site in SITES:
print(f"\n{'#'*80}")
print(f"# PROCESSING: {site['name']}")
print(f"# Type: {site['type']}")
print(f"# Coordinates: {len(site['coordinates'])} point(s)")
print(f"{'#'*80}")
# Step 1: Acquire tiles
tiles = acquire_tiles(site, str(lidar_dir))
if not tiles:
print(f"Warning: No tiles acquired for {site['name']}")
site["tiles"] = []
continue
site["tiles"] = tiles
print(f"\nAcquired {len(tiles)} tiles: {', '.join(tiles)}")
# Step 2: Unzip tiles
unzip_tiles(tiles, str(lidar_dir))
# Step 3: Convert to mound format
convert_to_mound(tiles, str(lidar_dir), str(mound_dir))
# Step 4: Generate metadata
metadata_path = base_dir / "hopewell_sites_metadata.json"
generate_metadata(SITES, str(metadata_path))
print(f"""
╔══════════════════════════════════════════════════════════════════════════╗
║ ║
║ PROCESSING COMPLETE ║
║ ║
║ Metadata: {str(metadata_path):55}
║ Lidar data: {str(lidar_dir):58}
║ Mound files: {str(mound_dir):57}
║ ║
╚══════════════════════════════════════════════════════════════════════════╝
""")
if __name__ == "__main__":
main()

401
tooling/request_tiles.py Normal file
View File

@@ -0,0 +1,401 @@
#!/usr/bin/env python3
"""
Request and download Ohio lidar tiles based on coordinates.
Usage:
./request_tiles.py --coords -82.4438 40.0547 --coords -82.4440 40.0548 --output tiles/
echo "-82.4438,40.0547" | ./request_tiles.py --output tiles/
./request_tiles.py --coords-file locations.txt --output tiles/
This tool:
1. Converts lon/lat coordinates to Web Mercator (EPSG:3857)
2. Queries Ohio's ArcGIS tile service to get tile metadata
3. Downloads tiles from OGRIP as .zip files
4. Outputs JSON summary of downloaded tiles to stdout
5. Respects rate limiting between requests
Output format (JSON to stdout):
{
"requested": 5,
"downloaded": 4,
"skipped": 1,
"failed": 0,
"tiles": [
{
"tile_name": "BS19820747",
"county": "LIC",
"year": "2020",
"block": "4",
"url": "https://gis1.oit.ohio.gov/...",
"output_path": "tiles/BS19820747.zip",
"status": "downloaded"
},
...
]
}
"""
import sys
import time
import json
import logging
import argparse
import requests
from pathlib import Path
from typing import List, Tuple, Optional, Dict, Any
try:
from pyproj import Transformer
except ImportError:
print("Error: pyproj not installed. Run: pip install pyproj", file=sys.stderr)
sys.exit(1)
# Configure logging to stderr only (stdout is for JSON output)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
stream=sys.stderr
)
logger = logging.getLogger(__name__)
# Ohio ArcGIS tile service
TILE_SERVICE_URL = "https://maps.ohio.gov/arcgis/rest/services/OGRIP/3DepTiles/MapServer/0/query"
# OGRIP download URL template
DOWNLOAD_URL_TEMPLATE = "https://gis1.oit.ohio.gov/ZIPARCHIVES_III/ELEVATION/3DEP/LIDAR/{county}/{tile_name}.zip"
def lonlat_to_webmercator(lon: float, lat: float) -> Tuple[float, float]:
"""Convert lon/lat (WGS84) to Web Mercator (EPSG:3857)."""
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
x, y = transformer.transform(lon, lat)
return x, y
def query_tile_info(x: float, y: float) -> Optional[Dict[str, Any]]:
"""
Query Ohio ArcGIS service for tile information at given Web Mercator coordinates.
Returns dict with keys: tile_name, county, year, block, note
Returns None if no tile found or on error.
"""
params = {
'f': 'json',
'returnGeometry': 'false',
'spatialRel': 'esriSpatialRelIntersects',
'geometry': json.dumps({
'x': x,
'y': y,
'spatialReference': {'wkid': 3857}
}),
'geometryType': 'esriGeometryPoint',
'inSR': '3857',
'outFields': '*',
'outSR': '3857'
}
try:
response = requests.get(TILE_SERVICE_URL, params=params, timeout=10)
response.raise_for_status()
data = response.json()
if 'features' not in data or len(data['features']) == 0:
return None
# Get first feature (should only be one)
attrs = data['features'][0]['attributes']
return {
'tile_name': attrs.get('TileName'),
'county': attrs.get('County'),
'year': attrs.get('Year'),
'block': attrs.get('Block'),
'note': attrs.get('note')
}
except requests.RequestException as e:
logger.error(f"Failed to query tile service: {e}")
return None
except (KeyError, json.JSONDecodeError) as e:
logger.error(f"Failed to parse tile service response: {e}")
return None
def download_tile(tile_name: str, county: str, output_dir: Path) -> Tuple[bool, Optional[Path]]:
"""
Download a tile zip file from OGRIP.
Returns (success, output_path)
"""
url = DOWNLOAD_URL_TEMPLATE.format(county=county, tile_name=tile_name)
output_path = output_dir / f"{tile_name}.zip"
# Check if already exists
if output_path.exists():
logger.info(f"Tile {tile_name} already exists, skipping download")
return True, output_path
try:
logger.info(f"Downloading {tile_name} from {county}...")
response = requests.get(url, timeout=30, stream=True)
response.raise_for_status()
# Download with progress
output_dir.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
size_mb = output_path.stat().st_size / (1024 * 1024)
logger.info(f"Downloaded {tile_name} ({size_mb:.2f} MB)")
return True, output_path
except requests.RequestException as e:
logger.error(f"Failed to download {tile_name}: {e}")
# Clean up partial download
if output_path.exists():
output_path.unlink()
return False, None
def process_coordinates(
coords: List[Tuple[float, float]],
output_dir: Path,
min_delay: float
) -> Dict[str, Any]:
"""
Process a list of coordinates and download corresponding tiles.
Returns summary dict with download results.
"""
results = {
'requested': len(coords),
'downloaded': 0,
'skipped': 0,
'failed': 0,
'tiles': []
}
# Track unique tiles to avoid duplicate downloads
seen_tiles = set()
for i, (lon, lat) in enumerate(coords):
logger.info(f"Processing coordinate {i+1}/{len(coords)}: ({lon:.6f}, {lat:.6f})")
# Convert to Web Mercator
x, y = lonlat_to_webmercator(lon, lat)
logger.debug(f"Web Mercator: ({x:.2f}, {y:.2f})")
# Query tile info
tile_info = query_tile_info(x, y)
if not tile_info:
logger.warning(f"No tile found for coordinates ({lon:.6f}, {lat:.6f})")
results['failed'] += 1
results['tiles'].append({
'lon': lon,
'lat': lat,
'status': 'not_found'
})
continue
tile_name = tile_info['tile_name']
county = tile_info['county']
# Check if we've already processed this tile
if tile_name in seen_tiles:
logger.info(f"Tile {tile_name} already processed in this run, skipping")
results['skipped'] += 1
continue
seen_tiles.add(tile_name)
# Download tile
output_path = output_dir / f"{tile_name}.zip"
if output_path.exists():
logger.info(f"Tile {tile_name} already exists on disk")
results['skipped'] += 1
status = 'already_exists'
success = True
else:
success, download_path = download_tile(tile_name, county, output_dir)
if success:
results['downloaded'] += 1
status = 'downloaded'
else:
results['failed'] += 1
status = 'download_failed'
# Add to results
tile_result = {
'tile_name': tile_name,
'county': county,
'year': tile_info.get('year'),
'block': tile_info.get('block'),
'url': DOWNLOAD_URL_TEMPLATE.format(county=county, tile_name=tile_name),
'output_path': str(output_path) if success else None,
'status': status
}
results['tiles'].append(tile_result)
# Rate limiting between downloads (skip if this was already on disk)
if status == 'downloaded' and i < len(coords) - 1:
logger.debug(f"Waiting {min_delay}s before next request...")
time.sleep(min_delay)
return results
def parse_coords_from_stdin() -> List[Tuple[float, float]]:
"""Parse coordinates from stdin (one per line, comma or space separated)."""
coords = []
for line in sys.stdin:
line = line.strip()
if not line or line.startswith('#'):
continue
# Try comma separator first, then space
parts = line.replace(',', ' ').split()
if len(parts) >= 2:
try:
lon = float(parts[0])
lat = float(parts[1])
coords.append((lon, lat))
except ValueError:
logger.warning(f"Skipping invalid line: {line}")
return coords
def parse_coords_from_file(filepath: Path) -> List[Tuple[float, float]]:
"""Parse coordinates from a file."""
coords = []
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
parts = line.replace(',', ' ').split()
if len(parts) >= 2:
try:
lon = float(parts[0])
lat = float(parts[1])
coords.append((lon, lat))
except ValueError:
logger.warning(f"Skipping invalid line: {line}")
return coords
def main():
parser = argparse.ArgumentParser(
description='Download Ohio lidar tiles based on coordinates',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Download tiles for specific coordinates
%(prog)s --coords -82.4438 40.0547 --coords -82.4440 40.0548 --output tiles/
# Read coordinates from stdin
echo "-82.4438,40.0547" | %(prog)s --output tiles/
# Read from file
%(prog)s --coords-file locations.txt --output tiles/
# Custom rate limiting
%(prog)s --coords -82.4438 40.0547 --output tiles/ --delay 2.0
Coordinate format:
Longitude, Latitude in decimal degrees (WGS84)
Example: -82.4438 40.0547 (Newark Earthworks area)
"""
)
parser.add_argument(
'--coords',
nargs=2,
type=float,
action='append',
metavar=('LON', 'LAT'),
help='Coordinate pair (longitude latitude). Can be specified multiple times.'
)
parser.add_argument(
'--coords-file',
type=Path,
metavar='FILE',
help='File containing coordinates (one pair per line, comma or space separated)'
)
parser.add_argument(
'--output',
type=Path,
required=True,
metavar='DIR',
help='Output directory for downloaded tile zip files'
)
parser.add_argument(
'--delay',
type=float,
default=1.0,
metavar='SECONDS',
help='Minimum delay between tile downloads (default: 1.0 seconds)'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Enable verbose debug logging'
)
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
# Collect coordinates from all sources
coords = []
if args.coords:
coords.extend(args.coords)
if args.coords_file:
if not args.coords_file.exists():
logger.error(f"Coordinates file not found: {args.coords_file}")
sys.exit(1)
coords.extend(parse_coords_from_file(args.coords_file))
# Check stdin if no coordinates provided
if not coords and not sys.stdin.isatty():
coords.extend(parse_coords_from_stdin())
if not coords:
logger.error("No coordinates provided. Use --coords, --coords-file, or pipe to stdin.")
parser.print_help(sys.stderr)
sys.exit(1)
logger.info(f"Processing {len(coords)} coordinate(s)")
logger.info(f"Output directory: {args.output}")
logger.info(f"Rate limit: {args.delay}s between downloads")
# Process coordinates and download tiles
results = process_coordinates(coords, args.output, args.delay)
# Output JSON to stdout
print(json.dumps(results, indent=2))
# Log summary to stderr
logger.info("="*50)
logger.info(f"Summary: {results['downloaded']} downloaded, {results['skipped']} skipped, {results['failed']} failed")
if results['failed'] > 0:
logger.error(f"{results['failed']} tile(s) failed to download or locate")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -20,10 +20,24 @@
</div>
<div class="control-section">
<label>
<input type="checkbox" v-model="showOctagon" @change="toggleOctagon">
Show Octagon
<label class="section-header" @click="historicMarkersExpanded = !historicMarkersExpanded" style="cursor: pointer;">
{{ historicMarkersExpanded ? '▼' : '▶' }} Historic Markers
</label>
<div v-if="historicMarkersExpanded" class="subsection">
<button class="hide-all-btn" @click="hideAllHistoricMarkers">
{{ allHistoricMarkersHidden ? 'Show All' : 'Hide All' }}
</button>
<div v-for="site in KNOWN_SITES" :key="site.name" class="site-control">
<label>
<input type="checkbox" v-model="visibleSites[site.name]" @change="toggleSite(site.name)">
{{ site.name }}
</label>
<button class="jump-btn" @click="jumpToSite(site)" title="Jump to location">📍</button>
</div>
</div>
</div>
<div class="control-section">
<label>
<input type="checkbox" v-model="showLidar" @change="toggleLidar">
Show Lidar
@@ -111,6 +125,11 @@
<div>Bearing: {{ popup.feature.properties.bearing.toFixed(1) }}°</div>
<button @click="deleteFeature(popup.feature.id)" class="popup-btn danger">Delete</button>
</div>
<div v-else-if="popup.type === 'site'" class="site-popup">
<strong>{{ popup.feature.properties.name }}</strong>
<div class="site-type">{{ popup.feature.properties.type === 'road_confirmed' ? 'Confirmed Road Segment' : 'Earthwork' }}</div>
<div class="site-description">{{ popup.feature.properties.description }}</div>
</div>
</div>
<button @click="popup.visible = false" class="popup-close">×</button>
</div>
@@ -138,7 +157,9 @@ const sandboxRef = ref(null);
const sandboxVisible = ref(false);
const sandboxOffscreen = ref(false);
const baseLayer = ref('osm');
const showOctagon = ref(true);
const historicMarkersExpanded = ref(true);
const visibleSites = ref({});
const allHistoricMarkersHidden = ref(false);
const showLidar = ref(true);
const showGeometry = ref(true);
const imperialUnits = ref(false);
@@ -155,7 +176,7 @@ const nextFeatureId = ref(1);
// UI state
const contextMenu = ref({ visible: false, x: 0, y: 0, lngLat: null, hasTile: false });
const popup = ref({ visible: false, x: 0, y: 0, type: null, feature: null });
const popup = ref({ visible: false, x: 0, y: 0, type: null, feature: null, lngLat: null });
// Map instance
let map = null;
@@ -237,29 +258,16 @@ const KNOWN_SITES = [
"tiles":["BS18430458"]}
];
// Tile names to load
const TILE_NAMES = [
'BS19820747',
'BS19820748',
'BS19830747',
'BS19830748',
'BS19820746',
'BS19810747',
'BS19810746',
];
// Newark Octagon coordinates
const octagonCoords = [
[-82.44123, 40.05443],
[-82.44260, 40.05309],
[-82.44464, 40.05237],
[-82.44631, 40.05342],
[-82.44728, 40.05500],
[-82.44589, 40.05633],
[-82.44389, 40.05698],
[-82.44216, 40.05595],
[-82.44123, 40.05443],
];
// Tile names to load - merged from all KNOWN_SITES
const TILE_NAMES = (() => {
const allTiles = new Set();
KNOWN_SITES.forEach(site => {
if (site.tiles) {
site.tiles.forEach(tile => allTiles.add(tile));
}
});
return Array.from(allTiles);
})();
const octagonCenter = [-82.44383, 40.05469];
@@ -521,12 +529,64 @@ function updateBaseLayer() {
}
}
function toggleOctagon() {
function toggleGeometry() {
if (!map) return;
const visibility = showOctagon.value ? 'visible' : 'none';
map.setLayoutProperty('octagon-fill', 'visibility', visibility);
map.setLayoutProperty('octagon-outline', 'visibility', visibility);
const visibility = showGeometry.value ? 'visible' : 'none';
if (map.getLayer('geometry-pins')) {
map.setLayoutProperty('geometry-pins', 'visibility', visibility);
}
if (map.getLayer('geometry-lines')) {
map.setLayoutProperty('geometry-lines', 'visibility', visibility);
}
if (map.getLayer('geometry-labels')) {
map.setLayoutProperty('geometry-labels', 'visibility', visibility);
}
}
function toggleSite(siteName) {
if (!map) return;
const visible = visibleSites.value[siteName];
const visibility = visible ? 'visible' : 'none';
// Toggle marker
const markerLayerId = `site-marker-${siteName}`;
if (map.getLayer(markerLayerId)) {
map.setLayoutProperty(markerLayerId, 'visibility', visibility);
}
// Toggle overlay geometry
const overlayLayerId = `site-overlay-${siteName}`;
if (map.getLayer(overlayLayerId)) {
map.setLayoutProperty(overlayLayerId, 'visibility', visibility);
}
// Toggle polyline
const polylineLayerId = `site-polyline-${siteName}`;
if (map.getLayer(polylineLayerId)) {
map.setLayoutProperty(polylineLayerId, 'visibility', visibility);
}
}
function jumpToSite(site) {
if (!map) return;
const coords = site.coordinates[0];
map.flyTo({
center: coords,
zoom: 15,
duration: 1500
});
}
function hideAllHistoricMarkers() {
allHistoricMarkersHidden.value = !allHistoricMarkersHidden.value;
KNOWN_SITES.forEach(site => {
visibleSites.value[site.name] = !allHistoricMarkersHidden.value;
toggleSite(site.name);
});
}
function toggleLidar() {
@@ -553,21 +613,6 @@ function updateLidarOpacity() {
}
}
function toggleGeometry() {
if (!map) return;
const visibility = showGeometry.value ? 'visible' : 'none';
if (map.getLayer('geometry-pins')) {
map.setLayoutProperty('geometry-pins', 'visibility', visibility);
}
if (map.getLayer('geometry-lines')) {
map.setLayoutProperty('geometry-lines', 'visibility', visibility);
}
if (map.getLayer('geometry-labels')) {
map.setLayoutProperty('geometry-labels', 'visibility', visibility);
}
}
function openSandbox() {
if (Object.keys(tileCache.value).length > 0) {
sandboxVisible.value = true;
@@ -826,12 +871,17 @@ onMounted(() => {
const featureData = geometryFeatures.value.features.find(f => f.id === feature.properties.id);
if (featureData) {
const coords = featureData.geometry.type === 'Point'
? featureData.geometry.coordinates
: featureData.geometry.coordinates[0]; // Use first point of line
popup.value = {
visible: true,
x: e.point.x,
y: e.point.y,
type: featureData.properties.type,
feature: featureData
feature: featureData,
lngLat: coords
};
}
} else {
@@ -870,37 +920,144 @@ onMounted(() => {
});
map.on('load', async () => {
// Add octagon overlay
map.addSource('octagon', {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [octagonCoords]
// Initialize visibility state for all sites
KNOWN_SITES.forEach(site => {
visibleSites.value[site.name] = true;
});
// Add historic site markers and overlays
KNOWN_SITES.forEach(site => {
// Add marker
const markerCoords = site.coordinates[0];
map.addSource(`site-marker-${site.name}`, {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Point',
coordinates: markerCoords
},
properties: {
name: site.name,
description: site.description,
type: site.type
}
}
});
map.addLayer({
id: `site-marker-${site.name}`,
type: 'circle',
source: `site-marker-${site.name}`,
paint: {
'circle-radius': 10,
'circle-color': site.type === 'road_confirmed' ? '#4A9EFF' : '#ff6b6b',
'circle-stroke-width': 3,
'circle-stroke-color': '#ffffff'
}
});
// Add overlay geometry if present
if (site.overlay) {
site.overlay.forEach((geom, idx) => {
map.addSource(`site-overlay-${site.name}-${idx}`, {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: [geom.coordinates]
}
}
});
map.addLayer({
id: `site-overlay-${site.name}-${idx}`,
type: 'fill',
source: `site-overlay-${site.name}-${idx}`,
paint: {
'fill-color': site.type === 'road_confirmed' ? '#4A9EFF' : '#ff6b6b',
'fill-opacity': 0.2
}
});
map.addLayer({
id: `site-overlay-outline-${site.name}-${idx}`,
type: 'line',
source: `site-overlay-${site.name}-${idx}`,
paint: {
'line-color': site.type === 'road_confirmed' ? '#4A9EFF' : '#ff6b6b',
'line-width': 2
}
});
});
}
});
map.addLayer({
id: 'octagon-fill',
type: 'fill',
source: 'octagon',
paint: {
'fill-color': '#ff0000',
'fill-opacity': 0.2
// Add polyline for multi-coordinate sites
if (site.coordinates.length > 1) {
map.addSource(`site-polyline-${site.name}`, {
type: 'geojson',
data: {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: site.coordinates
}
}
});
map.addLayer({
id: `site-polyline-${site.name}`,
type: 'line',
source: `site-polyline-${site.name}`,
paint: {
'line-color': '#4A9EFF',
'line-width': 3,
'line-dasharray': [2, 2]
}
});
}
// Add click handler for marker popups
map.on('click', `site-marker-${site.name}`, (e) => {
e.preventDefault();
popup.value = {
visible: true,
x: e.point.x,
y: e.point.y,
type: 'site',
feature: {
properties: {
name: site.name,
description: site.description,
type: site.type
}
},
lngLat: site.coordinates[0]
};
});
// Cursor pointer on hover
map.on('mouseenter', `site-marker-${site.name}`, () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', `site-marker-${site.name}`, () => {
map.getCanvas().style.cursor = '';
});
});
map.addLayer({
id: 'octagon-outline',
type: 'line',
source: 'octagon',
paint: {
'line-color': '#ff0000',
'line-width': 2
// Update popup position when map moves or zooms
const updatePopupPosition = () => {
if (popup.value.visible && popup.value.lngLat) {
const point = map.project(popup.value.lngLat);
popup.value.x = point.x;
popup.value.y = point.y;
}
});
};
map.on('move', updatePopupPosition);
map.on('zoom', updatePopupPosition);
// Add geometry source and layers
map.addSource('geometry', {
@@ -1023,6 +1180,69 @@ onUnmounted(() => {
cursor: default;
}
.control-section .section-header {
font-weight: bold;
margin-bottom: 8px;
user-select: none;
}
.subsection {
margin-left: 10px;
padding-left: 10px;
border-left: 2px solid #ddd;
}
.hide-all-btn {
width: 100%;
padding: 6px 10px;
margin: 8px 0;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.hide-all-btn:hover {
background: #e8e8e8;
border-color: #999;
}
.site-control {
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0;
}
.site-control label {
flex: 1;
margin: 0 !important;
font-weight: normal !important;
}
.jump-btn {
padding: 4px 8px;
background: #4A9EFF;
color: white;
border: none;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
margin-left: 8px;
}
.jump-btn:hover {
background: #2E8FE3;
}
.jump-btn:active {
transform: scale(0.95);
}
.control-section input[type="radio"],
.control-section input[type="checkbox"] {
margin-right: 8px;
@@ -1153,6 +1373,7 @@ onUnmounted(() => {
box-shadow: 0 2px 12px rgba(0,0,0,0.4);
z-index: 1000;
min-width: 180px;
max-width: 350px;
overflow: hidden;
}
@@ -1173,6 +1394,24 @@ onUnmounted(() => {
color: #666;
}
.site-popup {
max-width: 320px;
}
.site-type {
font-size: 11px !important;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #999 !important;
margin-bottom: 8px !important;
}
.site-description {
line-height: 1.5;
color: #333 !important;
font-size: 13px !important;
}
.popup-btn {
display: block;
width: 100%;