197 lines
8.8 KiB
Plaintext
197 lines
8.8 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>PokeMaps Beta</title>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover" />
|
|
<meta name="color-scheme" content="dark light" />
|
|
<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)}
|
|
*{box-sizing:border-box}
|
|
html,body{height:100%;margin:0}
|
|
body{background:#000;font:14px/1.4 ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Arial;color:var(--fg)}
|
|
.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:10px 12px;border-radius:10px;border:1px solid #333;background:#111;color:#fff;outline:none}
|
|
.suggest{position:absolute;top: calc(100% + 6px);left:0;right:0;max-height:42vh;overflow:auto;margin:0;padding:6px 0;list-style:none;border:1px solid #333;border-radius:10px;background:#0b0b0b;display:none}
|
|
.suggest li{padding:10px 12px;cursor:pointer;border-top:1px solid #111}
|
|
.suggest li:first-child{border-top:none}
|
|
.suggest li:active{background:#1a1a1a}
|
|
.btns{display:flex;gap:8px;white-space:nowrap}
|
|
.btn{border:0;border-radius:10px;background:#222;color:#fff;padding:10px 12px}
|
|
.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:20px;height:20px;border-radius:50%;transform:translate(-50%,-50%);background:#e53935;box-shadow:0 0 0 3px rgba(229,57,53,.35);z-index:3;pointer-events:none}
|
|
.brand{position:absolute;bottom:10px;left:10px;padding:6px 10px;font-size:18px;font-weight:600;background:var(--bg);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);border-radius:10px;z-index:4;pointer-events:none}
|
|
.fab{position:fixed;right:16px;bottom:16px;z-index:6;width:56px;height:56px;border-radius:50%;border:0;background:#111;color:#fff;box-shadow:0 6px 18px rgba(0,0,0,.45);font-size:28px;line-height:0}
|
|
@media (min-width: 768px){
|
|
.bar{padding:10px 14px}
|
|
.brand{font-size:20px}
|
|
}
|
|
@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…" aria-autocomplete="list" aria-expanded="false" aria-controls="suggestions" />
|
|
</form>
|
|
<ul id="suggestions" class="suggest" role="listbox"></ul>
|
|
</div>
|
|
<div class="btns">
|
|
<button class="btn" id="locate" type="button">📍 Locate</button>
|
|
<button class="btn" id="copy" type="button">📋 Copy</button>
|
|
<button class="btn" id="reset" type="button">🔁 Reset</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mapwrap" id="mapwrap">
|
|
<iframe id="map" title="Map"></iframe>
|
|
<div class="marker" aria-hidden="true"></div>
|
|
<div class="brand">PokeMaps</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="fab" id="fab" title="More Tools">+</button>
|
|
|
|
<script>
|
|
;(()=> {
|
|
const OSM_EMBED="https://www.openstreetmap.org/export/embed.html"
|
|
const OSM_HOST="www.openstreetmap.org"
|
|
const NOMINATIM="https://nominatim.openstreetmap.org/search"
|
|
const LAYER="mapnik"
|
|
const DEFAULT={lat:30.41015625,lon:72.44879155730672,delta:.25}
|
|
const S=sel=>document.querySelector(sel)
|
|
const map=S("#map"), wrap=S("#mapwrap"), q=S("#q"), sug=S("#suggestions")
|
|
const locateBtn=S("#locate"), copyBtn=S("#copy"), resetBtn=S("#reset"), fab=S("#fab")
|
|
let aborter=null, lastQuery="", state={lat:DEFAULT.lat,lon:DEFAULT.lon,delta:DEFAULT.delta}
|
|
|
|
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 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})=>{
|
|
const b=bboxFrom(lat,lon,delta)
|
|
return `${OSM_EMBED}?bbox=${b.left}%2C${b.bottom}%2C${b.right}%2C${b.top}&layer=${encodeURIComponent(LAYER)}`
|
|
}
|
|
const appURL=({lat,lon,delta})=>{
|
|
const p=new URLSearchParams({lat:lat.toFixed(6),lon:lon.toFixed(6),delta:delta.toFixed(4)})
|
|
return `${location.origin}${location.pathname}?${p.toString()}`
|
|
}
|
|
const parseURL=()=>{
|
|
const sp=new URLSearchParams(location.search)
|
|
const lat=parseFloat(sp.get("lat")), lon=parseFloat(sp.get("lon")), delta=parseFloat(sp.get("delta"))
|
|
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&&delta<45) state.delta=delta
|
|
}
|
|
|
|
const apply=push=>{
|
|
map.src=embedURL(state)
|
|
if(push) history.pushState(state,"",appURL(state))
|
|
}
|
|
|
|
const centerOn=(lat,lon,{push=true}={})=>{
|
|
state.lat=clamp(lat,-90,90)
|
|
state.lon=clamp(lon,-180,180)
|
|
apply(push)
|
|
}
|
|
|
|
const reset=()=>{
|
|
state={...DEFAULT}
|
|
q.value=""
|
|
sug.style.display="none"; q.setAttribute("aria-expanded","false")
|
|
apply(true)
|
|
}
|
|
|
|
const copyLink=async()=>{
|
|
try{
|
|
await navigator.clipboard.writeText(appURL(state))
|
|
alert("Map link copied!")
|
|
}catch{ alert("Could not copy.")}
|
|
}
|
|
|
|
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 debounced=(fn,ms=250)=>{let t;return(...a)=>{clearTimeout(t);t=setTimeout(()=>fn(...a),ms)}}
|
|
const searchPlaces=debounced(async term=>{
|
|
term=cleanStr(term); if(!term){sug.style.display="none"; q.setAttribute("aria-expanded","false"); return}
|
|
if(aborter) aborter.abort()
|
|
aborter=new AbortController()
|
|
try{
|
|
const url=`${NOMINATIM}?q=${encodeURIComponent(term)}&format=json&limit=7&addressdetails=0&accept-language=en`
|
|
const r=await fetch(url,{signal:aborter.signal,headers:{}})
|
|
if(!r.ok) throw new Error("HTTP "+r.status)
|
|
const data=await r.json()
|
|
sug.innerHTML=""
|
|
data.forEach((p,i)=>{
|
|
const li=document.createElement("li")
|
|
li.role="option"; li.id="opt"+i
|
|
li.textContent=p.display_name
|
|
li.onclick=()=>{ q.value=p.display_name; sug.style.display="none"; q.setAttribute("aria-expanded","false"); centerOn(parseFloat(p.lat),parseFloat(p.lon),{push:true}) }
|
|
sug.appendChild(li)
|
|
})
|
|
sug.style.display=data.length?"block":"none"; q.setAttribute("aria-expanded", String(!!data.length))
|
|
}catch(e){
|
|
if(e.name!=="AbortError"){ sug.style.display="none"; q.setAttribute("aria-expanded","false") }
|
|
}
|
|
},200)
|
|
|
|
q.addEventListener("input",e=>{
|
|
const v=e.target.value
|
|
if(v===lastQuery) return
|
|
lastQuery=v
|
|
searchPlaces(v)
|
|
},{passive:true})
|
|
|
|
S("#form").addEventListener("submit",async e=>{
|
|
e.preventDefault()
|
|
const term=cleanStr(q.value); if(!term) 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})
|
|
}catch{}
|
|
})
|
|
|
|
locateBtn.onclick=locate
|
|
copyBtn.onclick=copyLink
|
|
resetBtn.onclick=reset
|
|
fab.onclick=()=>alert("More features coming soon!")
|
|
|
|
addEventListener("popstate",e=>{
|
|
if(e.state && typeof e.state.lat==="number"){ state=e.state; apply(false) }
|
|
else { parseURL(); apply(false) }
|
|
})
|
|
|
|
addEventListener("click",e=>{
|
|
const t=e.target
|
|
if(t.tagName==="A" && t.href.includes(OSM_HOST)){ e.preventDefault(); map.src=t.href; history.pushState(state,"",t.href) }
|
|
},{capture:true})
|
|
|
|
parseURL(); apply(false)
|
|
})();
|
|
</script>
|
|
|
|
<script src="/static/data-mobile.js" defer></script>
|
|
</body>
|
|
</html> |