Add NASA GIBS satellite basemap world map tab (Leaflet)
Some checks failed
build-and-deploy / build (push) Failing after 5s

Interactive world map panel for the dashboard:
- Vendored Leaflet 1.9.4 under app/static/vendor/leaflet/ (served via
  new /static mount on the FastAPI app)
- New Map tab: GIBS WMTS raster basemap (BlueMarble_ShadedRelief_Bathymetry,
  VIIRS/MODIS/Aqua CorrectedReflectance_TrueColor, VIIRS_DayNightBand),
  layer picker + UTC date selector (Latest/-7d/-30d quick picks)
- /api/map/layers: curated GIBS catalog (tms, format, has_time, max_zoom)
- /api/map/times: per-layer date windows from GIBS Domains XML (6h cache,
  graceful 502 on GIBS hiccup; static layers 422)
- Fire hotspots (existing /api/fires) + open cameras (/api/cameras) as
  toggleable bbox-scoped overlays; dark UI matching dashboard theme
This commit is contained in:
Sirius DevOps 2026-08-24 17:35:44 -04:00
parent 91f436390b
commit 3aa6f265bb
10 changed files with 1142 additions and 2 deletions

157
app/gibs_map.py Normal file
View file

@ -0,0 +1,157 @@
"""NASA GIBS basemap catalog + time-domain helper for the OSINT map tab.
GIBS (Global Imagery Browse Services) is what worldview.earthdata.nasa.gov
renders from. Tiles are plain XYZ/WebMercator WMTS rasters, CORS-open
(access-control-allow-origin: *), so the browser can pull them directly
no tile proxy needed.
Layer metadata is curated here (identifier, display title, tile matrix set,
format, whether it has a Time dimension). Available time windows per layer
come from GIBS' per-layer Domains XML endpoint (tiny), fetched on demand and
cached for a few hours.
Tile URL template (RESTful WMTS):
https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/{layer}/default/{time}/{tms}/{z}/{y}/{x}.{ext}
The {time} path segment is omitted entirely for static layers (no Time dim).
"""
from __future__ import annotations
import asyncio
import re
import time
from datetime import date, datetime, timedelta
import httpx
GIBS_BASE = "https://gibs.earthdata.nasa.gov/wmts/epsg3857/best"
GIBS_TIMEOUT = 30.0
# ── Curated basemap catalog ────────────────────────────────────────────────
# has_time: True → tile URL includes /default/{time}/; the /api/map/times
# endpoint returns the valid date windows. False → static basemap.
MAP_LAYERS = [
{
"id": "BlueMarble_ShadedRelief_Bathymetry",
"title": "Blue Marble (shaded relief + bathymetry)",
"subtitle": "Static MODIS composite basemap",
"tms": "GoogleMapsCompatible_Level8",
"format": "jpeg",
"has_time": False,
"max_zoom": 8,
},
{
"id": "VIIRS_SNPP_CorrectedReflectance_TrueColor",
"title": "VIIRS S-NPP True Color (daily)",
"subtitle": "Suomi NPP corrected reflectance",
"tms": "GoogleMapsCompatible_Level9",
"format": "jpeg",
"has_time": True,
"max_zoom": 9,
},
{
"id": "MODIS_Terra_CorrectedReflectance_TrueColor",
"title": "MODIS Terra True Color (daily)",
"subtitle": "Terra corrected reflectance",
"tms": "GoogleMapsCompatible_Level9",
"format": "jpeg",
"has_time": True,
"max_zoom": 9,
},
{
"id": "MODIS_Aqua_CorrectedReflectance_TrueColor",
"title": "MODIS Aqua True Color (daily)",
"subtitle": "Aqua corrected reflectance",
"tms": "GoogleMapsCompatible_Level9",
"format": "jpeg",
"has_time": True,
"max_zoom": 9,
},
{
"id": "VIIRS_SNPP_DayNightBand_ENCC",
"title": "VIIRS Night Lights (DNB)",
"subtitle": "Earth at night, enhanced near-constant contrast",
"tms": "GoogleMapsCompatible_Level8",
"format": "png",
"has_time": True,
"max_zoom": 8,
},
]
_LAYER_BY_ID = {l["id"]: l for l in MAP_LAYERS}
# ── Time-domain cache (per layer) ──────────────────────────────────────────
# GIBS Domains XML gives a comma-joined list of ISO ranges: START/END/PERIOD
_DOMAIN_CACHE: dict[str, tuple[float, dict]] = {}
_DOMAIN_TTL = 6 * 3600 # 6h
_HTTPS_LOCK = asyncio.Lock()
# e.g. 2026-07-16/2026-08-24/P1D
_TIME_RANGE_RE = re.compile(
r"^(\d{4}-\d{2}-\d{2})/(\d{4}-\d{2}-\d{2})/(P\d+D|P\d+M|PT\d+H)$"
)
def _parse_time_domain(domain: str) -> dict:
"""Expand a Domains XML <Domain> string into usable date metadata.
Returns {min, max, latest, ranges:[{start,end}]} where ranges keep the
raw GIBS windows so the frontend can warn when a picked date is outside
every window (GIBS still serves nearest-time tiles, but this is honest).
"""
windows = []
min_d, max_d = None, None
for part in domain.split(","):
part = part.strip()
m = _TIME_RANGE_RE.match(part)
if not m:
continue
start = date.fromisoformat(m.group(1))
end = date.fromisoformat(m.group(2))
windows.append({"start": start.isoformat(), "end": end.isoformat()})
if min_d is None or start < min_d:
min_d = start
if max_d is None or end > max_d:
max_d = end
return {
"min": min_d.isoformat() if min_d else None,
"max": max_d.isoformat() if max_d else None,
"latest": max_d.isoformat() if max_d else None,
"ranges": windows,
}
async def fetch_layer_domain(layer_id: str, tms: str) -> dict:
"""Return parsed time domain for a layer, cached for _DOMAIN_TTL."""
now = time.time()
hit = _DOMAIN_CACHE.get(layer_id)
if hit and now - hit[0] < _DOMAIN_TTL:
return hit[1]
url = (
f"{GIBS_BASE}/1.0.0/{layer_id}/default/{tms}/all/all.xml"
)
# One in-flight request per layer; serialize so a burst of layer switches
# doesn't fan out parallel GIBS hits.
async with _HTTPS_LOCK:
hit = _DOMAIN_CACHE.get(layer_id)
if hit and time.time() - hit[0] < _DOMAIN_TTL:
return hit[1]
async with httpx.AsyncClient(timeout=GIBS_TIMEOUT) as client:
resp = await client.get(url)
resp.raise_for_status()
m = re.search(r"<Domain>([^<]+)</Domain>", resp.text)
if not m:
raise ValueError(f"No time <Domain> in GIBS response for {layer_id}")
parsed = _parse_time_domain(m.group(1))
_DOMAIN_CACHE[layer_id] = (time.time(), parsed)
return parsed
def default_time_for(layer_id: str) -> str | None:
"""Most recent data date for a time-aware layer (best effort, from cache)."""
hit = _DOMAIN_CACHE.get(layer_id)
if not hit:
return None
latest = hit[1].get("latest")
return latest or None

View file

@ -21,6 +21,7 @@ from uuid import UUID
import structlog
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import FileResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy import and_, func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
@ -841,6 +842,52 @@ async def index():
return FileResponse(str(STATIC_DIR / "index.html"))
# ── NASA GIBS basemap map tab ─────────────────────────────────────────────
@app.get("/api/map/layers")
async def map_layers():
"""Curated NASA GIBS raster basemap layers for the map tab.
Each entry has everything the browser needs to render the WMTS tiles:
id GIBS layer identifier (used in the tile URL path)
title human-readable display name
tms GIBS tile matrix set (GoogleMapsCompatible_LevelN)
format tile image extension (jpeg|png)
has_time whether the layer has a Time dimension (=> date selector)
max_zoom highest native zoom served by that tile matrix set
"""
from gibs_map import MAP_LAYERS
return {"layers": MAP_LAYERS}
@app.get("/api/map/times")
async def map_layer_times(
layer: str = Query(..., description="GIBS layer identifier, e.g. VIIRS_SNPP_CorrectedReflectance_TrueColor"),
):
"""Available date windows for a time-aware GIBS layer.
Parsed from the layer's Domains XML (GIBS serves nearest-time tiles even
for dates slightly outside a window, but we surface the real ranges so
the UI can clamp the picker and flag out-of-range picks).
"""
from gibs_map import _LAYER_BY_ID, fetch_layer_domain
meta = _LAYER_BY_ID.get(layer)
if not meta:
raise HTTPException(404, f"Unknown GIBS layer: {layer}")
if not meta["has_time"]:
raise HTTPException(422, f"Layer '{layer}' is static (no Time dimension)")
try:
domain = await fetch_layer_domain(meta["id"], meta["tms"])
except Exception as exc: # network / GIBS hiccup → degrade gracefully
logger.warning("gibs_domain_fetch_failed", layer=layer, error=str(exc))
raise HTTPException(502, f"GIBS time domain unavailable: {exc}")
return {"layer": layer, **domain}
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View file

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OSINT Dashboard</title>
<link rel="stylesheet" href="/static/vendor/leaflet/leaflet.css">
<style>
:root { --bg: #0f172a; --surface: #1e293b; --border: #334155; --text: #e2e8f0; --muted: #94a3b8; --accent: #38bdf8; --green: #4ade80; --red: #f87171; --yellow: #fbbf24; }
* { margin: 0; padding: 0; box-sizing: border-box; }
@ -60,6 +61,30 @@
.key-msg { margin-top: 0.5rem; font-size: 0.8rem; min-height: 1rem; }
.key-msg.ok { color: var(--green); }
.key-msg.err { color: var(--red); }
/* ── Map tab ─────────────────────────────────────────────── */
.map-toolbar { display: flex; flex-wrap: wrap; gap: 0.75rem; align-items: flex-end; margin-bottom: 0.9rem; }
.map-toolbar label { display: flex; flex-direction: column; gap: 0.3rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--muted); }
.map-toolbar select, .map-toolbar input[type="date"], .map-toolbar input[type="datetime-local"] {
background: var(--surface); color: var(--text); border: 1px solid var(--border);
border-radius: 6px; padding: 0.5rem 0.6rem; font-size: 0.85rem;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace; color-scheme: dark;
}
.map-toolbar select:focus, .map-toolbar input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 8px rgba(56,189,248,0.35); }
.map-toolbar .btn { white-space: nowrap; }
.map-toolbar .btn.active { background: var(--accent); color: #0f172a; }
.map-hint { font-size: 0.78rem; color: var(--muted); align-self: center; flex: 1 1 100%; margin-top: -0.2rem; }
#map { width: 100%; height: 70vh; min-height: 420px; border-radius: 10px; border: 1px solid var(--border); z-index: 0; background: #0b1220; }
.leaflet-container { background: #0b1220; font-family: inherit; }
.leaflet-control-zoom a { background: var(--surface) !important; color: var(--text) !important; border-color: var(--border) !important; }
.leaflet-control-zoom a:hover { background: #273449 !important; }
.leaflet-control-attribution { background: rgba(15,23,42,0.85) !important; color: var(--muted) !important; font-size: 10px !important; }
.leaflet-control-attribution a { color: var(--accent) !important; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip { background: var(--surface) !important; color: var(--text) !important; border: 1px solid var(--border); }
.leaflet-popup-content-wrapper a { color: var(--accent); }
.leaflet-popup-content { font-size: 0.82rem; }
.leaflet-popup-close-button { color: var(--muted) !important; }
.leaflet-overlay-pane svg { filter: drop-shadow(0 0 3px rgba(0,0,0,0.6)); }
</style>
</head>
<body>
@ -106,6 +131,7 @@
<button class="btn" onclick="showTab('entities')">Entities</button>
<button class="btn" onclick="showTab('ingest')">Ingest</button>
<button class="btn" onclick="showTab('keys')">Keys</button>
<button class="btn" onclick="showTab('map')">Map</button>
</div>
<!-- Recent Events -->
@ -172,6 +198,28 @@
<div class="key-grid" id="keys-grid"></div>
</div>
<!-- Map (NASA GIBS satellite basemap) -->
<div class="section" id="tab-map" style="display:none">
<h2>Global Satellite Map</h2>
<p class="sub" style="margin-bottom:0.75rem">NASA GIBS satellite basemap (the same tiles worldview.earthdata.nasa.gov renders). Pick a layer and date, then zoom/pan — fires and open cameras can be overlaid.</p>
<div class="map-toolbar">
<label>Layer
<select id="map-layer" onchange="mapLayerChanged()"></select>
</label>
<label id="map-time-label" style="display:none">Date / Time (UTC)
<input type="date" id="map-date" onchange="mapDateChanged()">
</label>
<button class="btn" id="map-latest-btn" onclick="mapGoLatest()" style="display:none">Latest</button>
<button class="btn" id="map-7d-btn" onclick="mapGoDays(7)" style="display:none">-7d</button>
<button class="btn" id="map-30d-btn" onclick="mapGoDays(30)" style="display:none">-30d</button>
<button class="btn" id="map-fires-toggle" onclick="mapToggleFires()">● Fires</button>
<button class="btn" id="map-cams-toggle" onclick="mapToggleCams()">◉ Cameras</button>
<button class="btn" id="map-reset" onclick="mapResetView()">Reset view</button>
<div class="map-hint" id="map-hint"></div>
</div>
<div id="map"></div>
</div>
<!-- Search Results -->
<div class="section" id="search-results" style="display:none">
<h2>Search Results (<span id="search-total">0</span>)</h2>
@ -182,6 +230,7 @@
</div>
</div>
<script src="/static/vendor/leaflet/leaflet.js"></script>
<script>
const API = '';
@ -343,15 +392,16 @@ async function clearKey(name) {
}
function showTab(name) {
['recent','alerts','entities','ingest','keys'].forEach(t => {
['recent','alerts','entities','ingest','keys','map'].forEach(t => {
document.getElementById('tab-'+t).style.display = t===name?'block':'none';
});
document.querySelectorAll('.tab-bar .btn').forEach((b,i) => {
b.classList.toggle('active', ['recent','alerts','entities','ingest','keys'][i]===name);
b.classList.toggle('active', ['recent','alerts','entities','ingest','keys','map'][i]===name);
});
if (name==='alerts') loadAlerts();
if (name==='entities') loadEntities();
if (name==='keys') loadKeys();
if (name==='map') initMap();
}
async function ingestRSS() {
@ -395,6 +445,225 @@ async function checkHealth() {
}
}
// ── Map (NASA GIBS satellite basemap) ────────────────────────────────────
let map = null, mapLayer = null, mapInitStarted = false;
let firesGroup = null, camsGroup = null, firesOn = false, camsOn = false;
let mapLayers = []; // catalog from /api/map/layers
let mapDomain = null; // time windows for current layer
let mapLatest = null; // ISO date of most recent imagery
let mapAttributionAdded = null; // last attribution string added to control
async function initMap() {
if (mapInitStarted) { if (map) setTimeout(() => map.invalidateSize(), 60); return; }
mapInitStarted = true;
const hint = document.getElementById('map-hint');
hint.textContent = 'Loading layer catalog…';
try {
const r = await fetch(`${API}/api/map/layers`);
const d = await r.json();
mapLayers = d.layers || [];
const sel = document.getElementById('map-layer');
sel.innerHTML = mapLayers.map(l =>
`<option value="${l.id}">${l.title}</option>`).join('');
// default to the first time-aware layer (more interesting than static basemap)
const preferred = mapLayers.find(l => l.has_time) || mapLayers[0];
if (preferred) sel.value = preferred.id;
map = L.map('map', {
center: [25, 10], zoom: 2,
zoomControl: true,
worldCopyJump: true,
minZoom: 1,
maxZoom: 12,
attributionControl: true,
});
map.attributionControl.setPrefix('');
map.on('moveend', () => { if (firesOn) loadFires(); if (camsOn) loadCams(); });
// default selection = first layer
await mapLayerChanged();
} catch(e) {
hint.textContent = `Failed to load map layers: ${e.message || e}`;
console.error('Map init failed', e);
}
}
function setMapAttribution(text) {
if (!map) return;
if (mapAttributionAdded) map.attributionControl.removeAttribution(mapAttributionAdded);
mapAttributionAdded = text;
if (text) map.attributionControl.addAttribution(text);
}
function tileUrlFor(layerMeta, time) {
// RESTful WMTS template: .../best/{layer}/default[/{time}]/{tms}/{z}/{y}/{x}.{fmt}
const base = `https://gibs.earthdata.nasa.gov/wmts/epsg3857/best/${layerMeta.id}/default`;
const timePart = layerMeta.has_time ? `/${time || 'default'}` : '';
return `${base}${timePart}/${layerMeta.tms}/{z}/{y}/{x}.${layerMeta.format}`;
}
async function mapLayerChanged() {
const hint = document.getElementById('map-hint');
const sel = document.getElementById('map-layer');
const meta = mapLayers.find(l => l.id === sel.value);
if (!meta) return;
// Show/hide time controls depending on whether layer has a Time dimension
const hasTime = !!meta.has_time;
document.getElementById('map-time-label').style.display = hasTime ? 'flex' : 'none';
document.getElementById('map-latest-btn').style.display = hasTime ? '' : 'none';
document.getElementById('map-7d-btn').style.display = hasTime ? '' : 'none';
document.getElementById('map-30d-btn').style.display = hasTime ? '' : 'none';
let time = null;
if (hasTime) {
hint.textContent = 'Loading available dates…';
const input = document.getElementById('map-date');
try {
const r = await fetch(`${API}/api/map/times?layer=${encodeURIComponent(meta.id)}`);
mapDomain = await r.json();
mapLatest = mapDomain.latest;
if (mapDomain.min && mapDomain.max) {
input.min = mapDomain.min;
input.max = mapDomain.max;
}
input.value = mapLatest || mapDomain.max || '';
time = input.value;
} catch(e) {
// fall back to today if the times endpoint is unreachable
mapDomain = null;
const today = new Date().toISOString().slice(0,10);
input.value = today;
time = today;
}
} else {
mapDomain = null;
mapLatest = null;
time = null;
}
const url = tileUrlFor(meta, time);
const attribution = `NASA GIBS / EOSDIS · <a href="https://earthdata.nasa.gov/gibs" target="_blank" rel="noopener">${meta.id}</a>`;
if (mapLayer) {
mapLayer.setUrl(url);
mapLayer.options.maxNativeZoom = meta.max_zoom;
} else {
mapLayer = L.tileLayer(url, {
maxZoom: 12,
maxNativeZoom: meta.max_zoom,
attribution: '',
}).addTo(map);
}
setMapAttribution(attribution);
hint.textContent = hasTime
? `${meta.subtitle || meta.title} · tile date ${time || 'default'} UTC`
: `${meta.subtitle || meta.title} · static basemap`;
}
function mapDateChanged() {
const meta = mapLayers.find(l => l.id === document.getElementById('map-layer').value);
if (!meta || !meta.has_time) return;
const time = document.getElementById('map-date').value;
if (!time) return;
mapLayer.setUrl(tileUrlFor(meta, time));
document.getElementById('map-hint').textContent =
`${meta.subtitle || meta.title} · tile date ${time} UTC`;
if (mapDomain && mapDomain.ranges && mapDomain.ranges.length) {
const inRange = mapDomain.ranges.some(r => time >= r.start && time <= r.end);
if (!inRange) {
document.getElementById('map-hint').textContent += ' ⚠ outside published windows (GIBS serves nearest-time)';
}
}
}
function mapGoLatest() { document.getElementById('map-date').value = mapLatest || ''; mapDateChanged(); }
function mapGoDays(n) {
const base = mapLatest || document.getElementById('map-date').value;
if (!base) return;
const dt = new Date(base + 'T00:00:00Z');
dt.setUTCDate(dt.getUTCDate() - n);
const iso = dt.toISOString().slice(0,10);
document.getElementById('map-date').value = iso;
mapDateChanged();
}
function mapResetView() { if (map) map.setView([25, 10], 2); }
function currentBBox() {
const b = map.getBounds();
return `${b.getWest().toFixed(4)},${b.getSouth().toFixed(4)},${b.getEast().toFixed(4)},${b.getNorth().toFixed(4)}`;
}
async function mapToggleFires() {
firesOn = !firesOn;
document.getElementById('map-fires-toggle').classList.toggle('active', firesOn);
if (firesOn) await loadFires();
else if (firesGroup) { map.removeLayer(firesGroup); firesGroup = null; }
}
async function loadFires() {
if (!map) return;
try {
const r = await fetch(`${API}/api/fires?bbox=${currentBBox()}&limit=2000`);
const fires = await r.json();
if (firesGroup) map.removeLayer(firesGroup);
firesGroup = L.layerGroup(fires.map(f => {
const color = f.confidence === 'h' ? '#f87171'
: f.confidence === 'l' ? '#fbbf24' : '#fb923c';
return L.circleMarker([f.latitude, f.longitude], {
radius: Math.min(6, 2 + (f.brightness || 300) / 100),
color: color, weight: 1, fillColor: color, fillOpacity: 0.6,
}).bindPopup(
`<b>Fire hotspot</b><br>brightness: ${(f.brightness||0).toFixed(1)}K` +
`<br>FRP: ${(f.frp||0).toFixed(1)} MW<br>confidence: ${f.confidence}` +
`<br>sat: ${f.satellite} ${f.instrument || ''}<br>acquired: ${f.acq_time}`
);
}));
firesGroup.addTo(map);
document.getElementById('map-hint').textContent =
`${fires.length} fire hotspots in view`;
} catch(e) {
document.getElementById('map-hint').textContent = `Fires load failed: ${e.message || e}`;
console.error('Fires load failed', e);
}
}
async function mapToggleCams() {
camsOn = !camsOn;
document.getElementById('map-cams-toggle').classList.toggle('active', camsOn);
if (camsOn) await loadCams();
else if (camsGroup) { map.removeLayer(camsGroup); camsGroup = null; }
}
async function loadCams() {
if (!map) return;
try {
const r = await fetch(`${API}/api/cameras?bbox=${currentBBox()}&limit=500`);
const cams = await r.json();
if (camsGroup) map.removeLayer(camsGroup);
camsGroup = L.layerGroup(cams.map(c => {
const icon = L.divIcon({
className: '',
html: '<span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:#38bdf8;box-shadow:0 0 8px #38bdf8;border:1px solid #0f172a;"></span>',
iconSize: [11, 11], iconAnchor: [5, 5],
});
const camLink = c.snapshot_url
? `<br><a href="/api/cameras/${c.id}/snapshot" target="_blank">snapshot</a>`
: '';
return L.marker([c.lat, c.lon], { icon })
.bindPopup(`<b>${c.location_name || 'Open camera'}</b>` +
`<br>vendor: ${c.vendor || '?'} ${c.device_type || ''}` +
`<br>src: ${c.discovery_source || '?'}` +
(c.source_url ? `<br><a href="${c.source_url}" target="_blank" rel="noopener">source</a>` : '') +
camLink);
}));
camsGroup.addTo(map);
document.getElementById('map-hint').textContent =
`${cams.length} open cameras in view`;
} catch(e) {
document.getElementById('map-hint').textContent = `Cameras load failed: ${e.message || e}`;
console.error('Cameras load failed', e);
}
}
// Initial load
loadSummary(); loadEvents(); checkHealth();
setInterval(() => { loadSummary(); loadEvents(); checkHealth(); }, 30000);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

661
app/static/vendor/leaflet/leaflet.css vendored Normal file
View file

@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
app/static/vendor/leaflet/leaflet.js vendored Normal file

File diff suppressed because one or more lines are too long