480 lines
42 KiB
Plaintext
480 lines
42 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>PokeMaps Public Beta</title>
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
||
<meta name="color-scheme" content="dark light" />
|
||
<meta name="theme-color" content="#0ea5e9" id="themeColorMeta"/>
|
||
<link rel="icon" href="/css/yt-ukraine.svg" />
|
||
<style>
|
||
:root{
|
||
--vh:100vh;--pad:12px;--radius:14px;--fg:#fff;--bg:rgba(0,0,0,.6);--glass:blur(12px);
|
||
--marker-size:20px;--marker-color:#e53935;--marker-ring:rgba(229,57,53,.35);--ring-width:3px;
|
||
--panel:#0b0b0b;--accent:#0ea5e9;--chip:#111;--border:#222;--surface:#0a0a0a;
|
||
--sidebar-w:320px;--sidebar-min:260px;--sidebar-max:60vw
|
||
}
|
||
*{box-sizing:border-box}html,body{height:100%;margin:0}
|
||
body{background:#000;color:var(--fg);font:14px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Arial}
|
||
.app{position:fixed;inset:0;display:grid;grid-template-rows:auto 1fr}
|
||
.bar{display:flex;gap:8px;align-items:center;padding:8px var(--pad);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);background:var(--bg);z-index:5}
|
||
.search{position:relative;flex:1;min-width:0}
|
||
.search input{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--border);background:#0d0d0d;color:#fff;outline:none}
|
||
.search input:focus{border-color:#444;box-shadow:0 0 0 3px color-mix(in oklab,var(--accent) 20%,transparent)}
|
||
.suggest{position:absolute;top:calc(100% + 6px);left:0;right:0;max-height:46vh;overflow:auto;margin:0;padding:0;list-style:none;border:1px solid var(--border);border-radius:12px;background:#0d0d0d;display:none}
|
||
.suggest .head{position:sticky;top:0;background:#0f0f0f;border-bottom:1px solid #171717;padding:6px 8px;display:flex;justify-content:space-between;align-items:center;z-index:1}
|
||
.suggest .head strong{font-size:12px;opacity:.8}.suggest .head button{border:0;border-radius:8px;background:#151515;color:#fff;padding:6px 8px;cursor:pointer}
|
||
.suggest li{padding:10px 12px;cursor:pointer;border-top:1px solid #111;display:flex;gap:8px;align-items:center}
|
||
.suggest li:first-child{border-top:none}.suggest li.active{background:#141414}
|
||
.suggest .pill{font-size:11px;border:1px solid #333;border-radius:999px;padding:2px 6px;opacity:.85}
|
||
.suggest mark{background:transparent;color:var(--accent);font-weight:600}
|
||
.iconbtn{border:0;border-radius:10px;background:#111;color:#fff;padding:10px 12px;min-width:44px}
|
||
.mapwrap{position:relative;height:calc(var(--vh) - 56px);overflow:hidden}
|
||
iframe#map{position:absolute;inset:0;border:0;width:100%;height:100%}
|
||
.marker{position:absolute;left:50%;top:50%;width:var(--marker-size);height:var(--marker-size);border-radius:50%;transform:translate(-50%,-50%);background:var(--marker-color);box-shadow:0 0 0 var(--ring-width) var(--marker-ring);z-index:3;pointer-events:none}
|
||
.marker.hidden{display:none}
|
||
.marker.crosshair{background:transparent;border-radius:0;box-shadow:0 0 0 var(--ring-width) var(--marker-ring)}
|
||
.marker.crosshair::before,.marker.crosshair::after{content:"";position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:var(--marker-color)}
|
||
.marker.crosshair::before{width:calc(var(--marker-size) * 1.6);height:2px}
|
||
.marker.crosshair::after{width:2px;height:calc(var(--marker-size) * 1.6)}
|
||
.marker .dot{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:calc(var(--marker-size) * .35);height:calc(var(--marker-size) * .35);background:var(--marker-color);border-radius:50%}
|
||
.brand{position:absolute;bottom:10px;left:10px;padding:6px 10px;font-weight:600;background:var(--bg);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);border-radius:10px;z-index:4;pointer-events:none}
|
||
.coords{position:absolute;bottom:10px;right:10px;z-index:5;padding:6px 10px;border-radius:10px;background:var(--bg);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);border:1px solid var(--border);display:none;min-width:240px;text-align:right;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","DejaVu Sans Mono",monospace}
|
||
.dock{position:absolute;left:10px;bottom:52px;z-index:7;display:flex;flex-direction:column;gap:8px}
|
||
.menu,.pins,.settings{position:fixed;right:10px;z-index:8;min-width:300px;max-width:92vw;background:var(--panel);border:1px solid var(--border);border-radius:12px;display:none;overflow:hidden}
|
||
.menu{top:56px}.pins,.settings{bottom:110px;left:10px;right:auto}
|
||
.menu header,.settings header,.pins header{display:flex;justify-content:space-between;align-items:center;padding:10px 12px;border-bottom:1px solid #222}
|
||
.menu .row,.settings .row{display:flex;gap:8px;align-items:center;padding:8px 12px;border-top:1px solid #111;flex-wrap:wrap}
|
||
.menu .row:first-of-type,.settings .row:first-of-type{border-top:0}
|
||
.select{appearance:none;-webkit-appearance:none;border:1px solid var(--border);background:#111;color:#fff;border-radius:10px;padding:10px 12px}
|
||
.pins ul{list-style:none;margin:0;padding:0}.pins li{display:flex;flex-direction:column;gap:8px;padding:10px;border-top:1px solid #111}
|
||
.settings .group{border-top:1px solid #111}.settings .ghead{display:flex;justify-content:space-between;align-items:center;padding:10px 12px}.settings .section{display:none;padding:10px 12px;border-top:1px solid #111}
|
||
.settings textarea{width:100%;min-height:110px;border-radius:10px;border:1px solid var(--border);background:#0a0a0a;color:#eee;padding:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||
.badge{font-size:11px;opacity:.8;border:1px solid #444;border-radius:999px;padding:2px 8px;margin-left:8px}
|
||
.desktop .app{grid-template-columns:var(--sidebar-w) 1fr;grid-template-rows:auto 1fr}
|
||
.desktop .bar{grid-column:2}
|
||
.desktop .sidebar{position:fixed;left:0;top:0;bottom:0;width:var(--sidebar-w);min-width:var(--sidebar-min);max-width:var(--sidebar-max);background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;gap:12px;padding:12px;z-index:9;overflow:auto}
|
||
.desktop .sidecard{border:1px solid #222;border-radius:12px;overflow:hidden}
|
||
.desktop .sidecard header{padding:10px 12px;border-bottom:1px solid #222;font-weight:600;display:flex;justify-content:space-between;align-items:center}
|
||
.desktop .sidecard .row{display:flex;gap:8px;align-items:center;padding:8px 12px;border-top:1px solid #111;flex-wrap:wrap}
|
||
.desktop .sidecard .row:first-of-type{border-top:0}
|
||
.desktop .mapwrap{grid-column:2}
|
||
.desktop .menu,.desktop .pins,.desktop .settings,.desktop .dock{display:none!important}
|
||
.side-resizer{position:fixed;top:0;bottom:0;left:calc(var(--sidebar-w) - 4px);width:8px;cursor:col-resize;z-index:10;background:linear-gradient(to right,transparent 0 6px,#ffffff10 6px 7px,transparent 7px 100%)}
|
||
.side-resizer.dragging{background:linear-gradient(to right,transparent 0 6px,#ffffff33 6px 7px,transparent 7px 100%)}
|
||
@media (min-width:768px){.bar{padding:10px 14px}}
|
||
@media (prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app" id="app">
|
||
<div class="bar">
|
||
<div class="search">
|
||
<form id="form" autocomplete="off">
|
||
<input id="q" type="text" inputmode="search" placeholder="Search places, or paste lat,lon…" aria-autocomplete="list" aria-expanded="false" aria-controls="suggestions" />
|
||
</form>
|
||
<ul id="suggestions" class="suggest" role="listbox">
|
||
<li class="head" aria-hidden="true"><strong>Suggestions</strong><button type="button" id="closeSuggestBtn" title="Close">✕</button></li>
|
||
</ul>
|
||
</div>
|
||
<button class="iconbtn" id="locate" title="Locate">📍</button>
|
||
<button class="iconbtn" id="menuBtn" title="Menu">⋯</button>
|
||
</div>
|
||
|
||
<div class="mapwrap" id="mapwrap">
|
||
<iframe id="map" title="Map"></iframe>
|
||
<div id="marker" class="marker" aria-hidden="true"><div class="dot"></div></div>
|
||
<div class="brand">PokeMaps Public Beta</div>
|
||
<div class="dock"><button id="savepin" class="iconbtn">⭐ Save Pin</button><button id="showpins" class="iconbtn">📒 Pins</button><button id="opensettings" class="iconbtn">⚙ Settings</button></div>
|
||
<div class="coords" id="coords"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mobile/Tablet popovers -->
|
||
<div class="menu" id="menu">
|
||
<header><strong>Menu</strong><button class="iconbtn" id="closeMenu">✕</button></header>
|
||
<div class="row"><select class="select" id="layer"><option value="mapnik">Standard</option><option value="cyclemap">Cycle</option><option value="transportmap">Transport</option><option value="hot">Humanitarian</option></select></div>
|
||
<div class="row"><button class="iconbtn" id="follow">🛰️ Follow</button><button class="iconbtn" id="reset">🔁 Reset</button></div>
|
||
<div class="row"><button class="iconbtn" id="quick-near">📍 Near me</button><button class="iconbtn" id="quick-clear">🧹 Clear</button></div>
|
||
<div class="row"><button class="iconbtn" id="copy">📋 Copy Link</button><button class="iconbtn" id="copycoords">📐 Coords</button></div>
|
||
<div class="row"><button class="iconbtn" id="share">🔗 Share</button><button class="iconbtn" id="openosm">🌐 Open OSM</button></div>
|
||
<div class="row"><button class="iconbtn" id="openGmaps">🛰️ Google Maps</button></div>
|
||
</div>
|
||
|
||
<div class="pins" id="pins">
|
||
<header><strong>Saved Pins</strong><button id="closepins" class="iconbtn" type="button">✕</button></header>
|
||
<ul id="pinlist"></ul>
|
||
</div>
|
||
|
||
<div class="settings" id="settings">
|
||
<header><strong>Settings</strong><span class="badge">Beta</span><button id="closesettings" class="iconbtn">✕</button></header>
|
||
|
||
<div class="group">
|
||
<div class="ghead"><div><strong>General</strong></div><button data-t="gen" class="iconbtn">Toggle</button></div>
|
||
<div class="section" data-sec="gen">
|
||
<div class="row"><label><input type="checkbox" id="autoFollow"> Auto-follow on load</label></div>
|
||
<div class="row"><label><input type="checkbox" id="toggleCoords"> Always show coordinates</label></div>
|
||
<div class="row"><label>Coordinates format</label><select id="coordFmt" class="select"><option value="dec">DD (Decimal)</option><option value="dms">DMS</option></select></div>
|
||
<div class="row"><label>Precision</label><input type="range" id="coordPrec" min="0" max="8" step="1" value="6"><span id="coordPrecVal">6</span></div>
|
||
<div class="row"><label>Theme</label><select id="themeMode" class="select"><option value="auto">Auto</option><option value="dark">Dark</option><option value="light">Light</option></select></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="group">
|
||
<div class="ghead"><div><strong>Marker</strong></div><button data-t="mrk" class="iconbtn">Toggle</button></div>
|
||
<div class="section" data-sec="mrk">
|
||
<div class="row"><label>Visible</label><input type="checkbox" id="markerVisible" checked></div>
|
||
<div class="row"><label>Style</label><select id="markerStyle" class="select"><option value="dot">Dot</option><option value="crosshair">Crosshair</option></select></div>
|
||
<div class="row"><label>Size</label><input type="range" id="markerSize" min="8" max="64" step="1" value="20"><span id="markerSizeVal">20px</span></div>
|
||
<div class="row"><label>Color</label><input type="color" id="markerColor" value="#e53935"></div>
|
||
<div class="row"><label>Ring glow</label><input type="checkbox" id="markerRing" checked></div>
|
||
<div class="row"><label>Ring width</label><input type="range" id="ringWidth" min="0" max="16" step="1" value="3"><span id="ringWidthVal">3px</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="group">
|
||
<div class="ghead"><div><strong>View & Links</strong></div><button data-t="view" class="iconbtn">Toggle</button></div>
|
||
<div class="section" data-sec="view">
|
||
<div class="row"><label><input type="checkbox" id="shareDelta" checked> Include Δ in shared links</label></div>
|
||
<div class="row"><label><input type="checkbox" id="confirmDelete" checked> Confirm before deleting pins</label></div>
|
||
<div class="row"><label>Accent</label><input type="color" id="accentColor" value="#0ea5e9"><button class="iconbtn" id="accentReset">Reset</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="group">
|
||
<div class="ghead"><div><strong>Custom CSS</strong></div><button data-t="css" class="iconbtn">Toggle</button></div>
|
||
<div class="section" data-sec="css">
|
||
<div class="row" style="flex-direction:column;align-items:stretch">
|
||
<textarea id="customCSS" placeholder="/* Your CSS here */"></textarea>
|
||
<div style="display:flex;gap:8px;margin-top:8px"><button class="iconbtn" id="applyCSS">Apply</button><button class="iconbtn" id="clearCSS">Clear</button></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(()=>{ "use strict";
|
||
const OSM_EMBED="https://www.openstreetmap.org/export/embed.html";
|
||
const OSM_VIEW ="https://www.openstreetmap.org";
|
||
const NOMINATIM="https://nominatim.openstreetmap.org/search";
|
||
const LAYERS=["mapnik","cyclemap","transportmap","hot"];
|
||
const DEFAULT={lat:30.410156,lon:72.448792,delta:.25,layer:"mapnik"};
|
||
const LIMITS={deltaMin:0.01,deltaMax:45};
|
||
const PREFS_KEY="pokemaps_prefs_min";
|
||
const LS_PINS="pokemaps_pins_v1";
|
||
const LS_SEARCH="pokemaps_search_hist_v1";
|
||
|
||
const S=s=>document.querySelector(s), map=S("#map"), q=S("#q"), sug=S("#suggestions");
|
||
const locateBtn=S("#locate"), menuBtn=S("#menuBtn"), menu=S("#menu"), closeMenu=S("#closeMenu");
|
||
const layerSel=S("#layer"), followBtn=S("#follow"), resetBtn=S("#reset");
|
||
const copyBtn=S("#copy"), copyCoordsBtn=S("#copycoords"), shareBtn=S("#share");
|
||
const openOSM=S("#openosm"), openGmaps=S("#openGmaps");
|
||
const pinsPane=S("#pins"), pinlist=S("#pinlist"), showpins=S("#showpins"), closepins=S("#closepins"), savepin=S("#savepin");
|
||
const opensettings=S("#opensettings"), settings=S("#settings"), closesettings=S("#closesettings");
|
||
const coordsEl=S("#coords"), themeColorMeta=S("#themeColorMeta");
|
||
const closeSuggestBtn=S("#closeSuggestBtn");
|
||
const quickNear=S("#quick-near"), quickClear=S("#quick-clear");
|
||
|
||
const autoFollowEl=S("#autoFollow"), toggleCoords=S("#toggleCoords"), coordFmtEl=S("#coordFmt"), coordPrecEl=S("#coordPrec"), coordPrecVal=S("#coordPrecVal"), themeModeEl=S("#themeMode");
|
||
const markerEl=S("#marker"), markerVisibleEl=S("#markerVisible"), markerSizeEl=S("#markerSize"), markerSizeVal=S("#markerSizeVal"), markerColorEl=S("#markerColor"), markerRingEl=S("#markerRing"), ringWidthEl=S("#ringWidth"), ringWidthVal=S("#ringWidthVal"), markerStyleEl=S("#markerStyle");
|
||
const shareDeltaEl=S("#shareDelta"), confirmDeleteEl=S("#confirmDelete"), accentColorEl=S("#accentColor"), accentResetEl=S("#accentReset");
|
||
const customCSSEl=S("#customCSS"), applyCSSEl=S("#applyCSS"), clearCSSEl=S("#clearCSS");
|
||
|
||
let aborter=null,lastQuery="",watchId=null,activeIndex=-1,lastCoordsStr="";
|
||
let state={...DEFAULT};
|
||
const prefs=loadPrefs();
|
||
|
||
const isDesktop=()=>matchMedia("(min-width: 1024px)").matches&&matchMedia("(pointer: fine)").matches;
|
||
const applyDesktopClass=()=>document.body.classList.toggle("desktop",isDesktop());
|
||
applyDesktopClass(); addEventListener("resize",applyDesktopClass,{passive:true});
|
||
const setVH=()=>{const vh=innerHeight*0.01;document.documentElement.style.setProperty("--vh",`${vh*100}px`)}; setVH(); addEventListener("resize",setVH,{passive:true});
|
||
|
||
const clamp=(n,min,max)=>Math.min(Math.max(n,min),max);
|
||
const clampDelta=d=>clamp(d,LIMITS.deltaMin,LIMITS.deltaMax);
|
||
const parseSize=v=>Math.max(0,parseFloat(String(v).replace(/[^\d.]/g,''))||0);
|
||
|
||
const bboxFrom=(lat,lon,d)=>({l:(lon-d).toFixed(6),b:(lat-d).toFixed(6),r:(lon+d).toFixed(6),t:(lat+d).toFixed(6)});
|
||
const embedURL=({lat,lon,delta,layer})=>{const b=bboxFrom(lat,lon,clampDelta(delta));const lyr=LAYERS.includes(layer)?layer:DEFAULT.layer;return `${OSM_EMBED}?bbox=${b.l}%2C${b.b}%2C${b.r}%2C${b.t}&layer=${encodeURIComponent(lyr)}`};
|
||
const appURL=({lat,lon,delta,layer})=>{const p=new URLSearchParams({lat:lat.toFixed(prefs.prec??6),lon:lon.toFixed(prefs.prec??6),delta:(prefs.includeDelta!==false?clampDelta(delta).toFixed(4):undefined),layer});if(p.get("delta")==="undefined")p.delete("delta");return `${location.origin}${location.pathname}?${p.toString()}`};
|
||
const osmViewURL=({lat,lon,delta,layer})=>{const z=deltaToZoom(clampDelta(delta));return `${OSM_VIEW}/?mlat=${lat.toFixed(6)}&mlon=${lon.toFixed(6)}#map=${z}/${lat.toFixed(6)}/${lon.toFixed(6)}&layers=${layer}`};
|
||
const gmapsURL=({lat,lon,delta})=>{const z=deltaToZoom(clampDelta(delta));return `https://www.google.com/maps/@${lat.toFixed(6)},${lon.toFixed(6)},${Math.max(3,Math.min(20,z))}z`};
|
||
const zoomToDelta=z=>Math.min(LIMITS.deltaMax,Math.max(LIMITS.deltaMin,Math.pow(2,(10-z))*0.02));
|
||
const deltaToZoom=d=>Math.round(10-Math.log2(d/0.02));
|
||
|
||
const parseURL=()=>{const sp=new URLSearchParams(location.search);const lat=parseFloat(sp.get("lat")),lon=parseFloat(sp.get("lon")),delta=parseFloat(sp.get("delta"));const layer=sp.get("layer");if(Number.isFinite(lat)&&Number.isFinite(lon)){state.lat=clamp(lat,-90,90);state.lon=clamp(lon,-180,180)};if(Number.isFinite(delta)&&delta>0)state.delta=clampDelta(delta);if(layer&&LAYERS.includes(layer))state.layer=layer; if(layerSel)layerSel.value=state.layer};
|
||
const apply=(push)=>{map.src=embedURL(state); if(push)history.pushState(state,"",appURL(state))};
|
||
|
||
const toDMS=(v,isLat)=>{const dir=isLat?(v>=0?"N":"S"):(v>=0?"E":"W");const av=Math.abs(v);const d=Math.floor(av);const m=Math.floor((av-d)*60);const s=((av-d)*60-m)*60;return `${d}°${m}′${s.toFixed(2)}″ ${dir}`};
|
||
const coordString=()=>{const fmt=prefs.coordFmt||"dec";const pr=prefs.prec??6;const dec=`${state.lat.toFixed(pr)}, ${state.lon.toFixed(pr)}`;const dms=toDMS(state.lat,true)+" "+toDMS(state.lon,false);return fmt==="dms"?dms:dec};
|
||
|
||
const updateCoordsFast=()=>{ if(!prefs.showCoords)return; const s=coordString()+` · Δ ${state.delta.toFixed(4)} · ${state.layer}`; if(s!==lastCoordsStr){coordsEl.textContent=s;lastCoordsStr=s}};
|
||
const centerOn=(lat,lon,{push=true}={})=>{state.lat=clamp(lat,-90,90);state.lon=clamp(lon,-180,180);apply(push)};
|
||
const setLayer=(layer)=>{state.layer=LAYERS.includes(layer)?layer:DEFAULT.layer;apply(true)};
|
||
const setDelta=(delta,{push=true}={})=>{state.delta=clampDelta(delta);apply(push)};
|
||
|
||
const locate=()=>{"geolocation" in navigator?navigator.geolocation.getCurrentPosition(p=>centerOn(p.coords.latitude,p.coords.longitude,{push:true}),e=>alert("Unable to retrieve location: "+e.message),{enableHighAccuracy:true,timeout:10000,maximumAge:0}):alert("Geolocation not supported.")};
|
||
const startFollow=()=>{"geolocation" in navigator?(watchId??(followBtn&&(followBtn.textContent="🛰️ Following"),watchId=navigator.geolocation.watchPosition(p=>centerOn(p.coords.latitude,p.coords.longitude,{push:false}),_=>stopFollow(),{enableHighAccuracy:true,timeout:15000,maximumAge:1000}))):alert("Geolocation not supported.")};
|
||
const stopFollow=()=>{if(watchId!==null){navigator.geolocation.clearWatch(watchId);watchId=null} if(followBtn)followBtn.textContent="🛰️ Follow"};
|
||
const toggleFollow=()=>watchId===null?startFollow():stopFollow();
|
||
|
||
const copyLink=async()=>{try{await navigator.clipboard.writeText(appURL(state));alert("Link copied!")}catch{alert("Could not copy.")}};
|
||
const copyCoords=async()=>{const msg=coordString();try{await navigator.clipboard.writeText(msg);alert("Coordinates copied!")}catch{alert(msg)}};
|
||
const shareLink=async()=>{const url=appURL(state);if(navigator.share){try{await navigator.share({title:"PokeMaps Public Beta",url})}catch{}}else copyLink()};
|
||
const reset=()=>{stopFollow();state={...DEFAULT};q.value="";hideSuggest(true);layerSel&&(layerSel.value=state.layer);apply(true)};
|
||
|
||
const debounced=(fn,ms=250)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms)}};
|
||
const cleanStr=s=>s.replace(/\s+/g," ").trim();
|
||
const parseCoordInput=str=>{str=str.trim();const m=str.match(/^\s*([+-]?\d+(?:\.\d+)?)\s*[, ]\s*([+-]?\d+(?:\.\d+)?)\s*$/);if(!m)return null;const lat=parseFloat(m[1]),lon=parseFloat(m[2]);if(!Number.isFinite(lat)||!Number.isFinite(lon))return null;if(Math.abs(lat)>90||Math.abs(lon)>180)return null;return {lat,lon}};
|
||
const highlight=(text,term)=>{term=term.trim();if(!term)return text;const esc=term.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return text.replace(new RegExp(esc,"ig"),m=>`<mark>${m}</mark>`)};
|
||
|
||
const loadSearchHist=()=>{try{return JSON.parse(localStorage.getItem(LS_SEARCH)||"[]")}catch{return[]}};
|
||
const saveSearchHist=a=>localStorage.setItem(LS_SEARCH,JSON.stringify(a.slice(0,12)));
|
||
const pushSearchHist=label=>{const h=loadSearchHist();const i=h.indexOf(label);if(i!==-1)h.splice(i,1);h.unshift(label);saveSearchHist(h)};
|
||
|
||
const renderSuggest=(items,term="")=>{
|
||
if(!sug)return; const head=sug.querySelector(".head"); sug.innerHTML=""; if(head)sug.appendChild(head);
|
||
if(!items.length){sug.style.display="none";q.setAttribute("aria-expanded","false");activeIndex=-1;return}
|
||
items.forEach((it,i)=>{const li=document.createElement("li");li.role="option";li.id="opt"+i;li.innerHTML=`<span class="pill">${it.type}</span><span class="txt">${highlight(it.label,term)}</span>`;li.addEventListener("mousedown",e=>{e.preventDefault();chooseItem(it)});sug.appendChild(li)});
|
||
sug.style.display="block";q.setAttribute("aria-expanded","true")
|
||
};
|
||
const hideSuggest=(force=false)=>{sug.style.display="none";q.setAttribute("aria-expanded","false");activeIndex=-1};
|
||
const chooseItem=it=>{if(it.type==="history"){q.value=it.label;searchNow(it.label);return} if(it.type==="coords"){centerOn(it.lat,it.lon,{push:true});pushSearchHist(it.label);hideSuggest(true);return} if(it.type==="place"){q.value=it.label;centerOn(it.lat,it.lon,{push:true});pushSearchHist(it.label);hideSuggest(true)}};
|
||
|
||
const searchPlaces=debounced(async term=>{
|
||
term=cleanStr(term);activeIndex=-1;
|
||
if(!term){const hist=loadSearchHist();renderSuggest(hist.map(x=>({type:"history",label:x})));return}
|
||
const coord=parseCoordInput(term); if(coord){renderSuggest([{type:"coords",label:`${coord.lat}, ${coord.lon}`,lat:coord.lat,lon:coord.lon}]);return}
|
||
aborter&&aborter.abort(); aborter=new AbortController();
|
||
try{const url=`${NOMINATIM}?q=${encodeURIComponent(term)}&format=json&limit=8&addressdetails=0&accept-language=en`;
|
||
const r=await fetch(url,{signal:aborter.signal}); if(!r.ok)throw new Error(r.status);
|
||
const data=await r.json();
|
||
renderSuggest(data.map(p=>({type:"place",label:p.display_name,lat:+p.lat,lon:+p.lon})),term)
|
||
}catch{const hist=loadSearchHist();renderSuggest(hist.map(x=>({type:"history",label:x})),"")}
|
||
},130);
|
||
|
||
const searchNow=async term=>{
|
||
term=cleanStr(term); if(!term)return;
|
||
const coord=parseCoordInput(term); if(coord){centerOn(coord.lat,coord.lon,{push:true});pushSearchHist(`${coord.lat}, ${coord.lon}`);return}
|
||
try{const r=await fetch(`${NOMINATIM}?q=${encodeURIComponent(term)}&format=json&limit=1`);const d=await r.json();if(d[0]){centerOn(parseFloat(d[0].lat),parseFloat(d[0].lon),{push:true});pushSearchHist(term)}}catch{}
|
||
};
|
||
|
||
// events: search box + suggest
|
||
q.addEventListener("input",e=>{const v=e.target.value;if(v===lastQuery)return;lastQuery=v;searchPlaces(v)},{passive:true});
|
||
q.addEventListener("focus",()=>{if(!q.value){const hist=loadSearchHist();renderSuggest(hist.map(x=>({type:"history",label:x})))}else searchPlaces(q.value)});
|
||
document.addEventListener("click",e=>{const within=e.target.closest(".search")||e.target.closest("#suggestions");if(!within)hideSuggest(true)});
|
||
closeSuggestBtn?.addEventListener("click",()=>hideSuggest(true));
|
||
q.addEventListener("keydown",e=>{
|
||
const visible=sug.style.display==="block"; if(e.key==="Escape"){hideSuggest(true);return} if(!visible)return;
|
||
const items=[...sug.querySelectorAll("li")].filter(li=>!li.classList.contains("head")); if(!items.length)return;
|
||
if(e.key==="ArrowDown"){e.preventDefault();activeIndex=(activeIndex+1)%items.length;activate(items)}
|
||
if(e.key==="ArrowUp"){e.preventDefault();activeIndex=(activeIndex-1+items.length)%items.length;activate(items)}
|
||
if(e.key==="Enter"){e.preventDefault(); if(activeIndex>=0)items[activeIndex].dispatchEvent(new MouseEvent("mousedown")); else searchNow(q.value)}
|
||
});
|
||
const activate=items=>{items.forEach(x=>x.classList.remove("active")); if(activeIndex>=0){items[activeIndex].classList.add("active");items[activeIndex].scrollIntoView({block:"nearest"})}};
|
||
S("#form").addEventListener("submit",async e=>{e.preventDefault();await searchNow(q.value);hideSuggest(true)});
|
||
|
||
// quick actions
|
||
quickNear&&(quickNear.onclick=locate);
|
||
quickClear&&(quickClear.onclick=()=>{q.value="";searchPlaces("");q.focus()});
|
||
locateBtn.onclick=locate;
|
||
layerSel&&(layerSel.onchange=()=>setLayer(layerSel.value));
|
||
followBtn&&(followBtn.onclick=toggleFollow);
|
||
resetBtn&&(resetBtn.onclick=reset);
|
||
shareBtn&&(shareBtn.onclick=shareLink);
|
||
copyBtn&&(copyBtn.onclick=copyLink);
|
||
copyCoordsBtn&&(copyCoordsBtn.onclick=copyCoords);
|
||
openOSM&&(openOSM.onclick=()=>open(osmViewURL(state),"_blank"));
|
||
openGmaps&&(openGmaps.onclick=()=>open(gmapsURL(state),"_blank"));
|
||
|
||
// pins
|
||
const loadPins=()=>{try{return JSON.parse(localStorage.getItem(LS_PINS)||"[]")}catch{return[]}};
|
||
const savePins=p=>localStorage.setItem(LS_PINS,JSON.stringify(p.slice(0,100)));
|
||
const renderPins=()=>{const pins=loadPins();pinlist.innerHTML="";if(!pins.length){const li=document.createElement("li");li.textContent="No pins yet.";pinlist.appendChild(li);return}
|
||
pins.forEach((p,idx)=>{const li=document.createElement("li");
|
||
const head=document.createElement("div");head.innerHTML=`<strong>${p.name}</strong><div class="tag">${p.lat.toFixed(5)}, ${p.lon.toFixed(5)} · ${p.layer}</div>`;
|
||
const row=document.createElement("div");row.className="row";
|
||
const go=btn("Go",()=>{pinsPane.style.display="none";state={...state,lat:p.lat,lon:p.lon,delta:p.delta,layer:p.layer};layerSel&&(layerSel.value=p.layer);apply(true)});
|
||
const share=btn("Share",async()=>{const url=appURL(p);if(navigator.share){try{await navigator.share({title:"PokeMaps Public Beta",url})}catch{}}else{try{await navigator.clipboard.writeText(url);alert("Link copied!") }catch{}}});
|
||
const copyc=btn("Copy Coords",()=>navigator.clipboard.writeText(`${p.lat.toFixed(6)},${p.lon.toFixed(6)}`).then(()=>alert("Copied!")));
|
||
const ren=btn("Rename",()=>{const nv=prompt("New name:",p.name||"");if(nv!==null){const arr=loadPins();arr[idx].name=(nv||"").trim()||new Date().toLocaleString();savePins(arr);renderPins() }});
|
||
const del=btn("Del",()=>{if(confirmDeleteEl.checked&&!confirm("Delete pin?"))return;const arr=loadPins();arr.splice(idx,1);savePins(arr);renderPins()});
|
||
row.append(go,share,copyc,ren,del); li.append(head,row); pinlist.appendChild(li);
|
||
})
|
||
};
|
||
const btn=(t,fn)=>{const b=document.createElement("button");b.textContent=t;b.className="iconbtn";b.onclick=fn;return b};
|
||
const nameFromState=()=>q.value?.trim()||new Date().toLocaleString();
|
||
savepin.onclick=()=>{const pins=loadPins();pins.unshift({name:nameFromState(),lat:state.lat,lon:state.lon,delta:state.delta,layer:state.layer});savePins(pins);renderPins();pinsPane.style.display="block"};
|
||
showpins.onclick=()=>{renderPins();pinsPane.style.display="block"};
|
||
closepins.onclick=()=>{pinsPane.style.display="none"};
|
||
|
||
// mobile panels
|
||
menuBtn.onclick=()=>{if(isDesktop())return;menu.style.display="block"};
|
||
closeMenu&&(closeMenu.onclick=()=>{menu.style.display="none"});
|
||
opensettings.onclick=()=>{if(isDesktop())return;settings.style.display="block"};
|
||
closesettings.onclick=()=>{settings.style.display="none"};
|
||
|
||
// prefs -> UI
|
||
function loadPrefs(){try{return JSON.parse(localStorage.getItem(PREFS_KEY)||"{}")}catch{return{}}}
|
||
function savePrefs(){localStorage.setItem(PREFS_KEY,JSON.stringify(prefs))}
|
||
function applyPrefs(){
|
||
document.documentElement.style.setProperty("--marker-size",(prefs.markerSize||20)+"px");
|
||
document.documentElement.style.setProperty("--marker-color",prefs.markerColor||"#e53935");
|
||
document.documentElement.style.setProperty("--marker-ring",(prefs.markerRing===false)?"transparent":"rgba(229,57,53,.35)");
|
||
document.documentElement.style.setProperty("--ring-width",(prefs.ringWidth!=null?prefs.ringWidth:3)+"px");
|
||
if(prefs.markerHidden)markerEl.classList.add("hidden");
|
||
markerEl.classList.toggle("crosshair",(prefs.markerStyle||"dot")==="crosshair");
|
||
setAccent(prefs.accent||"#0ea5e9");
|
||
applyTheme(prefs.theme||"auto");
|
||
coordsEl.style.display=prefs.showCoords?"block":"none";
|
||
if(prefs.sidebarW){const min=parseSize(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-min'));const max=innerWidth*0.6;const w=clamp(prefs.sidebarW,min,max);document.documentElement.style.setProperty("--sidebar-w",w+"px")}
|
||
}
|
||
function setAccent(hex){document.documentElement.style.setProperty("--accent",hex);themeColorMeta?.setAttribute("content",hex)}
|
||
function applyTheme(mode){document.documentElement.style.colorScheme=mode==="auto"?"":mode}
|
||
|
||
// settings bind
|
||
toggleCoords.checked=!!prefs.showCoords;
|
||
coordFmtEl.value=prefs.coordFmt||"dec";
|
||
coordPrecEl.value=prefs.prec??6; coordPrecVal.textContent=String(prefs.prec??6);
|
||
themeModeEl.value=prefs.theme||"auto";
|
||
autoFollowEl.checked=!!prefs.autoFollow;
|
||
shareDeltaEl.checked=prefs.includeDelta!==false;
|
||
confirmDeleteEl.checked=prefs.confirmDelete!==false;
|
||
markerVisibleEl.checked=!prefs.markerHidden;
|
||
markerSizeEl.value=prefs.markerSize||20; markerSizeVal.textContent=markerSizeEl.value+"px";
|
||
markerColorEl.value=prefs.markerColor||"#e53935";
|
||
markerRingEl.checked=prefs.markerRing!==false;
|
||
ringWidthEl.value=prefs.ringWidth!=null?prefs.ringWidth:3; ringWidthVal.textContent=ringWidthEl.value+"px";
|
||
markerStyleEl.value=prefs.markerStyle||"dot";
|
||
accentColorEl.value=prefs.accent||"#0ea5e9";
|
||
customCSSEl.value=prefs.userCSS||"";
|
||
|
||
// toggles
|
||
document.querySelectorAll(".settings .ghead button").forEach(b=>b.onclick=()=>{const t=b.getAttribute("data-t");const sec=document.querySelector(`.settings .section[data-sec="${t}"]`);sec.style.display=sec.style.display==="block"?"none":"block"});
|
||
markerVisibleEl.onchange=()=>{markerEl.classList.toggle("hidden",!markerVisibleEl.checked);prefs.markerHidden=!markerVisibleEl.checked;savePrefs()};
|
||
markerSizeEl.oninput=()=>{document.documentElement.style.setProperty("--marker-size",markerSizeEl.value+"px");markerSizeVal.textContent=markerSizeEl.value+"px";prefs.markerSize=+markerSizeEl.value;savePrefs()};
|
||
markerColorEl.oninput=()=>{document.documentElement.style.setProperty("--marker-color",markerColorEl.value);prefs.markerColor=markerColorEl.value;savePrefs()};
|
||
markerRingEl.onchange=()=>{document.documentElement.style.setProperty("--marker-ring",markerRingEl.checked?"rgba(229,57,53,.35)":"transparent");prefs.markerRing=markerRingEl.checked;savePrefs()};
|
||
ringWidthEl.oninput=()=>{document.documentElement.style.setProperty("--ring-width",ringWidthEl.value+"px");ringWidthVal.textContent=ringWidthEl.value+"px";prefs.ringWidth=+ringWidthEl.value;savePrefs()};
|
||
markerStyleEl.onchange=()=>{markerEl.classList.toggle("crosshair",markerStyleEl.value==="crosshair");prefs.markerStyle=markerStyleEl.value;savePrefs()};
|
||
toggleCoords.onchange=()=>{prefs.showCoords=toggleCoords.checked;coordsEl.style.display=prefs.showCoords?"block":"none";savePrefs()};
|
||
coordFmtEl.onchange=()=>{prefs.coordFmt=coordFmtEl.value;savePrefs()};
|
||
coordPrecEl.oninput=()=>{prefs.prec=+coordPrecEl.value;coordPrecVal.textContent=String(prefs.prec);savePrefs()};
|
||
autoFollowEl.onchange=()=>{prefs.autoFollow=autoFollowEl.checked;savePrefs()};
|
||
shareDeltaEl.onchange=()=>{prefs.includeDelta=shareDeltaEl.checked;savePrefs()};
|
||
confirmDeleteEl.onchange=()=>{prefs.confirmDelete=confirmDeleteEl.checked;savePrefs()};
|
||
themeModeEl.onchange=()=>{prefs.theme=themeModeEl.value;applyTheme(prefs.theme);savePrefs()};
|
||
accentColorEl.oninput=()=>{setAccent(accentColorEl.value);prefs.accent=accentColorEl.value;savePrefs()};
|
||
accentResetEl.onclick=()=>{const def="#0ea5e9";accentColorEl.value=def;setAccent(def);prefs.accent=def;savePrefs()};
|
||
applyCSSEl.onclick=()=>{const css=customCSSEl.value||"";prefs.userCSS=css;applyUserCSS(css);savePrefs()};
|
||
clearCSSEl.onclick=()=>{customCSSEl.value="";prefs.userCSS="";applyUserCSS("");savePrefs()};
|
||
function applyUserCSS(css){let tag=document.getElementById("user-css");if(!tag){tag=document.createElement("style");tag.id="user-css";document.head.appendChild(tag)}tag.textContent=css||""}
|
||
|
||
// history/nav/pwa
|
||
addEventListener("popstate",e=>{stopFollow(); if(e.state&&typeof e.state.lat==="number"){state=e.state; layerSel&&(layerSel.value=state.layer); apply(false)}else{parseURL(); layerSel&&(layerSel.value=state.layer); apply(false)}});
|
||
(function injectManifest(){const manifest={name:"PokeMaps Public Beta",short_name:"PokeMaps",start_url:location.pathname,display:"standalone",background_color:"#000",theme_color:getComputedStyle(document.documentElement).getPropertyValue("--accent").trim()||"#0ea5e9",icons:[{src:"/css/yt-ukraine.svg",sizes:"any",type:"image/svg+xml",purpose:"any"}]};const blob=new Blob([JSON.stringify(manifest)],{type:"application/manifest+json"});const url=URL.createObjectURL(blob);const link=document.createElement("link");link.rel="manifest";link.href=url;document.head.appendChild(link)})();
|
||
|
||
// desktop sidebar + resizer
|
||
if(isDesktop()){buildDesktopSidebar();setupSidebarResizer()}
|
||
function setupSidebarResizer(){
|
||
let handle=document.querySelector(".side-resizer"); if(!handle){handle=document.createElement("div");handle.className="side-resizer";document.body.appendChild(handle)}
|
||
const minPx=()=>parseSize(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-min'));
|
||
const maxPx=()=>Math.min(innerWidth*0.6,parseSize(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-max'))||innerWidth*0.6);
|
||
let dragging=false,startX=0,startW=0;
|
||
const down=e=>{if(!isDesktop())return;dragging=true;startX=e.clientX??(e.touches?.[0]?.clientX||0);startW=parseSize(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w'))||320;handle.classList.add("dragging");addEventListener("pointermove",move);addEventListener("pointerup",up);e.preventDefault()};
|
||
const move=e=>{if(!dragging)return;const x=e.clientX??(e.touches?.[0]?.clientX||0);const raw=startW+(x-startX);const clamped=clamp(raw,minPx(),maxPx());document.documentElement.style.setProperty('--sidebar-w',clamped+'px')};
|
||
const up=()=>{if(!dragging)return;dragging=false;handle.classList.remove("dragging");const w=parseSize(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w'))||320;prefs.sidebarW=Math.round(clamp(w,minPx(),maxPx()));savePrefs();removeEventListener("pointermove",move);removeEventListener("pointerup",up)};
|
||
handle.addEventListener("pointerdown",down);
|
||
addEventListener("resize",()=>{if(!isDesktop())return;const w=parseSize(getComputedStyle(document.documentElement).getPropertyValue('--sidebar-w'))||320;const clamped=clamp(w,minPx(),maxPx());document.documentElement.style.setProperty('--sidebar-w',clamped+'px')},{passive:true});
|
||
}
|
||
function buildDesktopSidebar(){
|
||
const side=document.createElement("div");side.className="sidebar";
|
||
side.innerHTML=`
|
||
<div class="sidecard">
|
||
<header><span>Overview</span></header>
|
||
<div class="row"><input id="d_search" type="text" placeholder="Search or lat,lon…" class="select" style="width:100%"></div>
|
||
<div class="row"><select class="select" id="d_layer"><option value="mapnik">Standard</option><option value="cyclemap">Cycle</option><option value="transportmap">Transport</option><option value="hot">Humanitarian</option></select></div>
|
||
<div class="row"><button class="iconbtn" id="d_locate">Locate</button><button class="iconbtn" id="d_follow">Follow</button><button class="iconbtn" id="d_reset">Reset</button></div>
|
||
<div class="row"><button class="iconbtn" id="d_copy">Copy Link</button><button class="iconbtn" id="d_copyc">Copy Coords</button></div>
|
||
<div class="row"><button class="iconbtn" id="d_osm">Open OSM</button><button class="iconbtn" id="d_gmaps">Google Maps</button></div>
|
||
</div>
|
||
<div class="sidecard">
|
||
<header>Marker</header>
|
||
<div class="row"><label style="flex:1">Visible</label><input type="checkbox" id="d_markerv"></div>
|
||
<div class="row"><label style="flex:1">Style</label><select class="select" id="d_style"><option value="dot">Dot</option><option value="crosshair">Crosshair</option></select></div>
|
||
<div class="row"><label style="flex:1">Size</label><input type="range" id="d_ms" min="8" max="64"><span id="d_msv"></span></div>
|
||
<div class="row"><label style="flex:1">Color</label><input type="color" id="d_mc"></div>
|
||
<div class="row"><label style="flex:1">Glow</label><input type="checkbox" id="d_mr"></div>
|
||
<div class="row"><label style="flex:1">Ring width</label><input type="range" id="d_rw" min="0" max="16"><span id="d_rwv"></span></div>
|
||
</div>
|
||
<div class="sidecard">
|
||
<header>Pins</header>
|
||
<div class="row"><button class="iconbtn" id="d_save">Save current</button><button class="iconbtn" id="d_showpins">Manage</button></div>
|
||
<div class="row" id="d_pinpeek" style="flex-direction:column;align-items:stretch;gap:6px"><small style="opacity:.75">Recent:</small><div id="d_pinlistmini" style="display:flex;flex-direction:column;gap:6px"></div></div>
|
||
</div>
|
||
<div class="sidecard">
|
||
<header>Display</header>
|
||
<div class="row"><label style="flex:1">Always show coords</label><input type="checkbox" id="d_showc"></div>
|
||
<div class="row"><select class="select" id="d_fmt"><option value="dec">DD (Decimal)</option><option value="dms">DMS</option></select></div>
|
||
<div class="row"><label style="flex:1">Precision</label><input type="range" id="d_prec" min="0" max="8"><span id="d_precv"></span></div>
|
||
<div class="row"><label style="flex:1">Accent</label><input type="color" id="d_accent"><button class="iconbtn" id="d_accent_reset">Reset</button></div>
|
||
<div class="row"><label style="flex:1">Theme</label><select id="d_theme" class="select"><option value="auto">Auto</option><option value="dark">Dark</option><option value="light">Light</option></select></div>
|
||
</div>
|
||
<div class="sidecard">
|
||
<header>Custom CSS</header>
|
||
<div class="row" style="flex-direction:column;align-items:stretch">
|
||
<textarea id="d_usercss" style="width:100%;min-height:100px"></textarea>
|
||
<div style="display:flex;gap:8px;margin-top:8px"><button class="iconbtn" id="d_applycss">Apply</button><button class="iconbtn" id="d_clearcss">Clear</button></div>
|
||
</div>
|
||
</div>
|
||
<div class="sidecard"><header>About</header><div class="row"><small>© OSM contributors • Viewer: PokeMaps Public Beta</small></div></div>
|
||
`;
|
||
document.body.appendChild(side);
|
||
const d_search=side.querySelector("#d_search");
|
||
d_search.value=q.value||""; d_search.addEventListener("input",()=>{q.value=d_search.value;q.dispatchEvent(new Event("input",{bubbles:true}))});
|
||
const d_layer=side.querySelector("#d_layer"); d_layer.value=state.layer; d_layer.onchange=()=>setLayer(d_layer.value);
|
||
side.querySelector("#d_locate").onclick=locate;
|
||
side.querySelector("#d_follow").onclick=toggleFollow;
|
||
side.querySelector("#d_reset").onclick=reset;
|
||
side.querySelector("#d_copy").onclick=copyLink;
|
||
side.querySelector("#d_copyc").onclick=copyCoords;
|
||
side.querySelector("#d_osm").onclick=()=>open(osmViewURL(state),"_blank");
|
||
side.querySelector("#d_gmaps").onclick=()=>open(gmapsURL(state),"_blank");
|
||
|
||
const d_markerv=side.querySelector("#d_markerv"),d_style=side.querySelector("#d_style"),d_ms=side.querySelector("#d_ms"),d_msv=side.querySelector("#d_msv"),d_mc=side.querySelector("#d_mc"),d_mr=side.querySelector("#d_mr"),d_rw=side.querySelector("#d_rw"),d_rwv=side.querySelector("#d_rwv");
|
||
d_markerv.checked=!prefs.markerHidden; d_style.value=prefs.markerStyle||"dot"; d_ms.value=markerSizeEl.value; d_msv.textContent=d_ms.value+"px"; d_mc.value=markerColorEl.value; d_mr.checked=markerRingEl.checked; d_rw.value=ringWidthEl.value; d_rwv.textContent=d_rw.value+"px";
|
||
d_markerv.onchange=()=>{markerVisibleEl.checked=d_markerv.checked;markerVisibleEl.onchange()};
|
||
d_style.onchange=()=>{markerStyleEl.value=d_style.value;markerStyleEl.onchange()};
|
||
d_ms.oninput=()=>{markerSizeEl.value=d_ms.value;markerSizeEl.oninput();d_msv.textContent=d_ms.value+"px"};
|
||
d_mc.oninput=()=>{markerColorEl.value=d_mc.value;markerColorEl.oninput()};
|
||
d_mr.onchange=()=>{markerRingEl.checked=d_mr.checked;markerRingEl.onchange()};
|
||
d_rw.oninput=()=>{ringWidthEl.value=d_rw.value;ringWidthEl.oninput();d_rwv.textContent=d_rw.value+"px"};
|
||
|
||
const d_save=side.querySelector("#d_save"),d_showpins=side.querySelector("#d_showpins"),d_pinlistmini=side.querySelector("#d_pinlistmini");
|
||
d_save.onclick=()=>savepin.click(); d_showpins.onclick=()=>{renderPins();pinsPane.style.display="block"};
|
||
const renderPinsMini=()=>{d_pinlistmini.innerHTML="";const pins=loadPins().slice(0,6);if(!pins.length){d_pinlistmini.innerHTML='<small style="opacity:.6">No pins yet.</small>';return}
|
||
pins.forEach(p=>{const row=document.createElement("div");row.style.display="flex";row.style.gap="6px";row.style.alignItems="center";row.style.justifyContent="space-between";const txt=document.createElement("div");txt.innerHTML=`<strong style="font-size:12px">${p.name}</strong><br><span style="opacity:.7;font-size:11px">${p.lat.toFixed(5)}, ${p.lon.toFixed(5)}</span>`;const go=document.createElement("button");go.className="iconbtn";go.textContent="Go";go.onclick=()=>{state={...state,lat:p.lat,lon:p.lon,delta:p.delta,layer:p.layer};layerSel&&(layerSel.value=p.layer);apply(true)};row.append(txt,go);d_pinlistmini.appendChild(row)})};
|
||
renderPinsMini();
|
||
|
||
const d_showc=side.querySelector("#d_showc"),d_fmt=side.querySelector("#d_fmt"),d_prec=side.querySelector("#d_prec"),d_precv=side.querySelector("#d_precv"),d_accent=side.querySelector("#d_accent"),d_accent_reset=side.querySelector("#d_accent_reset"),d_theme=side.querySelector("#d_theme");
|
||
d_showc.checked=!!prefs.showCoords; d_fmt.value=prefs.coordFmt||"dec"; d_prec.value=prefs.prec??6; d_precv.textContent=String(prefs.prec??6); d_accent.value=prefs.accent||"#0ea5e9"; d_theme.value=prefs.theme||"auto";
|
||
d_showc.onchange=()=>{toggleCoords.checked=d_showc.checked;toggleCoords.onchange()};
|
||
d_fmt.onchange=()=>{coordFmtEl.value=d_fmt.value;coordFmtEl.onchange()};
|
||
d_prec.oninput=()=>{coordPrecEl.value=d_prec.value;coordPrecEl.oninput();d_precv.textContent=d_prec.value};
|
||
d_accent.oninput=()=>{accentColorEl.value=d_accent.value;accentColorEl.oninput()};
|
||
d_accent_reset.onclick=()=>{accentResetEl.click();d_accent.value=accentColorEl.value};
|
||
d_theme.onchange=()=>{themeModeEl.value=d_theme.value;themeModeEl.onchange()};
|
||
const d_usercss=side.querySelector("#d_usercss"),d_applycss=side.querySelector("#d_applycss"),d_clearcss=side.querySelector("#d_clearcss");
|
||
d_usercss.value=prefs.userCSS||""; d_applycss.onclick=()=>applyCSSEl.onclick(); d_clearcss.onclick=()=>clearCSSEl.onclick();
|
||
}
|
||
|
||
// init
|
||
applyPrefs(); parseURL(); apply(false);
|
||
const urlHasLat=new URLSearchParams(location.search).has("lat");
|
||
if(!urlHasLat)locate();
|
||
if(prefs.autoFollow)startFollow();
|
||
(function tick(){updateCoordsFast();setTimeout(tick,120)})();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|