Add features
This commit is contained in:
parent
008aa845a2
commit
05120cf89d
|
@ -1,2 +1,2 @@
|
|||
data/
|
||||
tiles/
|
||||
/data
|
||||
/public/assets/tiles
|
||||
|
|
324
index.html
324
index.html
|
@ -1,324 +0,0 @@
|
|||
<!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>
|
2
ipmap.py
2
ipmap.py
|
@ -165,7 +165,7 @@ def generate_tiles(parquet_path: Path, tiles_dir: Path, *, tile_size = default_t
|
|||
|
||||
if not quiet:
|
||||
print(f"scaling {len(df)} coords down from {prev_tiles_per_side}x{prev_tiles_per_side} tiles to {tiles_per_side}x{tiles_per_side} tiles...", end = " ", flush = True)
|
||||
df = df.with_columns(x = pl.col("x") // scale, y = pl.col("y") // scale).group_by(["x", "y"]).agg(count = pl.sum("count"), rtt_us = pl.mean("rtt_us"))
|
||||
df = df.with_columns(x = pl.col("x") // scale, y = pl.col("y") // scale).group_by(["x", "y"]).agg(count = pl.sum("count"), rtt_us = pl.mean("rtt_us")) # maybe using median would be better?
|
||||
if not quiet:
|
||||
print(f"done with {len(df)} coords remaining")
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -0,0 +1,511 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<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: #eee;
|
||||
color: black;
|
||||
font-size: 1rem;
|
||||
padding: 0.6rem 2.8rem 0.6rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
.maplibregl-popup-close-button {
|
||||
height: 100%;
|
||||
width: 2.4rem;
|
||||
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;
|
||||
}
|
||||
.custom-control {
|
||||
user-select: none;
|
||||
}
|
||||
.custom-control summary {
|
||||
margin: 0;
|
||||
padding: 0.2rem 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.custom-control[open] summary {
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
.custom-control h3 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
display: inline;
|
||||
}
|
||||
.custom-control button {
|
||||
width: 100%;
|
||||
padding: 0 0.5rem;
|
||||
color: black;
|
||||
}
|
||||
.custom-control button.active {
|
||||
background-color: rgb(0 0 0/15%);
|
||||
}
|
||||
.maplibregl-ctrl button:not(:disabled):hover {
|
||||
background-color: rgb(0 0 0/10%);
|
||||
}
|
||||
.maplibregl-ctrl button:not(:disabled):active {
|
||||
background-color: rgb(0 0 0/15%);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div id="map"></div>
|
||||
</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.y |= (reflection & 1) << b
|
||||
coord.x |= ((reflection >>> 1) & 1) << b
|
||||
reflection ^= (1 << rotation)
|
||||
bits &= (-bits) & 1
|
||||
while (bits) {
|
||||
bits >>>= 1
|
||||
++rotation
|
||||
}
|
||||
if (++rotation >= 2)
|
||||
rotation -= 2
|
||||
}
|
||||
return coord
|
||||
}
|
||||
|
||||
const ipToString = ip => `${ip >> 24 & 0xFF}.${ip >> 16 & 0xFF}.${ip >> 8 & 0xFF}.${ip >> 0 & 0xFF}`
|
||||
const ipToRange = (ip, subnet) => `${ipToString((ip >> (32 - subnet)) << (32 - subnet))}/${subnet}`
|
||||
|
||||
const rangeToCoords = range => {
|
||||
const [a, b, c, d, subnet] = range.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/).slice(1).map(Number)
|
||||
const ip = a << 24 | b << 16 | c << 8 | d
|
||||
if (subnet % 2 === 0) {
|
||||
const subnetSize = 2 ** (32 - subnet)
|
||||
const subnetSideSize = Math.sqrt(subnetSize)
|
||||
const subnetsPerSide = 0x10000 / subnetSideSize
|
||||
const { x: hx, y: hy } = hilbertToCoords(ip + subnetSize / 2)
|
||||
const tx = Math.floor(hx / subnetSideSize)
|
||||
const ty = Math.floor(hy / subnetSideSize)
|
||||
const mxA = tx / subnetsPerSide
|
||||
const mxB = (tx + 1) / subnetsPerSide
|
||||
const myA = ty / subnetsPerSide
|
||||
const myB = (ty + 1) / subnetsPerSide
|
||||
return [
|
||||
new maplibregl.MercatorCoordinate(mxA, myA, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxB, myA, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxB, myB, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxA, myB, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxA, myA, 0).toLngLat().toArray(),
|
||||
]
|
||||
} else {
|
||||
const nearestSubnet = subnet + 1
|
||||
const subnetSize = 2 ** (32 - nearestSubnet)
|
||||
const subnetSideSize = Math.sqrt(subnetSize)
|
||||
const subnetsPerSide = 0x10000 / subnetSideSize
|
||||
const { x: hxA, y: hyA } = hilbertToCoords(ip + subnetSize / 2)
|
||||
const { x: hxB, y: hyB } = hilbertToCoords(ip + subnetSize + subnetSize / 2)
|
||||
const txA = Math.floor(hxA / subnetSideSize)
|
||||
const tyA = Math.floor(hyA / subnetSideSize)
|
||||
const txB = Math.floor(hxB / subnetSideSize)
|
||||
const tyB = Math.floor(hyB / subnetSideSize)
|
||||
const mxA = Math.min(txA, txB) / subnetsPerSide
|
||||
const mxB = (Math.max(txA, txB) + 1) / subnetsPerSide
|
||||
const myA = Math.min(tyA, tyB) / subnetsPerSide
|
||||
const myB = (Math.max(tyA, tyB) + 1) / subnetsPerSide
|
||||
return [
|
||||
new maplibregl.MercatorCoordinate(mxA, myA, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxB, myA, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxB, myB, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxA, myB, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(mxA, myA, 0).toLngLat().toArray(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: "map",
|
||||
attributionControl: false,
|
||||
renderWorldCopies: false,
|
||||
doubleClickZoom: false,
|
||||
dragRotate: false,
|
||||
pitchWithRotate: false,
|
||||
touchPitch: false,
|
||||
style: {
|
||||
version: 8,
|
||||
glyphs: "assets/glyphs/{fontstack}/{range}.pbf",
|
||||
sources: {},
|
||||
layers: []
|
||||
},
|
||||
center: [0, 0],
|
||||
minZoom: -2,
|
||||
maxZoom: 12,
|
||||
zoom: 0
|
||||
})
|
||||
|
||||
map.painter.context.extTextureFilterAnisotropic = undefined
|
||||
map.touchZoomRotate.disableRotation()
|
||||
|
||||
const tilesDir = "assets/tiles"
|
||||
const tilesId = "ipmap-tiles"
|
||||
const privateRangesId = "private-ranges"
|
||||
const dataP = fetch(`${tilesDir}/tiles.json`, { cache: "no-store" }).then(res => res.json())
|
||||
const privateRangePatternP = map.loadImage("assets/private-range-pattern.png")
|
||||
|
||||
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(tilesId, {
|
||||
type: "raster",
|
||||
tiles: [`${tilesDir}/${curDate}/${curVariant}/${curColormap}/{z}/{y}/{x}.png`],
|
||||
tileSize: 256,
|
||||
minzoom: 0,
|
||||
maxzoom: 8,
|
||||
})
|
||||
map.addLayer({
|
||||
id: tilesId,
|
||||
type: "raster",
|
||||
source: tilesId,
|
||||
paint: {
|
||||
"raster-resampling": "nearest"
|
||||
}
|
||||
})
|
||||
|
||||
const privateRanges = [
|
||||
{ range: "0.0.0.0/8", description: "Current Network Definition" },
|
||||
{ range: "10.0.0.0/8", description: "Private Networks" },
|
||||
{ range: "100.64.0.0/10", description: "CGNAT Transition" },
|
||||
{ range: "127.0.0.0/8", description: "Loopback" },
|
||||
{ range: "169.254.0.0/16", description: "Link-Local" },
|
||||
{ range: "172.16.0.0/12", description: "Private Networks" },
|
||||
{ range: "192.0.0.0/24", description: "DS-Lite Transition" },
|
||||
{ range: "192.0.2.0/24", description: "Documentation" },
|
||||
{ range: "192.88.99.0/24", description: "IPv4 to IPv6 Transition" },
|
||||
{ range: "192.168.0.0/16", description: "Private Networks" },
|
||||
{ range: "198.18.0.0/15", description: "Testing" },
|
||||
{ range: "198.51.100.0/24", description: "Documentation" },
|
||||
{ range: "203.0.113.0/24", description: "Documentation" },
|
||||
{ range: "224.0.0.0/4", description: "Multicast (Class D)" },
|
||||
{ range: "240.0.0.0/4", description: "Class E" },
|
||||
]
|
||||
|
||||
map.addSource(privateRangesId, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features: privateRanges.map(({ range, description }) => ({
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [rangeToCoords(range)]
|
||||
},
|
||||
properties: {
|
||||
range,
|
||||
description
|
||||
}
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const privateRangePatternId = "private-range-pattern"
|
||||
map.addImage(privateRangePatternId, (await privateRangePatternP).data)
|
||||
map.addLayer({
|
||||
id: privateRangesId,
|
||||
type: "fill",
|
||||
source: privateRangesId,
|
||||
paint: {
|
||||
"fill-color": "#0000FF",
|
||||
"fill-pattern": privateRangePatternId
|
||||
}
|
||||
})
|
||||
|
||||
;[...Array(8)].forEach((_, z) => {
|
||||
const labelsId = `subnet-labels-${z}`
|
||||
const width = 2 ** z
|
||||
const subnet = 2 * z
|
||||
const moff = 0.5 / width
|
||||
const hoff = Math.floor(0x10000 * moff)
|
||||
map.addSource(labelsId, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features: [...Array(width)].flatMap((_, y) => {
|
||||
const ny = y / width
|
||||
const my = ny + moff
|
||||
const hy = Math.floor(0x10000 * ny)
|
||||
return [...Array(width)].map((_, x) => {
|
||||
const nx = x / width
|
||||
const mx = nx + moff
|
||||
const hx = Math.floor(0x10000 * nx)
|
||||
return {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: new maplibregl.MercatorCoordinate(mx, my, 0).toLngLat().toArray()
|
||||
},
|
||||
properties: {
|
||||
name: ipToRange(coordsToHilbert({ x: hx, y: hy }), subnet)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
map.addLayer({
|
||||
id: labelsId,
|
||||
type: "symbol",
|
||||
source: labelsId,
|
||||
minzoom: Math.max(z - 0.5, 0),
|
||||
...(z < 7 ? { maxzoom: z + 0.5 } : {}),
|
||||
layout: {
|
||||
"text-field": "{name}",
|
||||
"text-font": ["Open Sans Bold"],
|
||||
"text-size": 24
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#FFFFFF"
|
||||
}
|
||||
})
|
||||
|
||||
const gridId = `grid-lines-${z}`
|
||||
map.addSource(gridId, {
|
||||
type: "geojson",
|
||||
data: {
|
||||
type: "FeatureCollection",
|
||||
features: [...Array(width + 1)].map((_, i) => i / width).flatMap(i => [
|
||||
{
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
new maplibregl.MercatorCoordinate(i, 0, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(i, 1, 0).toLngLat().toArray(),
|
||||
]
|
||||
}
|
||||
}, {
|
||||
type: "Feature",
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [
|
||||
new maplibregl.MercatorCoordinate(0, i, 0).toLngLat().toArray(),
|
||||
new maplibregl.MercatorCoordinate(1, i, 0).toLngLat().toArray(),
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
}
|
||||
})
|
||||
map.addLayer({
|
||||
id: gridId,
|
||||
type: "line",
|
||||
source: gridId,
|
||||
minzoom: Math.max(z - 0.5, 0),
|
||||
...(z < 7 ? { maxzoom: z + 0.5 } : {}),
|
||||
paint: {
|
||||
"line-color": "#FFFFFF",
|
||||
"line-width": 4
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const setStyle = (date, variant, colormap) => {
|
||||
if (date === curDate && variant === curVariant && colormap === curColormap || !data[date]?.[variant]?.includes(colormap))
|
||||
return false
|
||||
map.getSource(tilesId)?.setTiles([`${tilesDir}/${date}/${variant}/${colormap}/{z}/{y}/{x}.png`])
|
||||
curDate = date
|
||||
curVariant = variant
|
||||
curColormap = colormap
|
||||
return true
|
||||
}
|
||||
|
||||
const customControl = (name) => {
|
||||
const container = document.createElement("details")
|
||||
container.className = "maplibregl-ctrl maplibregl-ctrl-group custom-control"
|
||||
const header = document.createElement("h3")
|
||||
header.textContent = name
|
||||
const summary = document.createElement("summary")
|
||||
summary.replaceChildren(header)
|
||||
container.replaceChildren(summary)
|
||||
const setButtons = (buttons) => container.replaceChildren(summary, ...buttons.map(({ button }) => button))
|
||||
const addControl = () => map.addControl({
|
||||
onAdd: () => container,
|
||||
onRemove: () => container.parentNode.removeChild(container)
|
||||
}, "top-right")
|
||||
return { container, setButtons, addControl }
|
||||
}
|
||||
|
||||
const dateControl = customControl("Date")
|
||||
const variantControl = customControl("Variant")
|
||||
const colormapControl = customControl("Colormap")
|
||||
|
||||
const dates = Object.keys(data).sort()
|
||||
const dateButtons = dates.map(date => {
|
||||
const button = document.createElement("button")
|
||||
button.textContent = date
|
||||
if (curDate === date) button.className = "active"
|
||||
return { date, button }
|
||||
})
|
||||
dateButtons.forEach(({ date, button }) => button.addEventListener("click", () => {
|
||||
if (setStyle(date, curVariant, curColormap)) {
|
||||
dateButtons.forEach(({ button }) => button.className = "")
|
||||
button.className = "active"
|
||||
renderControls()
|
||||
}
|
||||
}))
|
||||
dateControl.setButtons(dateButtons)
|
||||
|
||||
const renderControls = () => {
|
||||
const variants = Object.keys(data[curDate]).sort()
|
||||
const variantButtons = variants.map(variant => {
|
||||
const button = document.createElement("button")
|
||||
button.textContent = variant
|
||||
if (curVariant === variant) button.className = "active"
|
||||
return { variant, button }
|
||||
})
|
||||
variantButtons.forEach(({ variant, button }) => button.addEventListener("click", () => {
|
||||
if (setStyle(curDate, variant, curColormap)) renderControls()
|
||||
}))
|
||||
variantControl.setButtons(variantButtons)
|
||||
|
||||
const colormaps = data[curDate][curVariant].sort()
|
||||
const colormapButtons = colormaps.map(colormap => {
|
||||
const button = document.createElement("button")
|
||||
button.textContent = colormap
|
||||
if (curColormap === colormap) button.className = "active"
|
||||
return { colormap, button }
|
||||
})
|
||||
colormapButtons.forEach(({ colormap, button }) => button.addEventListener("click", () => {
|
||||
if (setStyle(curDate, curVariant, colormap)) {
|
||||
colormapButtons.forEach(({ button }) => button.className = "")
|
||||
button.className = "active"
|
||||
}
|
||||
}))
|
||||
colormapControl.setButtons(colormapButtons)
|
||||
}
|
||||
|
||||
renderControls()
|
||||
|
||||
dateControl.addControl()
|
||||
variantControl.addControl()
|
||||
colormapControl.addControl()
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right")
|
||||
})
|
||||
|
||||
map.on("click", (e) => {
|
||||
const { x, y } = maplibregl.MercatorCoordinate.fromLngLat(e.lngLat, 0)
|
||||
const ip = coordsToHilbert({ x: Math.floor(0x10000 * x), y: Math.floor(0x10000 * y) })
|
||||
const subnet = Math.min(32, Math.round(map.getZoom()) * 2 + 18)
|
||||
const props = map.queryRenderedFeatures(e.point).find(f => f.layer.id === privateRangesId)?.properties
|
||||
const propsText = props ? `<br>Part of private range ${props.range}<br>Used for ${props.description}` : ""
|
||||
const text = subnet < 32 ?
|
||||
`Range: ${ipToRange(ip, subnet)}${propsText}` :
|
||||
`IP: ${ipToString(ip)}${propsText}`
|
||||
new maplibregl.Popup()
|
||||
.setHTML(text)
|
||||
.setLngLat(e.lngLat)
|
||||
.addTo(map)
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue