commit e7d99cc1bb980b2a019c9680ab146fa1c4c3e8ec Author: Corentin Date: Fri Oct 17 04:02:04 2025 +0900 Initial commit 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 0000000..950edf2 Binary files /dev/null and b/public/images/marker-icon.png differ diff --git a/public/images/marker-shadow.png b/public/images/marker-shadow.png new file mode 100644 index 0000000..9fd2979 Binary files /dev/null and b/public/images/marker-shadow.png differ 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()