364 lines
9.5 KiB
Plaintext
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>
|