|
| 1 | +# Global Tsunami Hazard Vector Tile Visualization |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +This document describes the implementation and rendering of global tsunami hazard vector tiles in the geospatial dataset publishing platform. The visualization displays over 200,000 tsunami hazard points worldwide with interactive charts and risk-based color coding. |
| 6 | + |
| 7 | +## Data Structure |
| 8 | + |
| 9 | +### Source Dataset |
| 10 | + |
| 11 | +- **Format**: GeoJSON (94MB) converted to FlatGeobuf (64MB) for efficient streaming |
| 12 | +- **Points**: ~200,000 global tsunami hazard locations |
| 13 | +- **Coordinate System**: WGS84 (EPSG:4326) |
| 14 | +- **Tile Format**: Mapbox Vector Tiles (MVT) using Tippecanoe |
| 15 | + |
| 16 | +### Feature Properties |
| 17 | + |
| 18 | +Each tsunami hazard point contains the following attributes: |
| 19 | + |
| 20 | +#### Core Location Data |
| 21 | + |
| 22 | +- `Longitude`: Longitude coordinate |
| 23 | +- `Latitude`: Latitude coordinate |
| 24 | +- `id`: Unique identifier |
| 25 | + |
| 26 | +#### Annual Recurrence Interval (ARI) Runup Heights |
| 27 | + |
| 28 | +- `ari10`: 10-year return period runup height (meters) |
| 29 | +- `ari50`: 50-year return period runup height (meters) |
| 30 | +- `ari100`: 100-year return period runup height (meters) |
| 31 | +- `ari200`: 200-year return period runup height (meters) |
| 32 | +- `ari500`: 500-year return period runup height (meters) |
| 33 | +- `ari1000`: 1000-year return period runup height (meters) |
| 34 | +- `ari2500`: 2500-year return period runup height (meters) |
| 35 | + |
| 36 | +#### Statistical Variants (500-year focus) |
| 37 | + |
| 38 | +- `ari500LL`: 500-year ARI with sigma=0.5 |
| 39 | +- `ari500ZL`: 500-year ARI with sigma=0.0 |
| 40 | +- `ari500M`: 500-year ARI lower 95% confidence |
| 41 | +- `ari500P`: 500-year ARI upper 95% confidence |
| 42 | + |
| 43 | +#### Rate Data |
| 44 | + |
| 45 | +- `rate_5`: Exceedance rate for 5m threshold |
| 46 | +- `rate_10`: Exceedance rate for 10m threshold |
| 47 | +- `rate_25`: Exceedance rate for 25m threshold |
| 48 | +- `rate_50`: Exceedance rate for 50m threshold |
| 49 | +- `rate_100`: Exceedance rate for 100m threshold |
| 50 | +- `rate_300`: Exceedance rate for 300m threshold |
| 51 | +- `rate_500`: Exceedance rate for 500m threshold |
| 52 | +- `rate_1000`: Exceedance rate for 1000m threshold |
| 53 | +- `rate_2000`: Exceedance rate for 2000m threshold |
| 54 | + |
| 55 | +## Vector Tile Configuration |
| 56 | + |
| 57 | +### Tile Service Setup |
| 58 | + |
| 59 | +```yaml |
| 60 | +# pygeoapi configuration |
| 61 | +collections: |
| 62 | + hazardglobal: |
| 63 | + type: collection |
| 64 | + title: Global Tsunami Hazard Points |
| 65 | + description: Global tsunami hazard points with ARI runup heights |
| 66 | + provider: |
| 67 | + name: MVT-tippecanoe |
| 68 | + data: data/tiles/global-hazard/ |
| 69 | + format: pbf |
| 70 | +``` |
| 71 | +
|
| 72 | +### Tippecanoe Generation |
| 73 | +
|
| 74 | +```bash |
| 75 | +# Generate vector tiles from source data |
| 76 | +make hazard-tiles |
| 77 | + |
| 78 | +# Equivalent docker command: |
| 79 | +docker run --rm -v $(pwd)/data:/data \ |
| 80 | + tippecanoe/tippecanoe:latest \ |
| 81 | + tippecanoe \ |
| 82 | + --output-to-directory=/data/tiles/global-hazard/ \ |
| 83 | + --force \ |
| 84 | + --minimum-zoom=0 \ |
| 85 | + --maximum-zoom=15 \ |
| 86 | + --layer=globalhazardpoints \ |
| 87 | + /data/hazard/global-hazard-points.fgb |
| 88 | +``` |
| 89 | + |
| 90 | +## Rendering Implementation |
| 91 | + |
| 92 | +### 1. MapLibre GL JS Layer Configuration |
| 93 | + |
| 94 | +#### Vector Source |
| 95 | + |
| 96 | +```javascript |
| 97 | +map.addSource("global-hazard-source", { |
| 98 | + type: "vector", |
| 99 | + tiles: [ |
| 100 | + `${apiBaseUrl}/collections/hazardglobal/tiles/WebMercatorQuad/{z}/{y}/{x}?f=mvt`, |
| 101 | + ], |
| 102 | + minzoom: 0, |
| 103 | + maxzoom: 15, |
| 104 | + bounds: [-180, -90, 180, 90], |
| 105 | +}); |
| 106 | +``` |
| 107 | + |
| 108 | +#### Circle Layer with Dynamic Styling |
| 109 | + |
| 110 | +```javascript |
| 111 | +map.addLayer({ |
| 112 | + id: "hazard-pt", |
| 113 | + type: "circle", |
| 114 | + source: "global-hazard-source", |
| 115 | + "source-layer": "globalhazardpoints", |
| 116 | + layout: { |
| 117 | + visibility: "visible", |
| 118 | + }, |
| 119 | + paint: { |
| 120 | + // Dynamic sizing based on zoom level |
| 121 | + "circle-radius": [ |
| 122 | + "interpolate", |
| 123 | + ["linear"], |
| 124 | + ["zoom"], |
| 125 | + 0, |
| 126 | + 1, // Continental view: 1px dots |
| 127 | + 5, |
| 128 | + 2, // Regional view: 2px dots |
| 129 | + 10, |
| 130 | + 4, // Local view: 4px dots |
| 131 | + 15, |
| 132 | + 8, // Detailed view: 8px dots |
| 133 | + ], |
| 134 | + |
| 135 | + // Risk-based color coding using ari500 values |
| 136 | + "circle-color": [ |
| 137 | + "case", |
| 138 | + ["has", "ari500"], |
| 139 | + [ |
| 140 | + "interpolate", |
| 141 | + ["linear"], |
| 142 | + ["get", "ari500"], |
| 143 | + 0, |
| 144 | + "#4CAF50", // Green: Low risk (0-5m) |
| 145 | + 5, |
| 146 | + "#FFC107", // Yellow: Medium risk (5-10m) |
| 147 | + 10, |
| 148 | + "#FF9800", // Orange: High risk (10-20m) |
| 149 | + 20, |
| 150 | + "#F44336", // Red: Very high risk (>20m) |
| 151 | + ], |
| 152 | + "#1a73e8", // Default blue for missing data |
| 153 | + ], |
| 154 | + |
| 155 | + // Progressive opacity for performance |
| 156 | + "circle-opacity": [ |
| 157 | + "interpolate", |
| 158 | + ["linear"], |
| 159 | + ["zoom"], |
| 160 | + 0, |
| 161 | + 0.6, // More transparent at low zoom |
| 162 | + 10, |
| 163 | + 0.8, // Medium opacity at mid zoom |
| 164 | + 15, |
| 165 | + 0.9, // Full opacity at high zoom |
| 166 | + ], |
| 167 | + |
| 168 | + // Zoom-dependent stroke |
| 169 | + "circle-stroke-width": [ |
| 170 | + "interpolate", |
| 171 | + ["linear"], |
| 172 | + ["zoom"], |
| 173 | + 0, |
| 174 | + 0, // No stroke at low zoom |
| 175 | + 10, |
| 176 | + 0.5, // Thin stroke at mid zoom |
| 177 | + 15, |
| 178 | + 1, // Full stroke at high zoom |
| 179 | + ], |
| 180 | + "circle-stroke-color": "#ffffff", |
| 181 | + }, |
| 182 | +}); |
| 183 | +``` |
| 184 | + |
| 185 | +### 2. Interactive Popup System |
| 186 | + |
| 187 | +#### Popup Configuration |
| 188 | + |
| 189 | +```javascript |
| 190 | +const popup = new maplibregl.Popup({ |
| 191 | + maxWidth: "480px", |
| 192 | + className: "chart-popup", |
| 193 | + closeOnClick: false, |
| 194 | + closeOnMove: false, |
| 195 | + anchor: "bottom", |
| 196 | + offset: [0, -10], |
| 197 | +}); |
| 198 | +``` |
| 199 | + |
| 200 | +#### Smart Positioning |
| 201 | + |
| 202 | +The popup system includes intelligent positioning to prevent jumping outside the viewport: |
| 203 | + |
| 204 | +```javascript |
| 205 | +// Adjust popup position if it goes off screen |
| 206 | +setTimeout(() => { |
| 207 | + const popupElement = popup.getElement(); |
| 208 | + const rect = popupElement.getBoundingClientRect(); |
| 209 | + const mapContainer = map.getContainer().getBoundingClientRect(); |
| 210 | + |
| 211 | + // Check boundaries and adjust offset |
| 212 | + if (rect.right > mapContainer.right) { |
| 213 | + popup.setOffset([-rect.width / 2, -10]); |
| 214 | + } |
| 215 | + if (rect.left < mapContainer.left) { |
| 216 | + popup.setOffset([rect.width / 2, -10]); |
| 217 | + } |
| 218 | + if (rect.top < mapContainer.top) { |
| 219 | + popup.setOffset([0, 10]); |
| 220 | + } |
| 221 | +}, 50); |
| 222 | +``` |
| 223 | + |
| 224 | +### 3. Chart.js Integration |
| 225 | + |
| 226 | +#### ARI Runup Height Visualization |
| 227 | + |
| 228 | +```javascript |
| 229 | +const ariKeys = [ |
| 230 | + "ari10", |
| 231 | + "ari50", |
| 232 | + "ari100", |
| 233 | + "ari200", |
| 234 | + "ari500", |
| 235 | + "ari1000", |
| 236 | + "ari2500", |
| 237 | +]; |
| 238 | + |
| 239 | +const datasets = [ |
| 240 | + { |
| 241 | + label: "Runup height (m)", |
| 242 | + data: ariKeys.map((k) => ({ |
| 243 | + x: Number(k.replace("ari", "")), |
| 244 | + y: Number(properties[k] || 0), |
| 245 | + })), |
| 246 | + borderColor: "#1a73e8", |
| 247 | + backgroundColor: "rgba(26, 115, 232, 0.1)", |
| 248 | + fill: true, |
| 249 | + tension: 0.4, |
| 250 | + }, |
| 251 | +]; |
| 252 | +``` |
| 253 | + |
| 254 | +#### Statistical Variants Display |
| 255 | + |
| 256 | +```javascript |
| 257 | +const variants = [ |
| 258 | + ["ari500LL", "σ=0.5", "#FF9800"], |
| 259 | + ["ari500ZL", "σ=0.0", "#4CAF50"], |
| 260 | + ["ari500M", "Lower 95%", "#F44336"], |
| 261 | + ["ari500P", "Upper 95%", "#9C27B0"], |
| 262 | +]; |
| 263 | + |
| 264 | +variants.forEach(([key, label, color]) => { |
| 265 | + if (properties[key] !== undefined) { |
| 266 | + datasets.push({ |
| 267 | + label, |
| 268 | + data: [{ x: 500, y: Number(properties[key]) }], |
| 269 | + showLine: false, |
| 270 | + pointRadius: 5, |
| 271 | + pointBackgroundColor: color, |
| 272 | + pointBorderColor: "#ffffff", |
| 273 | + pointBorderWidth: 2, |
| 274 | + }); |
| 275 | + } |
| 276 | +}); |
| 277 | +``` |
| 278 | + |
| 279 | +## Performance Optimization |
| 280 | + |
| 281 | +### 1. Vector Tile Advantages |
| 282 | + |
| 283 | +- **Efficient Loading**: 64MB FlatGeobuf vs 94MB GeoJSON (32% reduction) |
| 284 | +- **Progressive Loading**: Only visible tiles loaded based on zoom/pan |
| 285 | +- **Client-side Styling**: No server re-rendering needed for style changes |
| 286 | +- **Scalable**: Handles 200K+ points smoothly |
| 287 | + |
| 288 | +### 2. Zoom-based LOD (Level of Detail) |
| 289 | + |
| 290 | +- **Zoom 0-5**: Minimal 1-2px dots for overview |
| 291 | +- **Zoom 6-10**: Medium 2-4px dots for regional view |
| 292 | +- **Zoom 11-15**: Large 4-8px dots for detailed analysis |
| 293 | + |
| 294 | +### 3. Conditional Rendering |
| 295 | + |
| 296 | +- **Opacity ramping**: Reduces visual clutter at low zoom levels |
| 297 | +- **Stroke simplification**: No borders at low zoom for performance |
| 298 | +- **Color computation**: Only when ari500 data exists |
| 299 | + |
| 300 | +## Risk Assessment Color Scheme |
| 301 | + |
| 302 | +The visualization uses a scientifically-informed color scheme based on tsunami runup heights: |
| 303 | + |
| 304 | +| Runup Height | Risk Level | Color | Hex Code | Description | |
| 305 | +| ------------ | ---------- | ------ | -------- | --------------------------------- | |
| 306 | +| 0-5m | Low | Green | #4CAF50 | Minimal infrastructure damage | |
| 307 | +| 5-10m | Medium | Yellow | #FFC107 | Moderate coastal impact | |
| 308 | +| 10-20m | High | Orange | #FF9800 | Significant infrastructure damage | |
| 309 | +| >20m | Very High | Red | #F44336 | Catastrophic damage potential | |
| 310 | + |
| 311 | +## Data Export Features |
| 312 | + |
| 313 | +### CSV Download |
| 314 | + |
| 315 | +Each popup includes a download button that generates a CSV file with all point attributes: |
| 316 | + |
| 317 | +```javascript |
| 318 | +const header = Object.keys(properties).join(","); |
| 319 | +const values = Object.values(properties).join(","); |
| 320 | +const csv = `${header}\n${values}`; |
| 321 | +const blob = new Blob([csv], { type: "text/csv" }); |
| 322 | +``` |
| 323 | + |
| 324 | +## Technical Requirements |
| 325 | + |
| 326 | +### Browser Support |
| 327 | + |
| 328 | +- **Modern browsers** with WebGL support |
| 329 | +- **MapLibre GL JS**: Vector tile rendering engine |
| 330 | +- **Chart.js**: Interactive chart generation |
| 331 | + |
| 332 | +### Server Components |
| 333 | + |
| 334 | +- **pygeoapi**: OGC API compliance and MVT serving |
| 335 | +- **Tippecanoe**: Vector tile generation |
| 336 | +- **Docker**: Containerized tile generation |
| 337 | + |
| 338 | +## Usage Instructions |
| 339 | + |
| 340 | +### 1. Basic Interaction |
| 341 | + |
| 342 | +- **Pan/Zoom**: Standard map navigation |
| 343 | +- **Click point**: Opens interactive popup with chart |
| 344 | +- **Hover**: Cursor changes to pointer over points |
| 345 | + |
| 346 | +### 2. Popup Features |
| 347 | + |
| 348 | +- **Runup Chart**: Shows ARI values across return periods |
| 349 | +- **Statistical Variants**: Uncertainty quantification at 500-year ARI |
| 350 | +- **Properties**: Expandable table with all attributes |
| 351 | +- **CSV Export**: Download point data |
| 352 | + |
| 353 | +### 3. Map Controls |
| 354 | + |
| 355 | +- **Satellite Toggle**: Switch between map and satellite view |
| 356 | +- **Navigation**: Zoom in/out and compass controls |
| 357 | +- **Full Screen**: Responsive design with fixed viewport |
| 358 | + |
| 359 | +## Configuration Options |
| 360 | + |
| 361 | +### Environment Variables |
| 362 | + |
| 363 | +```bash |
| 364 | +API_BASE_URL=http://localhost:5000 # API endpoint |
| 365 | +``` |
| 366 | + |
| 367 | +### Build Process |
| 368 | + |
| 369 | +```bash |
| 370 | +cd frontend |
| 371 | +npm run build # Compiles and copies assets to dist/ |
| 372 | +``` |
| 373 | + |
| 374 | +## Future Enhancements |
| 375 | + |
| 376 | +### Planned Features |
| 377 | + |
| 378 | +1. **Clustering**: Point aggregation at low zoom levels |
| 379 | +2. **Time Series**: Animation through different return periods |
| 380 | +3. **Filtering**: Risk-level and region-based filtering |
| 381 | +4. **3D Visualization**: Height-based extrusion of runup values |
| 382 | + |
| 383 | +### Performance Improvements |
| 384 | + |
| 385 | +1. **WebGL Point Rendering**: For even better performance with 200K+ points |
| 386 | +2. **Adaptive Simplification**: Dynamic point reduction based on screen density |
| 387 | +3. **Caching**: Browser-based tile caching for offline usage |
| 388 | + |
| 389 | +--- |
| 390 | + |
| 391 | +_This documentation covers the complete implementation of tsunami hazard vector tile visualization. For technical support or feature requests, please refer to the project repository._ |
0 commit comments