Add json metadata generation and improve site

This commit is contained in:
LilyRose2798 2024-04-06 00:31:10 +11:00
parent 4540c36cdd
commit 4997a7716c
2 changed files with 264 additions and 64 deletions

View File

@ -7,6 +7,7 @@
<title>IP Map</title> <title>IP Map</title>
<script src="https://unpkg.com/maplibre-gl@4.1.2/dist/maplibre-gl.js"></script> <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="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> <style>
body { body {
margin: 0; margin: 0;
@ -16,11 +17,15 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#map { main {
width: 100vh; width: 100vh;
height: 100%; height: 100%;
margin: 0 auto; margin: 0 auto;
} }
#map {
width: 100%;
height: 100%;
}
.maplibregl-canvas { .maplibregl-canvas {
cursor: pointer; cursor: pointer;
} }
@ -29,12 +34,54 @@
font-size: 1rem; font-size: 1rem;
padding: 0.8rem 1.2rem; padding: 0.8rem 1.2rem;
} }
.map-overlay {
position: absolute;
top: 1rem;
right: 1rem;
padding: 1.5rem;
background-color: #222;
color: #eee;
box-shadow: 3px 3px 2px rgba(0, 0, 0, 0.8);
border-radius: 3px;
}
.map-overlay h2 {
display: block;
margin: 0;
margin-bottom: 1rem;
}
#map-style-controls ul {
padding-left: 1em;
list-style-type: none;
}
#map-style-controls > ul {
padding-left: 0;
}
#map-style-controls ul.hidden {
display: none;
}
#map-style-controls label {
display: block;
padding: 0.5rem;
font-weight: bold;
}
#map-style-controls input[type=radio] {
padding: 0;
margin: 0;
border: 0;
margin-right: 0.5rem;
}
</style> </style>
</head> </head>
<body> <body>
<div id="map"></div> <main>
<div id="map"></div>
<div class="map-overlay">
<h2>Map Style</h2>
<div id="map-style-controls"><p>Loading available styles...</p></div>
</div>
</main>
<script> <script>
const hilbert_c2i = ({ x, y }) => { const coordsToHilbert = ({ x, y }) => {
let rotation = 0 let rotation = 0
let reflection = 0 let reflection = 0
let index = 0 let index = 0
@ -63,7 +110,7 @@
return index return index
} }
const hilbert_i2c = index => { const hilbertToCoords = index => {
let rotation = 0 let rotation = 0
let reflection = 0 let reflection = 0
let coord = { x: 0, y: 0 } let coord = { x: 0, y: 0 }
@ -85,7 +132,15 @@
return coord return coord
} }
const tilesDir = "tiles"
const sourceId = "ipmap-tiles-source"
const styleControlsDiv = document.getElementById("map-style-controls")
const dateDir = (date = new Date()) => `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}` const dateDir = (date = new Date()) => `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`
const getId = (date, variant, colormap) => `${date.replaceAll("-", "")}-${variant}-${colormap}`
const getTilesUrl = (date, variant, colormap) => `${tilesDir}/${date}/${variant}/${colormap}/{z}/{y}/{x}.png`
const getSourceId = (date, variant, colormap) => `ipmap-tiles-source-${getId(date, variant, colormap)}`
const getLayerId = (date, variant, colormap) => `ipmap-tiles-layer-${getId(date, variant, colormap)}`
const map = new maplibregl.Map({ const map = new maplibregl.Map({
container: "map", container: "map",
@ -95,27 +150,8 @@
dragRotate: false, dragRotate: false,
style: { style: {
version: 8, version: 8,
sources: { sources: {},
"ipmap-tiles": { layers: []
type: "raster",
tiles: [
"tiles/2024-03-30/density/jet/{z}/{y}/{x}.png" // change to using remote json with list of tilemaps
],
minzoom: 0,
maxzoom: 8,
tileSize: 256
}
},
layers: [
{
id: "ipmap-tiles-layer",
type: "raster",
source: "ipmap-tiles",
paint: {
"raster-resampling": "nearest"
}
}
]
}, },
center: [0, 0], center: [0, 0],
minZoom: -1, minZoom: -1,
@ -123,11 +159,135 @@
zoom: 0 zoom: 0
}) })
map.painter.context.extTextureFilterAnisotropic = undefined map.painter.context.extTextureFilterAnisotropic = undefined
const dataP = fetch(`${tilesDir}/tiles.json`).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: [getTilesUrl(curDate, curVariant, curColormap)],
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([getTilesUrl(date, variant, colormap)])
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)
}
styleControlsDiv.replaceChildren(dateList)
})
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-left") map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-left")
const toIp = v => `${v >> 24 & 0xFF}.${v >> 16 & 0xFF}.${v >> 8 & 0xFF}.${v >> 0 & 0xFF}` const toIp = v => `${v >> 24 & 0xFF}.${v >> 16 & 0xFF}.${v >> 8 & 0xFF}.${v >> 0 & 0xFF}`
map.on("click", (e) => { map.on("click", (e) => {
const { x, y } = maplibregl.MercatorCoordinate.fromLngLat(e.lngLat, 0) const { x, y } = maplibregl.MercatorCoordinate.fromLngLat(e.lngLat, 0)
const rawIp = hilbert_c2i({ x: Math.floor(0x10000 * x), y: Math.floor(0x10000 * y) }) 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 subnet = Math.min(32, Math.round(map.getZoom()) * 2 + 18)
const text = subnet < 32 ? const text = subnet < 32 ?
`Range: ${toIp((rawIp >> (32 - subnet)) << (32 - subnet))}/${subnet}` : `Range: ${toIp((rawIp >> (32 - subnet)) << (32 - subnet))}/${subnet}` :
@ -137,7 +297,6 @@
.setLngLat(e.lngLat) .setLngLat(e.lngLat)
.addTo(map) .addTo(map)
}) })
const setTileUrl = (date, variant, colormap) => map.getSource("ipmap-tiles").setTiles([`tiles/${dateDir(date)}/${variant}/${colormap}/{z}/{y}/{x}.png`])
</script> </script>
</body> </body>
</html> </html>

115
ipmap.py
View File

@ -4,6 +4,7 @@ import sys
import math import math
import functools import functools
import argparse import argparse
import json
from pathlib import Path from pathlib import Path
import png import png
import hilbert import hilbert
@ -14,9 +15,18 @@ from multiprocessing import Pool
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal from typing import Literal
def convert_to_parquet(csv_path: Path, parquet_path: Path, quiet=False): def dedup_preserving_order(vals: list) -> list:
seen = set()
result = []
for item in vals:
if item not in seen:
seen.add(item)
result.append(item)
return result
def convert_to_parquet(csv_path: Path, parquet_path: Path, quiet = False):
if not quiet: if not quiet:
print(f"scanning csv '{csv_path}' into parquet '{parquet_path}'...", end=" ", flush=True) print(f"scanning csv '{csv_path}' into parquet '{parquet_path}'...", end = " ", flush = True)
lf = pl.scan_csv(csv_path, schema={ lf = pl.scan_csv(csv_path, schema={
"saddr": pl.String, "saddr": pl.String,
"rtt_us": pl.UInt64, "rtt_us": pl.UInt64,
@ -28,7 +38,7 @@ def convert_to_parquet(csv_path: Path, parquet_path: Path, quiet=False):
lf = lf.with_columns(saddr = pl.col("saddr").str.split_exact(".", 3).struct.rename_fields(["a", "b", "c", "d"])) lf = lf.with_columns(saddr = pl.col("saddr").str.split_exact(".", 3).struct.rename_fields(["a", "b", "c", "d"]))
lf = lf.with_columns(saddr = pl.col("saddr").struct.field("a").cast(pl.UInt32) * 0x1000000 + pl.col("saddr").struct.field("b").cast(pl.UInt32) * 0x10000 + pl.col("saddr").struct.field("c").cast(pl.UInt32) * 0x100 + pl.col("saddr").struct.field("d").cast(pl.UInt32)) lf = lf.with_columns(saddr = pl.col("saddr").struct.field("a").cast(pl.UInt32) * 0x1000000 + pl.col("saddr").struct.field("b").cast(pl.UInt32) * 0x10000 + pl.col("saddr").struct.field("c").cast(pl.UInt32) * 0x100 + pl.col("saddr").struct.field("d").cast(pl.UInt32))
lf = lf.unique("saddr") lf = lf.unique("saddr")
lf = lf.with_columns(coords = pl.col("saddr").map_batches(functools.partial(hilbert.decode, num_dims=2, num_bits=16), pl.Array(pl.UInt16, 2), is_elementwise = True)) lf = lf.with_columns(coords = pl.col("saddr").map_batches(functools.partial(hilbert.decode, num_dims = 2, num_bits = 16), pl.Array(pl.UInt16, 2), is_elementwise = True))
lf = lf.with_columns(x = pl.col("coords").arr.get(0), y = pl.col("coords").arr.get(1)) lf = lf.with_columns(x = pl.col("coords").arr.get(0), y = pl.col("coords").arr.get(1))
lf = lf.drop("coords") lf = lf.drop("coords")
lf.sink_parquet(parquet_path) lf.sink_parquet(parquet_path)
@ -36,18 +46,18 @@ def convert_to_parquet(csv_path: Path, parquet_path: Path, quiet=False):
print("done") print("done")
def write_tile(path: Path, rows: np.ndarray): def write_tile(path: Path, rows: np.ndarray):
path.parent.mkdir(exist_ok=True, parents=True) path.parent.mkdir(exist_ok = True, parents = True)
png.Writer(rows.shape[0], rows.shape[1], greyscale=False, alpha=False).write_packed(path.open("wb"), rows) png.Writer(rows.shape[0], rows.shape[1], greyscale = False, alpha = False).write_packed(path.open("wb"), rows)
default_tile_size = 256 default_tile_size = 256
default_colormaps = ["viridis"] default_colormaps = ["viridis"]
default_variants = ["density", "rtt"] default_variants = ["density", "rtt"]
default_processes = 16 default_processes = 16
def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size=default_tile_size, def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size = default_tile_size,
variants: list[str] = default_variants, colormaps: list[str] = default_colormaps, variants: list[str] = default_variants, colormaps: list[str] = default_colormaps,
processes=default_processes, num_rows: int | None = None, processes = default_processes, num_rows: int | None = None,
skip_iters: int | None = None, quiet=False): skip_iters: int | None = None, json_path: Path | None = None, quiet = False):
if tile_size < 1 or tile_size > 0x10000 or tile_size & (tile_size - 1) != 0: if tile_size < 1 or tile_size > 0x10000 or tile_size & (tile_size - 1) != 0:
raise ValueError(f"tile size must be a power of 2 between 1 and {0x10000}") raise ValueError(f"tile size must be a power of 2 between 1 and {0x10000}")
@ -56,6 +66,7 @@ def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size=default_tile_s
if len(colormaps) == 0: if len(colormaps) == 0:
raise ValueError("must specify at least one colormap") raise ValueError("must specify at least one colormap")
colormaps = dedup_preserving_order(colormaps)
colormaps_by_name = { colormap: [bytes(c) for c in (Colormap(colormap).lut()[:,0:3] * (256.0 - np.finfo(np.float32).eps)).astype(np.uint8)] for colormap in colormaps } colormaps_by_name = { colormap: [bytes(c) for c in (Colormap(colormap).lut()[:,0:3] * (256.0 - np.finfo(np.float32).eps)).astype(np.uint8)] for colormap in colormaps }
generate_density = False generate_density = False
@ -68,8 +79,35 @@ def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size=default_tile_s
else: else:
raise ValueError(f"unknown variant '{variant}'") raise ValueError(f"unknown variant '{variant}'")
if json_path:
if json_path.is_dir():
raise ValueError("json path must not be a directory")
try:
tiles_dir_parts = tiles_dir.relative_to(json_path.parent).parts
except ValueError:
raise ValueError("tiles path must be relative to the json path")
try:
tile_metadata = json.loads(json_path.read_text(encoding = "UTF-8"))
except:
if not quiet: if not quiet:
print(f"reading parquet '{parquet_path}'...", end=" ", flush=True) print("json file not found at provided path, so it will be created instead")
tile_metadata = {}
tile_metadata_cur = tile_metadata
for part in tiles_dir_parts:
if not part in tile_metadata_cur:
tile_metadata_cur[part] = {}
tile_metadata_cur = tile_metadata_cur[part]
for variant in variants:
if not variant in tile_metadata_cur:
tile_metadata_cur[variant] = colormaps
else:
tile_metadata_cur[variant] = dedup_preserving_order(tile_metadata_cur[variant] + colormaps)
json_path.write_text(json.dumps(tile_metadata, indent=2), encoding = "UTF-8")
if not quiet:
print(f"wrote metadata to json file at '{json_path}'")
if not quiet:
print(f"reading parquet '{parquet_path}'...", end = " ", flush = True)
df = pl.read_parquet(parquet_path, columns = ["x", "y", "rtt_us"], n_rows=num_rows).with_columns(count = pl.lit(1, pl.UInt32)) df = pl.read_parquet(parquet_path, columns = ["x", "y", "rtt_us"], n_rows=num_rows).with_columns(count = pl.lit(1, pl.UInt32))
if not quiet: if not quiet:
print("done") print("done")
@ -81,18 +119,18 @@ def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size=default_tile_s
nonlocal df nonlocal df
if not quiet: if not quiet:
print(f"creating {type_name} image data with {colormap} colormap...", end=" ", flush=True) print(f"creating {type_name} image data with {colormap} colormap...", end = " ", flush = True)
image_data = np.zeros((tiles_per_side * tile_size, tiles_per_side * tile_size), dtype = "S3") image_data = np.zeros((tiles_per_side * tile_size, tiles_per_side * tile_size), dtype = "S3")
image_data[(df.get_column("y"), df.get_column("x"))] = (255 * df.get_column(col_name) // divisor).clip(0, 255).cast(pl.UInt8).replace(pl.int_range(256), colormaps_by_name[colormap], return_dtype=pl.Binary) image_data[(df.get_column("y"), df.get_column("x"))] = (255 * df.get_column(col_name) // divisor).clip(0, 255).cast(pl.UInt8).replace(pl.int_range(256), colormaps_by_name[colormap], return_dtype = pl.Binary)
if not quiet: if not quiet:
print("done") print("done")
if not quiet: if not quiet:
print(f"writing {tiles_per_side}x{tiles_per_side}={tiles_per_side * tiles_per_side} {type_name} images with {colormap} colormap...", end=" ", flush=True) print(f"writing {tiles_per_side}x{tiles_per_side}={tiles_per_side * tiles_per_side} {type_name} images with {colormap} colormap...", end = " ", flush = True)
with Pool(processes) as pool: with Pool(processes) as pool:
z = tiles_per_side.bit_length() - 1 z = tiles_per_side.bit_length() - 1
z_path = tiles_dir / type_name / colormap / f"{z}" z_path = tiles_dir / type_name / colormap / f"{z}"
z_path.mkdir(exist_ok=True, parents=True) z_path.mkdir(exist_ok = True, parents = True)
pool.starmap(write_tile, [ pool.starmap(write_tile, [
(z_path / f"{y}" / f"{x}.png", image_data[ (z_path / f"{y}" / f"{x}.png", image_data[
y * tile_size : y * tile_size + tile_size, y * tile_size : y * tile_size + tile_size,
@ -115,8 +153,8 @@ def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size=default_tile_s
possible_overlaps *= scale * scale possible_overlaps *= scale * scale
if not quiet: 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) 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"))
if not quiet: if not quiet:
print(f"done with {len(df)} coords remaining") print(f"done with {len(df)} coords remaining")
@ -133,7 +171,7 @@ def generate_tiles(parquet_path: Path, tiles_dir: Path, tile_size=default_tile_s
if generate_density: if generate_density:
generate_images(colormap, "density", "count", 256 if possible_overlaps == 1 else possible_overlaps) generate_images(colormap, "density", "count", 256 if possible_overlaps == 1 else possible_overlaps)
if generate_rtt: if generate_rtt:
generate_images(colormap, "rtt", "rtt_us", int(df.get_column("rtt_us").std())) generate_images(colormap, "rtt", "rtt_us", int(df.get_column("rtt_us").std() / (2.0 * tiles_per_side.bit_length() ** 0.5)))
if tiles_per_side == 1: if tiles_per_side == 1:
break break
scale_down_coords() scale_down_coords()
@ -150,40 +188,43 @@ class IpMapArgs:
processes: int processes: int
num_rows: int | None num_rows: int | None
skip_iters: int | None skip_iters: int | None
json: str | None
def parse_list_arg(arg: str): def parse_list_arg(arg: str):
return [x.strip().lower() for x in arg.split(",") if x.strip()] return [x.strip().lower() for x in arg.split(",") if x.strip()]
def main(): def main():
parser = argparse.ArgumentParser("ipmap") parser = argparse.ArgumentParser("ipmap")
parser.add_argument("-q", "--quiet", action="store_true", help="decrease output verbosity") parser.add_argument("-q", "--quiet", action = "store_true", help = "decrease output verbosity")
subparsers = parser.add_subparsers(dest="command", required=True, help="the command to run") subparsers = parser.add_subparsers(dest = "command", required = True, help = "the command to run")
convert_parser = subparsers.add_parser("convert", help="convert scan data from csv to parquet format") convert_parser = subparsers.add_parser("convert", help = "convert scan data from csv to parquet format")
convert_parser.add_argument("input", help="the input path of the csv file to read the scan data from") convert_parser.add_argument("input", help = "the input path of the csv file to read the scan data from")
convert_parser.add_argument("output", help="the output path of the parquet file to save the converted scan data to") convert_parser.add_argument("output", help = "the output path of the parquet file to save the converted scan data to")
generate_parser = subparsers.add_parser("generate", help="generate tile images from scan data in parquet format") generate_parser = subparsers.add_parser("generate", help = "generate tile images from scan data in parquet format")
generate_parser.add_argument("-t", "--tile-size", default=default_tile_size, type=int, help="the tile size to use (default: %(default)s)") generate_parser.add_argument("-t", "--tile-size", default = default_tile_size, type = int, help = "the tile size to use (default: %(default)s)")
generate_parser.add_argument("-v", "--variants", default=",".join(default_variants), help="a comma separated list of variants to generate (default: %(default)s)") generate_parser.add_argument("-v", "--variants", default = ",".join(default_variants), help = "a comma separated list of variants to generate (default: %(default)s)")
generate_parser.add_argument("-c", "--colormaps", default=",".join(default_colormaps), help="a comma separated list of colormaps to generate (default: %(default)s)") generate_parser.add_argument("-c", "--colormaps", default = ",".join(default_colormaps), help = "a comma separated list of colormaps to generate (default: %(default)s)")
generate_parser.add_argument("-p", "--processes", default=default_processes, type=int, help="how many processes to spawn for saving images (default: %(default)s)") generate_parser.add_argument("-p", "--processes", default = default_processes, type = int, help = "how many processes to spawn for saving images (default: %(default)s)")
generate_parser.add_argument("-n", "--num-rows", type=int, help="how many rows to read from the scan data (default: all)") generate_parser.add_argument("-n", "--num-rows", type = int, help = "how many rows to read from the scan data (default: all)")
generate_parser.add_argument("-s", "--skip-iters", type=int, help="how many iterations to skip generating images for (default: none)") generate_parser.add_argument("-s", "--skip-iters", type = int, help = "how many iterations to skip generating images for (default: none)")
generate_parser.add_argument("input", help="the input path of the parquet file to read the scan data from") generate_parser.add_argument("-j", "--json", help = "the path for the json file to store metadata about the tile images (default: none)")
generate_parser.add_argument("output", help="the output path to save the generated tile images to") generate_parser.add_argument("input", help = "the input path of the parquet file to read the scan data from")
args = parser.parse_args(namespace=IpMapArgs) generate_parser.add_argument("output", help = "the output path to save the generated tile images to")
args = parser.parse_args(namespace = IpMapArgs)
try: try:
if args.command == "convert": if args.command == "convert":
convert_to_parquet(csv_path=Path(args.input), parquet_path=Path(args.output), quiet=args.quiet) convert_to_parquet(csv_path = Path(args.input), parquet_path = Path(args.output), quiet = args.quiet)
elif args.command == "generate": elif args.command == "generate":
generate_tiles(parquet_path=Path(args.input), tiles_dir=Path(args.output), generate_tiles(parquet_path = Path(args.input), tiles_dir = Path(args.output),
tile_size=args.tile_size, variants=parse_list_arg(args.variants), tile_size = args.tile_size, variants = parse_list_arg(args.variants),
colormaps=parse_list_arg(args.colormaps), processes=args.processes, colormaps = parse_list_arg(args.colormaps), processes = args.processes,
num_rows=args.num_rows, skip_iters=args.skip_iters, quiet=args.quiet) num_rows = args.num_rows, skip_iters = args.skip_iters,
json_path = Path(args.json) if args.json else None, quiet = args.quiet)
else: else:
raise ValueError("invalid command") raise ValueError("invalid command")
except ValueError as e: except ValueError as e:
print(f"error: {e}") print(f"error: {e}", file = sys.stderr)
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":