poke/html/map.ejs
2025-08-17 22:20:14 +02:00

364 lines
9.5 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 {
--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 {
font: 14px/1.4 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Arial;
background: #000;
color: var(--fg);
}
.app {
display: flex;
flex-direction: column;
height: 100%;
}
.bar {
display: flex;
gap: 8px;
align-items: center;
padding: 8px var(--pad);
backdrop-filter: var(--glass);
-webkit-backdrop-filter: var(--glass);
background: var(--bg);
}
.search {
flex: 1;
position: relative;
}
.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>
<div class="app" id="app">
<div class="bar">
<div class="search">
<form id="form" autocomplete="off">
<input id="q" type="search" placeholder="Search or paste lat,lon…" aria-expanded="false" aria-controls="suggestions" />
</form>
<ul id="suggestions" class="suggest"></ul>
</div>
<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 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" 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>
<script>
(async () => {
const S = sel => document.querySelector(sel);
const map = S("#map");
const q = S("#q");
const marker = S("#marker");
const coords = S("#coords");
const state = {
lat: 30.410156,
lon: 72.448792,
delta: 0.25,
layer: "mapnik"
};
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(3)} · ${state.layer}`;
coords.style.display = "block";
};
const centerMap = (lat, lon) => {
state.lat = lat;
state.lon = lon;
updateMap();
};
const locate = () => {
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 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 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";
};
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";
};
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();
if (!pins.length) {
const li = document.createElement("li");
li.textContent = "No pins yet.";
return list.appendChild(li);
}
pins.forEach(p => {
const li = document.createElement("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)
};
S("#savepin").onclick = () => {
const pins = loadPins();
pins.unshift({
name: new Date().toLocaleString(),
lat: state.lat,
lon: state.lon,
delta: state.delta,
layer: state.layer
});
savePins(pins);
alert("Pin saved!");
};
S("#showpins").onclick = renderPins;
// Marker size
document.documentElement.style.setProperty("--marker-size", "20px");
// 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>
</body>
</html>