120 lines
34 KiB
Plaintext
120 lines
34 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: rgba(0, 0, 0, .85); --accent: #0ea5e9; --chip: #111; --chipb: #222; --tip: #888;
|
||
--surface: #0b0b0b; --border: #333
|
||
}
|
||
* { 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 {
|
||
position: relative;
|
||
z-index: 5;
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: center;
|
||
padding: 8px var(--pad);
|
||
backdrop-filter: var(--glass);
|
||
-webkit-backdrop-filter: var(--glass);
|
||
background: var(--bg);
|
||
}
|
||
.search {
|
||
position: relative;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
.search input {
|
||
width: 100%;
|
||
padding: 12px 14px;
|
||
border-radius: 12px;
|
||
border: 1px solid var(--border);
|
||
background: var(--surface);
|
||
color: #fff;
|
||
outline: none;
|
||
}
|
||
.search input:focus {
|
||
border-color: #444;
|
||
box-shadow: 0 0 0 3px color-mix(in oklab, var(--accent) 20%, transparent);
|
||
}
|
||
|
||
/* Sidebar resizing and scrolling */
|
||
.desktop .sidebar {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 320px;
|
||
max-width: 100%;
|
||
background: var(--panel);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
z-index: 9;
|
||
overflow-y: auto;
|
||
resize: horizontal;
|
||
}
|
||
|
||
/* Hide dock buttons on desktop */
|
||
.desktop .dock { display: none; }
|
||
|
||
/* Ensure mobile UI also in sidebar */
|
||
.menu, .pins, .settings {
|
||
display: block !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 suggestions">✕</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">⭐ Save Pin</button>
|
||
<button id="showpins" title="Show saved pins">📒 Pins</button>
|
||
<button id="opensettings" title="Marker & display settings">⚙ Settings</button>
|
||
</div>
|
||
<div class="coords" id="coords"></div>
|
||
</div>
|
||
</div>
|
||
<script src="/static/data-mobile.js" defer></script>
|
||
<script> ;(()=> { 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 S=sel=>document.querySelector(sel); const map=S("#map"), q=S("#q"), sug=S("#suggestions"); const closeSuggestBtn=S("#closeSuggestBtn"); const locateBtn=S("#locate"), menuBtn=S("#menuBtn"), menu=S("#menu"), closeMenu=S("#closeMenu"); const layerSel=S("#layer"), followBtn=S("#follow"), resetBtn=S("#reset"), shareBtn=S("#share"); const copyBtn=S("#copy"), copyCoordsBtn=S("#copycoords"), openBtn=S("#openosm"), openGmapsBtn=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"); const quickNear=S("#quick-near"), quickClear=S("#quick-clear"); const themeColorMeta=S("#themeColorMeta"); const autoFollowEl=S("#autoFollow"); const toggleCoords=S("#toggleCoords"); const coordFmtEl=S("#coordFmt"); const coordPrecEl=S("#coordPrec"); const coordPrecVal=S("#coordPrecVal"); const 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"); const accentColorEl=S("#accentColor"), accentResetEl=S("#accentReset"); const customCSSEl=S("#customCSS"), applyCSSEl=S("#applyCSS"), clearCSSEl=S("#clearCSS"); /* Pro mode */ const PREFS_KEY="pokemaps_prefs_v6"; const LS_PINS="pokemaps_pins_v1"; const LS_SEARCH="pokemaps_search_hist_v1"; let aborter=null, lastQuery="", watchId=null, activeIndex=-1, lastCoordsStr=""; let state={...DEFAULT}; const prefs=loadPrefs(); applyPrefs(); 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=window.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 cleanStr=s=>s.replace(/\s+/g," ").trim(); const bboxFrom=(lat,lon,d)=>({left:(lon-d).toFixed(6),bottom:(lat-d).toFixed(6),right:(lon+d).toFixed(6),top:(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.left}%2C${b.bottom}%2C${b.right}%2C${b.top}&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 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 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 locate=()=>{ if(!("geolocation" in navigator)){ alert("Geolocation not supported."); return } navigator.geolocation.getCurrentPosition( pos=>centerOn(pos.coords.latitude,pos.coords.longitude,{push:true}), err=>alert("Unable to retrieve location: "+err.message), {enableHighAccuracy:true,timeout:10000,maximumAge:0} ) }; const startFollow=()=>{ if(!("geolocation" in navigator)){ alert("Geolocation not supported."); return } if(watchId!==null) return; followBtn && (followBtn.textContent="🛰️ Following"); watchId = navigator.geolocation.watchPosition( pos=>{ centerOn(pos.coords.latitude,pos.coords.longitude,{push:false}) }, _=>stopFollow(), {enableHighAccuracy:true,timeout:15000,maximumAge:1000} ) }; const stopFollow=()=>{ if(watchId!==null){ navigator.geolocation.clearWatch(watchId); watchId=null } ; 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 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=(arr)=>{ localStorage.setItem(LS_SEARCH, JSON.stringify(arr.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; // keep the sticky head; rebuild the rest 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)=>{ if(force){ sug.style.display="none"; q.setAttribute("aria-expanded","false"); activeIndex=-1; return } 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); return } }; 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 } if(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("HTTP "+r.status); const data=await r.json(); renderSuggest(data.map(p=>({type:"place",label:p.display_name,lat:+p.lat,lon:+p.lon,raw:p})), term); }catch(e){ const hist=loadSearchHist(); renderSuggest(hist.map(x=>({type:"history",label:x})), ""); } },140); 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{} }; // Suggestion UX 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 (Menu) 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); openBtn && (openBtn.onclick=()=>window.open(osmViewURL(state), "_blank")); openGmapsBtn && (openGmapsBtn.onclick=()=>window.open(gmapsURL(state), "_blank")); // Pins const loadPins=()=>{ try{ return JSON.parse(localStorage.getItem(LS_PINS)||"[]") }catch{ return [] } }; const savePins=(pins)=>{ localStorage.setItem(LS_PINS, JSON.stringify(pins.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 actions=document.createElement("div"); actions.className="row"; const go=button("Go",()=>{ pinsPane.style.display="none"; state.lat=p.lat; state.lon=p.lon; state.delta=p.delta; state.layer=p.layer; layerSel && (layerSel.value=p.layer); apply(true) }); const share=button("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=button("Copy Coords",()=>navigator.clipboard.writeText(${p.lat.toFixed(6)},${p.lon.toFixed(6)}).then(()=>alert("Copied!"))); const ren=button("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=button("Del",()=>{ if(confirmDeleteEl.checked){ if(!confirm("Delete pin?")) return } const arr=loadPins(); arr.splice(idx,1); savePins(arr); renderPins() }); actions.append(go,share,copyc,ren,del); li.append(head,actions); pinlist.appendChild(li); }); }; function button(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" }; // Panels (mobile/tablet only; desktop has sidebar) 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 toggleCoords.checked = !!prefs.showCoords; coordsEl.style.display = prefs.showCoords ? "block" : "none"; 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"; applyMarkerStyle(markerStyleEl.value); accentColorEl.value = prefs.accent || "#0ea5e9"; setAccent(prefs.accent || "#0ea5e9"); customCSSEl.value = prefs.userCSS || ""; applyUserCSS(prefs.userCSS||""); 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" } }); // Pref listeners 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=()=>{ applyMarkerStyle(markerStyleEl.value); 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() }; addEventListener("keydown",(e)=>{ if(e.target.matches("input, textarea")) return; if(e.key.toLowerCase()==="l"){ e.preventDefault(); locate() } if(e.key.toLowerCase()==="s"){ e.preventDefault(); savepin.click() } if(e.key.toLowerCase()==="h"){ e.preventDefault(); toggleFollow() } if(e.key.toLowerCase()==="r"){ e.preventDefault(); reset() } if(e.key.toLowerCase()==="1"){ e.preventDefault(); setLayer("mapnik") } if(e.key.toLowerCase()==="2"){ e.preventDefault(); setLayer("cyclemap") } if(e.key.toLowerCase()==="3"){ e.preventDefault(); setLayer("transportmap") } if(e.key.toLowerCase()==="4"){ e.preventDefault(); setLayer("hot") } if(e.key==="/"){ e.preventDefault(); q.focus() } if(e.key==="Escape"){ menu.style.display="none"; settings.style.display="none"; pinsPane.style.display="none"; hideSuggest(true) } }); 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 loadPrefs(){ try{ return JSON.parse(localStorage.getItem(PREFS_KEY)||"{}") }catch{ return {} } } function savePrefs(){ localStorage.setItem(PREFS_KEY, JSON.stringify(prefs)) } function applyPrefs(){ if(prefs.markerSize) document.documentElement.style.setProperty("--marker-size", prefs.markerSize+"px"); if(prefs.markerColor) document.documentElement.style.setProperty("--marker-color", prefs.markerColor); 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"); applyMarkerStyle(prefs.markerStyle||"dot"); setAccent(prefs.accent||"#0ea5e9"); applyTheme(prefs.theme||"auto"); // apply pro mode class on body document.body.classList.toggle("pro-enabled", !!prefs.proMode); } function applyMarkerStyle(style){ markerEl.classList.toggle("crosshair", style==="crosshair") } function setAccent(hex){ document.documentElement.style.setProperty("--accent",hex); themeColorMeta?.setAttribute("content",hex) } function applyTheme(mode){ if(mode==="dark"){ document.documentElement.style.colorScheme="dark" } else if(mode==="light"){ document.documentElement.style.colorScheme="light" } else { document.documentElement.style.colorScheme="" } } // PWA manifest (runtime-injected, no service worker) (function injectManifest(){ const manifest={ name:"PokeMaps Public Beta", short_name:"PokeMaps", start_url: location.pathname, display:"standalone", background_color:"#000000", 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); })(); // Custom CSS injector 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||""; } // Init navigation parseURL(); apply(false); const urlHasLat = new URLSearchParams(location.search).has("lat"); if(!urlHasLat){ locate() } if(prefs.autoFollow) startFollow(); // Desktop sidebar builder (clean, with Pro toggle) if(isDesktop()) buildDesktopSidebar(); function buildDesktopSidebar(){ const side=document.createElement("div"); side.className="sidebar"; side.innerHTML= <div class="sidecard"> <header> <span>Overview</span> <label style="font-weight:400;font-size:12px;display:flex;align-items:center;gap:6px;"> <input type="checkbox" id="pro_toggle" ${prefs.proMode?'checked':''}/> Pro </label> </header> <div class="row"><input id="d_searchmirror" type="text" placeholder="Search or lat,lon…" class="select" style="width:100%;background:#111;border:1px solid var(--border);border-radius:10px;padding:10px 12px"></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 in OSM</button><button class="iconbtn" id="d_gmaps">Google Maps</button></div> <div class="hint">Tip: / to focus search • 1–4 change layers • S save pin • R reset</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 pro-only"><label style="flex:1">Glow</label><input type="checkbox" id="d_mr"></div> <div class="row pro-only"><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 pro-only"><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 pro-only"><button class="iconbtn" id="d_share">Share</button><button class="iconbtn" id="d_copy2">Copy Link</button></div> </div> <div class="sidecard pro-only"> <header>Custom CSS</header> <div class="row" style="flex-direction:column;align-items:stretch"> <textarea id="d_usercss" style="width:100%;min-height:100px;border-radius:10px;border:1px solid var(--border);background:#0a0a0a;color:#eee;padding:8px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" placeholder="/* Your CSS here */"></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>Powered by openstreetmap.org • Data © OSM contributors • Public Beta</small></div> </div> ; document.body.appendChild(side); // Mirror search const d_searchmirror=side.querySelector("#d_searchmirror"); d_searchmirror.value=q.value||""; d_searchmirror.addEventListener("input",()=>{ q.value=d_searchmirror.value; q.dispatchEvent(new Event("input",{bubbles:true})) }); // Pro toggle const proToggle=side.querySelector("#pro_toggle"); const updatePro=()=>{ prefs.proMode = proToggle.checked; savePrefs(); document.body.classList.toggle("pro-enabled", !!prefs.proMode) }; proToggle.addEventListener("change",updatePro); document.body.classList.toggle("pro-enabled", !!prefs.proMode); const d_layer=side.querySelector("#d_layer"); side.querySelector("#d_osm").onclick=()=>window.open(osmViewURL(state),"_blank"); side.querySelector("#d_copy").onclick=copyLink; side.querySelector("#d_copy2").onclick=copyLink; side.querySelector("#d_share").onclick=shareLink; side.querySelector("#d_locate").onclick=locate; side.querySelector("#d_follow").onclick=toggleFollow; side.querySelector("#d_reset").onclick=reset; side.querySelector("#d_gmaps").onclick=()=>window.open(gmapsURL(state), "_blank"); d_layer.value=state.layer; d_layer.onchange=()=>setLayer(d_layer.value); // Marker group const d_markerv=side.querySelector("#d_markerv"); const d_style=side.querySelector("#d_style"); const d_ms=side.querySelector("#d_ms"), d_msv=side.querySelector("#d_msv"); const d_mc=side.querySelector("#d_mc"); const d_mr=side.querySelector("#d_mr"); const 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 && (d_mr.checked=markerRingEl.checked); d_rw && (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 && (d_mr.onchange=()=>{ markerRingEl.checked=d_mr.checked; markerRingEl.onchange() }); d_rw && (d_rw.oninput=()=>{ ringWidthEl.value=d_rw.value; ringWidthEl.oninput(); d_rwv.textContent=d_rw.value+"px" }); // Pins mini const d_save=side.querySelector("#d_save"); const d_showpins=side.querySelector("#d_showpins"); const 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,idx)=>{ 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.lat=p.lat; state.lon=p.lon; state.delta=p.delta; state.layer=p.layer; layerSel && (layerSel.value=p.layer); apply(true) }; row.append(txt,go); d_pinlistmini.appendChild(row); }); }; renderPinsMini(); // Display group const d_showc=side.querySelector("#d_showc"); const d_fmt=side.querySelector("#d_fmt"); const d_prec=side.querySelector("#d_prec"), d_precv=side.querySelector("#d_precv"); const d_accent=side.querySelector("#d_accent"), d_accent_reset=side.querySelector("#d_accent_reset"); d_showc.checked=!!prefs.showCoords; d_fmt.value=prefs.coordFmt||"dec"; d_prec && (d_prec.value=prefs.prec??6, d_precv.textContent=String(prefs.prec??6)); d_accent.value=prefs.accent||"#0ea5e9"; d_showc.onchange=()=>{ toggleCoords.checked=d_showc.checked; toggleCoords.onchange() }; d_fmt.onchange=()=>{ coordFmtEl.value=d_fmt.value; coordFmtEl.onchange() }; d_prec && (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 }; // Custom CSS const d_usercss=side.querySelector("#d_usercss"); const d_applycss=side.querySelector("#d_applycss"); const d_clearcss=side.querySelector("#d_clearcss"); if(d_usercss){ d_usercss.value=prefs.userCSS||""; d_applycss.onclick=()=>applyCSSEl.onclick(); d_clearcss.onclick=()=>clearCSSEl.onclick() } } function tickCoords(){ updateCoordsFast(); setTimeout(tickCoords,120) } tickCoords(); })(); </body>
|
||
</html>
|