Update html/map.ejs
This commit is contained in:
parent
18a9e170b8
commit
d6972c6671
564
html/map.ejs
564
html/map.ejs
@ -3,73 +3,180 @@
|
||||
<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="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"/>
|
||||
<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
|
||||
:root {
|
||||
--pad: 12px;
|
||||
--radius: 14px;
|
||||
--fg: #fff;
|
||||
--bg: rgba(0, 0, 0, 0.6);
|
||||
--glass: blur(12px);
|
||||
--panel: rgba(0, 0, 0, 0.85);
|
||||
--accent: #0ea5e9;
|
||||
--surface: #0b0b0b;
|
||||
--border: #333;
|
||||
--marker-size: 20px;
|
||||
--marker-color: #e53935;
|
||||
--marker-ring: rgba(229,57,53,.35);
|
||||
--ring-width: 3px;
|
||||
}
|
||||
*{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)}
|
||||
|
||||
/* Suggestions */
|
||||
.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:var(--surface);display:none}
|
||||
.suggest .head{position:sticky;top:0;background:#0d0d0d;border-bottom:1px solid #1a1a1a;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:#1a1a1a;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}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
font: 14px/1.4 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Arial;
|
||||
background: #000;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.iconbtn{border:0;border-radius:10px;background:#222;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-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}
|
||||
.dock{position:absolute;left:10px;bottom:52px;z-index:7;display:flex;flex-direction:column;gap:8px}
|
||||
.dock button{border:0;border-radius:10px;background:#111;color:#fff;padding:8px 10px}
|
||||
.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:260px;text-align:right;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","DejaVu Sans Mono",monospace}
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Popovers (for mobile) */
|
||||
.menu,.pins,.settings{position:fixed;z-index:8;min-width:280px;max-width:92vw;background:var(--panel);backdrop-filter:var(--glass);-webkit-backdrop-filter:var(--glass);border:1px solid var(--border);border-radius:12px;display:none;overflow:hidden}
|
||||
.menu{top:56px;right:10px}
|
||||
.pins{left:10px;bottom:110px;max-height:50vh;overflow:auto}
|
||||
.settings{left:10px;bottom:110px;max-width:92vw}
|
||||
.menu header,.pins header,.settings header{display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border-bottom:1px solid #222}
|
||||
.menu .row,.pins .row,.settings .row{display:flex;gap:8px;align-items:center;padding:8px 12px;flex-wrap:wrap}
|
||||
.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}
|
||||
.bar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px var(--pad);
|
||||
backdrop-filter: var(--glass);
|
||||
-webkit-backdrop-filter: var(--glass);
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* Desktop layout */
|
||||
.desktop .app{grid-template-columns:320px 1fr;grid-template-rows:auto 1fr}
|
||||
.desktop .bar{grid-column:2}
|
||||
.desktop .sidebar{position:fixed;left:0;top:0;bottom:0;width:320px;background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;gap:12px;padding:12px;z-index:9}
|
||||
.desktop .mapwrap{grid-column:2}
|
||||
.desktop .menu,.desktop .pins,.desktop .settings{display:none !important}
|
||||
.search {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media (min-width:768px){ .bar{padding:10px 14px} .brand{font-size:20px} }
|
||||
.search input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: #fff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.suggest {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
max-height: 46vh;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.suggest li {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid #111;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.suggest li:first-child { border-top: none; }
|
||||
.suggest li.active { background: #141414; }
|
||||
.suggest mark { color: var(--accent); font-weight: 600; background: none; }
|
||||
|
||||
.iconbtn {
|
||||
border: 0;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
min-width: 44px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.mapwrap {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
iframe#map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.marker {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: var(--marker-size);
|
||||
height: var(--marker-size);
|
||||
background: var(--marker-color);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 var(--ring-width) var(--marker-ring);
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.marker.hidden { display: none; }
|
||||
|
||||
.brand {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
padding: 6px 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
background: var(--bg);
|
||||
backdrop-filter: var(--glass);
|
||||
-webkit-backdrop-filter: var(--glass);
|
||||
border-radius: 10px;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dock {
|
||||
position: absolute;
|
||||
bottom: 52px;
|
||||
left: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.coords {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
padding: 6px 10px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
backdrop-filter: var(--glass);
|
||||
-webkit-backdrop-filter: var(--glass);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bar { padding: 10px 14px; }
|
||||
.brand { font-size: 18px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@ -77,269 +184,180 @@
|
||||
<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" />
|
||||
<input id="q" type="search" placeholder="Search or paste lat,lon…" 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>
|
||||
<ul id="suggestions" class="suggest"></ul>
|
||||
</div>
|
||||
<button class="iconbtn" id="locate" title="Locate">📍</button>
|
||||
<button class="iconbtn" id="menuBtn" title="Menu">⋯</button>
|
||||
<button id="locate" class="iconbtn" title="Locate">📍</button>
|
||||
<button id="menuBtn" class="iconbtn" 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>
|
||||
<iframe id="map" title="Map view"></iframe>
|
||||
<div class="marker" id="marker"><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>
|
||||
<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" title="Close">✕</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 (not recommended)</button></div>
|
||||
</div>
|
||||
<script>
|
||||
(async () => {
|
||||
const S = sel => document.querySelector(sel);
|
||||
const map = S("#map");
|
||||
const q = S("#q");
|
||||
const marker = S("#marker");
|
||||
const coords = S("#coords");
|
||||
|
||||
<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>
|
||||
|
||||
<script src="/static/data-mobile.js" defer></script>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const $ = s => document.querySelector(s);
|
||||
const $$ = s => [...document.querySelectorAll(s)];
|
||||
|
||||
const isDesktop = () => matchMedia("(min-width:1024px)").matches && matchMedia("(pointer: fine)").matches;
|
||||
const applyVH = () => document.documentElement.style.setProperty("--vh", `${window.innerHeight}px`);
|
||||
const setClass = () => document.body.classList.toggle("desktop", isDesktop());
|
||||
|
||||
// DOM elements
|
||||
const q = $("#q"), sug = $("#suggestions"), closeBtn = $("#closeSuggestBtn");
|
||||
const locateBtn = $("#locate"), menuBtn = $("#menuBtn"), menu = $("#menu"), closeMenu = $("#closeMenu");
|
||||
const settings = $("#settings"), opensettings = $("#opensettings"), closesettings = $("#closesettings");
|
||||
const map = $("#map"), marker = $("#marker"), coords = $("#coords");
|
||||
const pinlist = $("#pinlist"), pinsPane = $("#pins"), showpins = $("#showpins"), savepin = $("#savepin"), closepins = $("#closepins");
|
||||
const markerStyleSel = $("#markerStyle"), markerVisible = $("#markerVisible");
|
||||
|
||||
// Core state
|
||||
const DEFAULT = { lat: 30.410156, lon: 72.448792, delta: 0.25, layer: "mapnik" };
|
||||
const LAYERS = ["mapnik", "cyclemap", "transportmap", "hot"];
|
||||
const state = { ...DEFAULT };
|
||||
|
||||
let watchId = null;
|
||||
let suggestions = [];
|
||||
|
||||
// Init
|
||||
applyVH();
|
||||
setClass();
|
||||
window.addEventListener("resize", () => { applyVH(); setClass(); }, { passive: true });
|
||||
|
||||
const embed = ({ lat, lon, delta, layer }) => {
|
||||
const b = delta || 0.25;
|
||||
const l = layer || "mapnik";
|
||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${lon - b},${lat - b},${lon + b},${lat + b}&layer=${l}`;
|
||||
const state = {
|
||||
lat: 30.410156,
|
||||
lon: 72.448792,
|
||||
delta: 0.25,
|
||||
layer: "mapnik"
|
||||
};
|
||||
|
||||
const apply = () => {
|
||||
map.src = embed(state);
|
||||
const NOMINATIM = "https://nominatim.openstreetmap.org/search";
|
||||
const LAYERS = ["mapnik", "cyclemap", "transportmap", "hot"];
|
||||
const embedURL = ({ lat, lon, delta, layer }) => {
|
||||
const bbox = {
|
||||
left: lon - delta,
|
||||
right: lon + delta,
|
||||
top: lat + delta,
|
||||
bottom: lat - delta
|
||||
};
|
||||
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.left},${bbox.bottom},${bbox.right},${bbox.top}&layer=${layer}`;
|
||||
};
|
||||
const updateMap = () => {
|
||||
map.src = embedURL(state);
|
||||
updateCoords();
|
||||
};
|
||||
|
||||
const updateCoords = () => {
|
||||
coords.textContent = `${state.lat.toFixed(6)}, ${state.lon.toFixed(6)} · Δ ${state.delta.toFixed(4)} · ${state.layer}`;
|
||||
coords.textContent = `${state.lat.toFixed(6)}, ${state.lon.toFixed(6)} · Δ ${state.delta.toFixed(3)} · ${state.layer}`;
|
||||
coords.style.display = "block";
|
||||
};
|
||||
|
||||
// Suggest logic
|
||||
const renderSuggest = items => {
|
||||
if (!sug) return;
|
||||
sug.style.display = items.length ? "block" : "none";
|
||||
q.setAttribute("aria-expanded", items.length ? "true" : "false");
|
||||
|
||||
const head = sug.querySelector(".head");
|
||||
sug.innerHTML = "";
|
||||
if (head) sug.appendChild(head);
|
||||
|
||||
items.forEach((item, i) => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `<span class="pill">${item.type}</span><span>${item.label}</span>`;
|
||||
li.addEventListener("mousedown", e => {
|
||||
e.preventDefault();
|
||||
chooseItem(item);
|
||||
});
|
||||
sug.appendChild(li);
|
||||
});
|
||||
const centerMap = (lat, lon) => {
|
||||
state.lat = lat;
|
||||
state.lon = lon;
|
||||
updateMap();
|
||||
};
|
||||
|
||||
const chooseItem = item => {
|
||||
if (item.lat && item.lon) {
|
||||
state.lat = item.lat;
|
||||
state.lon = item.lon;
|
||||
apply();
|
||||
}
|
||||
sug.style.display = "none";
|
||||
};
|
||||
|
||||
const search = async query => {
|
||||
if (!query) {
|
||||
renderSuggest([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const coord = parseCoords(query);
|
||||
if (coord) {
|
||||
renderSuggest([{ type: "coords", label: `${coord.lat}, ${coord.lon}`, ...coord }]);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&limit=6`;
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
|
||||
suggestions = data.map(d => ({
|
||||
type: "place",
|
||||
label: d.display_name,
|
||||
lat: parseFloat(d.lat),
|
||||
lon: parseFloat(d.lon),
|
||||
}));
|
||||
renderSuggest(suggestions);
|
||||
};
|
||||
|
||||
const parseCoords = s => {
|
||||
const m = s.trim().match(/^([+-]?\d+(\.\d+)?),\s*([+-]?\d+(\.\d+)?)$/);
|
||||
if (m) return { lat: parseFloat(m[1]), lon: parseFloat(m[3]) };
|
||||
return null;
|
||||
};
|
||||
|
||||
// Events
|
||||
q?.addEventListener("input", e => search(e.target.value));
|
||||
q?.addEventListener("focus", () => search(q.value));
|
||||
closeBtn?.addEventListener("click", () => { sug.style.display = "none"; });
|
||||
|
||||
document.addEventListener("click", e => {
|
||||
if (!e.target.closest(".search") && !e.target.closest("#suggestions")) {
|
||||
sug.style.display = "none";
|
||||
}
|
||||
});
|
||||
|
||||
// Marker prefs
|
||||
markerVisible?.addEventListener("change", () => {
|
||||
marker.classList.toggle("hidden", !markerVisible.checked);
|
||||
});
|
||||
|
||||
markerStyleSel?.addEventListener("change", () => {
|
||||
const val = markerStyleSel.value;
|
||||
marker.classList.toggle("crosshair", val === "crosshair");
|
||||
});
|
||||
|
||||
// Geo
|
||||
const locate = () => {
|
||||
if (!navigator.geolocation) return alert("Geolocation unsupported");
|
||||
navigator.geolocation.getCurrentPosition(pos => {
|
||||
const { latitude: lat, longitude: lon } = pos.coords;
|
||||
state.lat = lat;
|
||||
state.lon = lon;
|
||||
apply();
|
||||
}, err => alert("Location error: " + err.message));
|
||||
if (!navigator.geolocation) return alert("Geolocation not supported.");
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
pos => centerMap(pos.coords.latitude, pos.coords.longitude),
|
||||
err => alert("Location error: " + err.message),
|
||||
{ enableHighAccuracy: true }
|
||||
);
|
||||
};
|
||||
|
||||
const follow = () => {
|
||||
if (watchId) return;
|
||||
watchId = navigator.geolocation.watchPosition(pos => {
|
||||
state.lat = pos.coords.latitude;
|
||||
state.lon = pos.coords.longitude;
|
||||
apply();
|
||||
});
|
||||
const isCoord = txt => /^-?\d+(\.\d+)?\s*[, ]\s*-?\d+(\.\d+)?$/.test(txt.trim());
|
||||
|
||||
const fetchPlaces = async q => {
|
||||
const res = await fetch(`${NOMINATIM}?q=${encodeURIComponent(q)}&format=json&limit=8`);
|
||||
const data = await res.json();
|
||||
return data.map(p => ({ label: p.display_name, lat: +p.lat, lon: +p.lon }));
|
||||
};
|
||||
|
||||
const stopFollow = () => {
|
||||
if (!watchId) return;
|
||||
navigator.geolocation.clearWatch(watchId);
|
||||
watchId = null;
|
||||
const renderSuggestions = async qstr => {
|
||||
const ul = S("#suggestions");
|
||||
ul.innerHTML = "";
|
||||
if (!qstr.trim()) return ul.style.display = "none";
|
||||
|
||||
if (isCoord(qstr)) {
|
||||
const [lat, lon] = qstr.split(/[ ,]+/).map(parseFloat);
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${lat}, ${lon}`;
|
||||
li.onclick = () => centerMap(lat, lon);
|
||||
ul.appendChild(li);
|
||||
} else {
|
||||
const results = await fetchPlaces(qstr);
|
||||
results.forEach(r => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `<mark>${r.label}</mark>`;
|
||||
li.onclick = () => centerMap(r.lat, r.lon);
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
ul.style.display = "block";
|
||||
};
|
||||
|
||||
// Buttons
|
||||
locateBtn?.addEventListener("click", locate);
|
||||
menuBtn?.addEventListener("click", () => menu.style.display = "block");
|
||||
closeMenu?.addEventListener("click", () => menu.style.display = "none");
|
||||
opensettings?.addEventListener("click", () => settings.style.display = "block");
|
||||
closesettings?.addEventListener("click", () => settings.style.display = "none");
|
||||
S("#form").onsubmit = e => {
|
||||
e.preventDefault();
|
||||
const txt = q.value.trim();
|
||||
if (!txt) return;
|
||||
if (isCoord(txt)) {
|
||||
const [lat, lon] = txt.split(/[ ,]+/).map(parseFloat);
|
||||
centerMap(lat, lon);
|
||||
} else {
|
||||
fetchPlaces(txt).then(results => {
|
||||
if (results[0]) centerMap(results[0].lat, results[0].lon);
|
||||
});
|
||||
}
|
||||
S("#suggestions").style.display = "none";
|
||||
};
|
||||
|
||||
// Pins
|
||||
const LS_PINS = "pokemaps_pins_v1";
|
||||
const savePins = pins => localStorage.setItem(LS_PINS, JSON.stringify(pins.slice(0, 100)));
|
||||
q.oninput = e => renderSuggestions(e.target.value);
|
||||
|
||||
// UI buttons
|
||||
S("#locate").onclick = locate;
|
||||
|
||||
// Pin system
|
||||
const LS_PINS = "pokemaps_pins";
|
||||
const loadPins = () => JSON.parse(localStorage.getItem(LS_PINS) || "[]");
|
||||
const savePins = pins => localStorage.setItem(LS_PINS, JSON.stringify(pins));
|
||||
|
||||
const renderPins = () => {
|
||||
const list = document.createElement("ul");
|
||||
const pins = loadPins();
|
||||
pinlist.innerHTML = "";
|
||||
if (!pins.length) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = "No pins yet.";
|
||||
pinlist.appendChild(li);
|
||||
return;
|
||||
return list.appendChild(li);
|
||||
}
|
||||
|
||||
pins.forEach((p, i) => {
|
||||
pins.forEach(p => {
|
||||
const li = document.createElement("li");
|
||||
li.innerHTML = `<strong>${p.name}</strong><small>${p.lat.toFixed(5)}, ${p.lon.toFixed(5)}</small>`;
|
||||
li.addEventListener("click", () => {
|
||||
state.lat = p.lat;
|
||||
state.lon = p.lon;
|
||||
state.layer = p.layer;
|
||||
apply();
|
||||
pinsPane.style.display = "none";
|
||||
});
|
||||
pinlist.appendChild(li);
|
||||
li.textContent = `${p.name} (${p.lat.toFixed(5)}, ${p.lon.toFixed(5)})`;
|
||||
li.onclick = () => centerMap(p.lat, p.lon);
|
||||
list.appendChild(li);
|
||||
});
|
||||
alert(list.outerHTML); // Simplified display (replace with modal/pane later)
|
||||
};
|
||||
|
||||
savepin?.addEventListener("click", () => {
|
||||
S("#savepin").onclick = () => {
|
||||
const pins = loadPins();
|
||||
pins.unshift({ name: new Date().toLocaleString(), lat: state.lat, lon: state.lon, layer: state.layer });
|
||||
pins.unshift({
|
||||
name: new Date().toLocaleString(),
|
||||
lat: state.lat,
|
||||
lon: state.lon,
|
||||
delta: state.delta,
|
||||
layer: state.layer
|
||||
});
|
||||
savePins(pins);
|
||||
renderPins();
|
||||
pinsPane.style.display = "block";
|
||||
});
|
||||
alert("Pin saved!");
|
||||
};
|
||||
|
||||
showpins?.addEventListener("click", () => {
|
||||
renderPins();
|
||||
pinsPane.style.display = "block";
|
||||
});
|
||||
S("#showpins").onclick = renderPins;
|
||||
|
||||
closepins?.addEventListener("click", () => {
|
||||
pinsPane.style.display = "none";
|
||||
});
|
||||
// Marker size
|
||||
document.documentElement.style.setProperty("--marker-size", "20px");
|
||||
|
||||
// Load
|
||||
apply();
|
||||
// Try parsing initial URL
|
||||
const sp = new URLSearchParams(location.search);
|
||||
if (sp.has("lat") && sp.has("lon")) {
|
||||
state.lat = parseFloat(sp.get("lat"));
|
||||
state.lon = parseFloat(sp.get("lon"));
|
||||
state.delta = parseFloat(sp.get("delta")) || state.delta;
|
||||
state.layer = sp.get("layer") || "mapnik";
|
||||
}
|
||||
|
||||
updateMap();
|
||||
})();
|
||||
</script>
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user