ipmap/index.html

325 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta name="color-scheme" content="dark light">
<title>IP Map</title>
<script src="https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.js"></script>
<link rel="stylesheet" href="https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.css" />
<link rel="shortcut icon" href="data:image/png;base64,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" />
<style>
body {
margin: 0;
padding: 0;
background-color: #111;
}
html, body {
width: 100%;
height: 100%;
}
main {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-family: sans-serif;
}
#map {
width: min(100vw, 100vh);
height: min(100vw, 100vh);
}
.maplibregl-canvas {
cursor: pointer;
}
.maplibregl-popup {
max-width: unset !important;
}
.maplibregl-popup-content {
background-color: #333;
font-size: 1rem;
padding: 0.6rem 2.8rem 0.6rem 1rem;
border-radius: 0.5rem;
}
.maplibregl-popup-close-button {
height: 100%;
aspect-ratio: 1 / 1;
margin: 0;
padding: 0;
padding-bottom: 0.25rem;
font-size: 1.5rem;
}
.map-overlay {
position: absolute;
top: 1rem;
left: 1rem;
padding: 0.6rem;
background-color: #333;
color: #eee;
box-shadow: 3px 3px 2px rgba(0, 0, 0, 0.8);
border-radius: 0.5rem;
max-height: calc(100% - 2rem);
box-sizing: border-box;
overflow-y: scroll;
}
.map-overlay summary {
margin: 0 0.4rem;
}
.map-overlay h2 {
display: inline-block;
margin: 0 0 0 0.4rem;
user-select: none;
vertical-align: middle;
}
#map-style-controls {
margin-top: 0.6rem;
}
#map-style-controls ul {
padding-left: 1em;
list-style-type: none;
margin: 0;
}
#map-style-controls > ul {
padding-left: 0;
}
#map-style-controls ul.hidden {
display: none;
}
#map-style-controls label {
display: block;
padding: 0.2rem 1rem 0.2rem 0.4rem;
user-select: none;
}
#map-style-controls input[type=radio] {
padding: 0;
margin: 0;
border: 0;
margin-right: 0.5rem;
}
</style>
</head>
<body>
<main>
<div id="map"></div>
<details class="map-overlay">
<summary><h2>Style</h2></summary>
<div id="map-style-controls"><p>Loading available styles...</p></div>
</details>
</main>
<script>
const coordsToHilbert = ({ x, y }) => {
let rotation = 0
let reflection = 0
let index = 0
for (let b = 15; b >= 0; b--) {
let bits = reflection
reflection = (y >>> b) & 1
reflection |= ((x >>> b) & 1) << 1
bits = bits ^ reflection
bits = ((bits >>> rotation) | (bits << (2 - rotation))) & 3
index |= bits << (b << 1)
reflection ^= (1 << rotation)
bits = bits & (-bits) & 1
while (bits) {
++rotation
bits >>>= 1
}
if (++rotation >= 2)
rotation -= 2
}
index ^= 0x2aaaaaaa
for (let d = 1; d < 32; d *= 2) {
let t = index >>> d
if (!t) break
index ^= t
}
return index
}
const hilbertToCoords = index => {
let rotation = 0
let reflection = 0
let coord = { x: 0, y: 0 }
index ^= (index >>> 1) ^ 0x2aaaaaaa
for (let b = 15; b >= 0; b--) {
var bits = index >>> (2 * b) & 3
reflection ^= ((bits >>> (2 - rotation)) | (bits << rotation)) & 3
coord.x |= (reflection & 1) << b
coord.y |= ((reflection >>> 1) & 1) << b
reflection ^= (1 << rotation)
bits &= (-bits) & 1
while (bits) {
bits >>>= 1
++rotation
}
if (++rotation >= 2)
rotation -= 2
}
return coord
}
const map = new maplibregl.Map({
container: "map",
attributionControl: false,
renderWorldCopies: false,
doubleClickZoom: false,
dragRotate: false,
pitchWithRotate: false,
touchPitch: false,
style: {
version: 8,
sources: {},
layers: []
},
center: [0, 0],
minZoom: -2,
maxZoom: 12,
zoom: 0
})
map.painter.context.extTextureFilterAnisotropic = undefined
map.touchZoomRotate.disableRotation()
const tilesDir = "tiles"
const sourceId = "ipmap-tiles-source"
const dataP = fetch(`${tilesDir}/tiles.json`, { cache: "no-store" }).then(res => res.json())
map.once("style.load", async () => {
const data = await dataP
const flatData = Object.entries(data)
.sort(([a], [b]) => a.localeCompare(b))
.flatMap(([date, variantData]) =>
Object.entries(variantData)
.sort(([a], [b]) => a.localeCompare(b))
.flatMap(([variant, colormaps]) => colormaps
.sort((a, b) => a.localeCompare(b))
.flatMap(colormap => ({ date, variant, colormap }))))
if (flatData.length === 0) {
console.log("no data found")
return
}
let { date: curDate, variant: curVariant, colormap: curColormap } = flatData[flatData.length - 1]
map.addSource(sourceId, {
type: "raster",
tiles: [`${tilesDir}/${curDate}/${curVariant}/${curColormap}/{z}/{y}/{x}.png`],
tileSize: 256,
minzoom: 0,
maxzoom: 8,
})
map.addLayer({
id: "ipmap-tiles-layer",
type: "raster",
source: sourceId,
paint: {
"raster-resampling": "nearest"
}
})
const setStyle = (date, variant, colormap) => {
if (date === curDate && variant === curVariant && colormap === curColormap || !data[date]?.[variant]?.includes(colormap))
return
map.getSource(sourceId)?.setTiles([`${tilesDir}/${date}/${variant}/${colormap}/{z}/{y}/{x}.png`])
curDate = date
curVariant = variant
curColormap = colormap
}
const dateList = document.createElement("ul")
for (const [date, variantData] of Object.entries(data).sort(([a], [b]) => a.localeCompare(b))) {
const isCurDate = date === curDate
const dateInput = document.createElement("input")
dateInput.type = "radio"
dateInput.name = "date"
dateInput.value = date
dateInput.checked = isCurDate
const dateLabel = document.createElement("label")
dateLabel.appendChild(dateInput)
dateLabel.appendChild(document.createTextNode(date))
const dateItem = document.createElement("li")
dateItem.appendChild(dateLabel)
const variantList = document.createElement("ul")
if (!isCurDate) variantList.className = "hidden"
for (const [variant, colormaps] of Object.entries(variantData).sort(([a], [b]) => a.localeCompare(b))) {
const isCurVariant = variant === curVariant
const variantInput = document.createElement("input")
variantInput.type = "radio"
variantInput.name = `${date}-variant`
variantInput.value = variant
variantInput.checked = isCurVariant
const variantLabel = document.createElement("label")
variantLabel.appendChild(variantInput)
variantLabel.appendChild(document.createTextNode(variant))
const variantItem = document.createElement("li")
variantItem.appendChild(variantLabel)
const colormapList = document.createElement("ul")
if (!isCurVariant) colormapList.classList.add("hidden")
for (const colormap of colormaps.sort((a, b) => a.localeCompare(b))) {
const isCurColormap = colormap === curColormap
const colormapInput = document.createElement("input")
colormapInput.type = "radio"
colormapInput.name = `${date}-${variant}-colormap`
colormapInput.value = colormap
colormapInput.checked = isCurColormap
const colormapLabel = document.createElement("label")
colormapLabel.appendChild(colormapInput)
colormapLabel.appendChild(document.createTextNode(colormap))
const colormapItem = document.createElement("li")
colormapItem.appendChild(colormapLabel)
colormapItem.addEventListener("click", e => {
;[...dateList.children].forEach(el =>
[...el.lastChild.children].forEach(el =>
[...el.lastChild.children].forEach(el => {
const cb = el.firstChild.firstChild
cb.checked = cb.value === colormap
})))
setStyle(curDate, curVariant, colormap)
})
colormapList.appendChild(colormapItem)
}
variantInput.addEventListener("click", e => {
;[...dateList.children].forEach(el =>
[...el.lastChild.children].forEach(el => {
const cb = el.firstChild.firstChild
const isCur = cb.value === variant
el.lastChild.className = isCur ? "" : "hidden"
cb.checked = isCur
}))
setStyle(curDate, variant, curColormap)
})
variantItem.appendChild(colormapList)
variantList.appendChild(variantItem)
}
dateInput.addEventListener("click", e => {
;[...dateList.children].forEach(el => {
const cb = el.firstChild.firstChild
const isCur = cb.value === date
el.lastChild.className = isCur ? "" : "hidden"
})
setStyle(date, curVariant, curColormap)
})
dateItem.appendChild(variantList)
dateList.appendChild(dateItem)
}
document.getElementById("map-style-controls").replaceChildren(dateList)
})
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right")
const toIp = v => `${v >> 24 & 0xFF}.${v >> 16 & 0xFF}.${v >> 8 & 0xFF}.${v >> 0 & 0xFF}`
map.on("click", (e) => {
const { x, y } = maplibregl.MercatorCoordinate.fromLngLat(e.lngLat, 0)
const rawIp = coordsToHilbert({ x: Math.floor(0x10000 * x), y: Math.floor(0x10000 * y) })
const subnet = Math.min(32, Math.round(map.getZoom()) * 2 + 18)
const text = subnet < 32 ?
`Range: ${toIp((rawIp >> (32 - subnet)) << (32 - subnet))}/${subnet}` :
`IP: ${toIp(rawIp)}`
new maplibregl.Popup()
.setHTML(text)
.setLngLat(e.lngLat)
.addTo(map)
})
</script>
</body>
</html>