From e7d99cc1bb980b2a019c9680ab146fa1c4c3e8ec Mon Sep 17 00:00:00 2001 From: Corentin Date: Fri, 17 Oct 2025 04:02:04 +0900 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 1 + consolidate_tiles.py | 87 ++++++++++++++++++++++++++++++++ public/images/location_city.svg | 1 + public/images/marker-icon.png | Bin 0 -> 1466 bytes public/images/marker-shadow.png | Bin 0 -> 618 bytes public/images/nutrition.svg | 1 + public/index.html | 58 +++++++++++++++++++++ public/index.js | 81 +++++++++++++++++++++++++++++ public/leaflet_1.9.4.css | 3 ++ public/leaflet_1.9.4.js | 3 ++ public/marker.json | 3 ++ pyproject.toml | 61 ++++++++++++++++++++++ server.py | 51 +++++++++++++++++++ 14 files changed, 352 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 consolidate_tiles.py create mode 100644 public/images/location_city.svg create mode 100644 public/images/marker-icon.png create mode 100644 public/images/marker-shadow.png create mode 100644 public/images/nutrition.svg create mode 100644 public/index.html create mode 100644 public/index.js create mode 100644 public/leaflet_1.9.4.css create mode 100644 public/leaflet_1.9.4.js create mode 100644 public/marker.json create mode 100644 pyproject.toml create mode 100644 server.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..0c5e216 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +public/leaflet* filter=lfs diff=lfs merge=lfs -text +public/marker.json filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6320cd2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data \ No newline at end of file diff --git a/consolidate_tiles.py b/consolidate_tiles.py new file mode 100644 index 0000000..14d0130 --- /dev/null +++ b/consolidate_tiles.py @@ -0,0 +1,87 @@ +from argparse import ArgumentParser +from pathlib import Path +import time +import urllib.error +import urllib.request + + +def download_tile(zoom: int, x: int, y: int, tile_path: Path): + osm_timeout = 3 + http_code_ok = 200 + + request = urllib.request.Request(f'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png') + request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0') + request.add_header('Accept', 'image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5') + request.add_header('Accept-Language', 'en-US,en;q=0.5') + request.add_header('Accept-Encoding', 'gzip, deflate, br, zstd') + try: + with urllib.request.urlopen(request, timeout=osm_timeout) as response: + if response.status != http_code_ok: + raise RuntimeError( + f'tile.openstreetmap.org answer is not OK (status: {response.status}) : {response.read()}') + tile_path.write_bytes(response.read()) + except urllib.error.URLError as error: + raise RuntimeError(f'Cannot retrieve tile: {error}') from error + + +def main(): + parser = ArgumentParser() + parser.add_argument('data', type=Path, help='Path to tile data to consolidate') + arguments = parser.parse_args() + + data_path: Path = arguments.data + del arguments + + zoom_levels = sorted([int(p.name) for p in data_path.iterdir()]) + download_time = 0.2 + try: + for zoom in zoom_levels: + x_min = 10_000_000 + x_max = -10_000_000 + y_min = 10_000_000 + y_max = -10_000_000 + tile_count = 0 + for tile_path in (data_path / str(zoom)).rglob('*.png'): + x_value = int(tile_path.parent.name) + x_min = min(x_min, x_value) + x_max = max(x_max, x_value) + y_value = int(tile_path.stem) + y_min = min(y_min, y_value) + y_max = max(y_max, y_value) + tile_count += 1 + total_tile_count = (x_max - x_min + 1) * (y_max - y_min + 1) + print(f'{zoom=} x in [{x_min}, {x_max}], y in [{y_min}, {y_max}]:' + f' {tile_count} files out of {total_tile_count} -> {total_tile_count - tile_count} to download') + start_download = time.monotonic() + for x in range(x_min, x_max + 1): + for y in range(y_min, y_max + 1): + tile_path = Path(data_path / str(zoom) / str(x) / f'{y}.png') + if tile_path.exists(): + continue + try: + if not tile_path.parent.exists(): + tile_path.parent.mkdir(parents=True, exist_ok=True) + download_tile(zoom=zoom, x=x, y=y, tile_path=tile_path) + time.sleep(0.1) + except RuntimeError as error: + print(error) + continue + tile_count += 1 + if tile_count % 5 == 0 or tile_count == total_tile_count: + download_time = 0.99 * download_time + (0.01 * (time.monotonic() - start_download) / 5) + remaining_time = int((total_tile_count - tile_count) * download_time) + remaining_hours = remaining_time // 3600 + remaining_time -= 3600 * remaining_hours + remaining_minutes = remaining_time // 60 + remaining_time -= 60 * remaining_minutes + print(f'Downloading tiles {tile_count} / {total_tile_count}' + f' ({remaining_hours:02d}:{remaining_minutes:02d}:{remaining_time:02d} remaining)', + end='\r') + start_download = time.monotonic() + print() + except KeyboardInterrupt: + print('\rDo') + + +if __name__ == '__main__': + main() diff --git a/public/images/location_city.svg b/public/images/location_city.svg new file mode 100644 index 0000000..aff28cd --- /dev/null +++ b/public/images/location_city.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/images/marker-icon.png b/public/images/marker-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..950edf24677ded147df13b26f91baa2b0fa70513 GIT binary patch literal 1466 zcmV;r1x5OaP)P001cn1^@s6z>|W`000GnNklGNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6)aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlft#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR!XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTnMpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3*`;(#{U^d-vY)&xEE>n5lYl&T?Amke9$Lam@{1K@O ze*LXqlKQHiv=gx+V^Cbb2?z@ISBQ*3amF;9UJ3SBg(N|710TLamQmYZ&Qjn2LuO<* zCZlB4n%@pc&7NNnY1}x+NWpHlq`OJEo|`aYN9<`RBUB+79g;>dgb6YlfN#kGL?lO_ z!6~M^7sOnbsUkKk<@Ysie&`G>ruxH&Mgy&8;i=A zB9OO!xR{AyODw>DS-q5YM{0ExFEAzt zm>RdS+ssW(-8|?xr0(?$vBVB*%(xDLtq3Hf0I5yFm<_g=W2`QWAax{1rWVH=I!VrP zs(rTFX@W#t$hXNvbgX`gK&^w_YD;CQ!B@e0QbLIWaKAXQe2-kkloo;{iF#6}z!4=W zi$giRj1{ zt;2w`VSCF#WE&*ev7jpsC=6175@(~nTE2;7M-L((0bH@yG}-TB$R~WXd?tA$s3|%y zA`9$sA(>F%J3ioz<-LJl*^o1|w84l>HBR`>3l9c8$5Xr@xCiIQ7{x$fMCzOk_-M=% z+{a_Q#;42`#KfUte@$NT77uaTz?b-fBe)1s5XE$yA79fm?KqM^VgLXD07*qoM6N<$ Ef<_J(9smFU literal 0 HcmV?d00001 diff --git a/public/images/nutrition.svg b/public/images/nutrition.svg new file mode 100644 index 0000000..d665ef3 --- /dev/null +++ b/public/images/nutrition.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..90b19fb --- /dev/null +++ b/public/index.html @@ -0,0 +1,58 @@ + + + + + 神山町 + + + + +
+ + + + diff --git a/public/index.js b/public/index.js new file mode 100644 index 0000000..bd63558 --- /dev/null +++ b/public/index.js @@ -0,0 +1,81 @@ +let map = L.map('map'); +map.setMaxBounds(L.latLngBounds(L.latLng(33.9033300, 134.2141300), L.latLng(34.08, 134.4836400))); +map.setView([33.96515, 134.34889], 13); + +//L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { +L.tileLayer('/{z}/{x}/{y}.png', { + minZoom: 13, + maxZoom: 19, + attribution: '© OpenStreetMap' +}).addTo(map); + + +// Click event +let popup = L.popup(); +function onMapClick(event) { + popup + .setLatLng(event.latlng) + .setContent(`GPS: ${[event.latlng.lat, event.latlng.lng]}`) + .openOn(map); +} +map.on('click', onMapClick); + +// Markers +let markers = {}; +let categories = {}; +fetch('/marker.json').then(response => response.json()).then(marker_data => { + for(let category_name in marker_data) + { + const category_data = marker_data[category_name]; + let category_markers = []; + let icon = L.icon({iconUrl: `/images/${category_data.icon}`, iconSize: [32, 32]}); + category_data.places.forEach(marker_info => { + const marker = L.marker( + [marker_info.lat, marker_info.lon], + {title: marker_info.name, icon: icon}).bindPopup(`

${marker_info.name}

${marker_info.description}`); + category_markers.push(marker); + }); + categories[category_name] = { + icon: icon, icon_url: `/images/${category_data.icon}`, name: category_data.name, enable: false}; + markers[category_name] = category_markers; + } + + // Menu + let menu = L.control(); + menu.onAdd = _map => { + let div = L.DomUtil.create('div', 'menu') + for(let category_name in markers) + { + let category_elt = document.createElement('div'); + category_elt.classList.add('menu-category'); + category_elt.classList.add('menu-disable'); + + let category_icon = document.createElement('img'); + category_icon.src = categories[category_name].icon_url; + category_elt.appendChild(category_icon); + + let category_label = document.createElement('label'); + category_label.innerHTML = categories[category_name].name; + category_elt.appendChild(category_label); + + category_elt.onclick = event => { + if(categories[category_name].enable) + { + category_elt.classList.add('menu-disable'); + markers[category_name].forEach(marker => { marker.remove(); }); + } + else + { + markers[category_name].forEach(marker => { marker.addTo(map); }); + category_elt.classList.remove('menu-disable'); + } + categories[category_name].enable = !categories[category_name].enable; + event.preventDefault(); + event.stopPropagation(); + }; + div.appendChild(category_elt); + } + return div; + }; + menu.addTo(map); +}); \ No newline at end of file diff --git a/public/leaflet_1.9.4.css b/public/leaflet_1.9.4.css new file mode 100644 index 0000000..d60c695 --- /dev/null +++ b/public/leaflet_1.9.4.css @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79cab275563428aac0ad39ee4c4271610d0b40133390bffd96331224aebbc689 +size 14852 diff --git a/public/leaflet_1.9.4.js b/public/leaflet_1.9.4.js new file mode 100644 index 0000000..918bf29 --- /dev/null +++ b/public/leaflet_1.9.4.js @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db49d009c841f5ca34a888c96511ae936fd9f5533e90d8b2c4d57596f4e5641a +size 147552 diff --git a/public/marker.json b/public/marker.json new file mode 100644 index 0000000..841c90e --- /dev/null +++ b/public/marker.json @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d756d5b9b1fe557cd7933f60aca6e0fc46b866aa032c1933291050194b5c3b9d +size 748 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6ff3213 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,61 @@ +[project] +name = "kamiyama-map" +version = "0.0.1" +requires-python = ">=3.10" + +[project.optional-dependencies] +test = ["pytest"] + +[tool.ruff] +cache-dir = "/tmp/ruff" +exclude = [ + ".git", + ".ruff_cache", + ".venv" +] +line-length = 120 +indent-width = 4 +target-version = "py310" + + +[tool.isort] +combine_as_imports = true +force_sort_within_sections = true +lexicographical = true +lines_after_imports = 2 +multi_line_output = 4 +no_sections = false +order_by_type = true + +[tool.pytest.ini_options] +addopts = "-p no:cacheprovider -s -vv" +testpaths = ["tests"] +pythonpath = ["."] + +[tool.ruff.lint] +preview = true +select = ["A", "ARG", "B", "C", "E", "F", "FURB", "G", "I","ICN", "ISC", "PERF", "PIE", "PL", "PLE", "PTH", + "Q", "RET", "RSE", "RUF", "SLF", "SIM", "T20", "TCH", "UP", "W"] +ignore = ["E275", "FURB140", "I001", "PERF203", "RET502", "RET503", "SIM105", "T201"] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = ["SLF001"] + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.lint.pylint] +max-args=16 +max-branches=24 +max-locals=16 +max-nested-blocks=8 +max-public-methods=16 +max-returns=8 +max-statements=96 + +[tool.ruff.lint.mccabe] +max-complexity = 20 + +[tool.coverage.run] +command_line = "-m pytest" +omit = ["tests/*"] diff --git a/server.py b/server.py new file mode 100644 index 0000000..484a73e --- /dev/null +++ b/server.py @@ -0,0 +1,51 @@ +from argparse import ArgumentParser +from pathlib import Path +import urllib.error +import urllib.request + +from flask import Flask, send_file + + +def main(): + parser = ArgumentParser() + parser.add_argument('--debug', action='store_true', help='Run in debug mode (auto-reload on change)') + arguments = parser.parse_args() + + debug_mode: bool = arguments.debug + del arguments + + app = Flask('map_server', static_folder='public', static_url_path='/') + osm_timeout = 3 + http_code_ok = 200 + + @app.route('/') + def _index(): + return app.send_static_file('index.html') + + @app.route('///.png') + def _tile(zoom: int, x: int, y: int): + tile_path = Path('data', str(zoom), str(x), f'{y}.png') + if tile_path.exists(): + return send_file(tile_path) + if not tile_path.parent.exists(): + tile_path.parent.mkdir(parents=True, exist_ok=True) + request = urllib.request.Request(f'https://tile.openstreetmap.org/{zoom}/{x}/{y}.png') + request.add_header('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:138.0) Gecko/20100101 Firefox/138.0') + request.add_header('Accept', 'image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5') + request.add_header('Accept-Language', 'en-US,en;q=0.5') + request.add_header('Accept-Encoding', 'gzip, deflate, br, zstd') + try: + with urllib.request.urlopen(request, timeout=osm_timeout) as response: + if response.status != http_code_ok: + return (f'tile.openstreetmap.org answer is not OK (status: {response.status}) : {response.read()}', + 501) + tile_path.write_bytes(response.read()) + return send_file(tile_path) + except urllib.error.URLError as error: + return f'Cannot retrieve tile: {error}', 501 + + app.run(host='0.0.0.0', port=8000, debug=debug_mode) + + +if __name__ == '__main__': + main()