Initial commit
This commit is contained in:
commit
e7d99cc1bb
14 changed files with 352 additions and 0 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
public/leaflet* filter=lfs diff=lfs merge=lfs -text
|
||||||
|
public/marker.json filter=lfs diff=lfs merge=lfs -text
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
data
|
||||||
87
consolidate_tiles.py
Normal file
87
consolidate_tiles.py
Normal file
|
|
@ -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()
|
||||||
1
public/images/location_city.svg
Normal file
1
public/images/location_city.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#8383b3"><path d="M82-186.67V-612q0-44.48 30.23-74.9 30.24-30.43 74.44-30.43h140V-738q0-19.96 7.33-38.48 7.33-18.52 22.33-32.19l50-49.33q30.77-30.33 73.55-30.33 42.79 0 73.79 30.33l48.66 47.33q15.34 15.34 23.5 34.68 8.17 19.35 8.17 40.32v182.34h139.33q44.48 0 74.91 30.43t30.43 74.9v261.33q0 44.2-30.43 74.44Q817.81-82 773.33-82H186.67q-44.2 0-74.44-30.23Q82-142.47 82-186.67Zm104.67 0H284V-284h-97.33v97.33Zm0-164H284V-448h-97.33v97.33Zm0-164H284V-612h-97.33v97.33Zm244.66 328h97.34V-284h-97.34v97.33Zm0-164h97.34V-448h-97.34v97.33Zm0-164h97.34V-612h-97.34v97.33Zm0-164h97.34V-776h-97.34v97.33Zm244.67 492h97.33V-284H676v97.33Zm0-164h97.33V-448H676v97.33Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 763 B |
BIN
public/images/marker-icon.png
Normal file
BIN
public/images/marker-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
public/images/marker-shadow.png
Normal file
BIN
public/images/marker-shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
1
public/images/nutrition.svg
Normal file
1
public/images/nutrition.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e89030"><path d="M480-120q-117 0-198.5-81.5T200-400q0-98 59.5-173.83 59.5-75.84 154.17-98.5Q380-679 354.5-698.83 329-718.67 312-747q-17-28.33-25-62.67-8-34.33-6.67-70 42.34-3 80 10.67 37.67 13.67 67 39 29.34 25.33 46.84 60.33 17.5 35 18.16 76.67 13.67-39 36.84-72.83 23.16-33.84 52.16-62.84 9.67-9.66 23.34-9.66 13.66 0 23.33 9.66 9.67 9.67 9.67 23.34 0 13.66-9.67 23.33-24 24-43 52.17-19 28.16-31 59.5Q646-645 703-570.17q57 74.84 57 170.17 0 117-81.5 198.5T480-120Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 574 B |
58
public/index.html
Normal file
58
public/index.html
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>神山町</title>
|
||||||
|
<link rel="stylesheet" href="/leaflet_1.9.4.css"/>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
background: #333;
|
||||||
|
color: #eee;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
#map {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon {
|
||||||
|
background-color: rgba(255, 255, 255, .8);;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.menu {
|
||||||
|
padding: 8px;
|
||||||
|
background-color: rgba(255, 255, 255, .5);
|
||||||
|
color: #222;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.menu-category {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.menu-category:hover {
|
||||||
|
background-color: rgba(255, 255, 255, .8);;
|
||||||
|
}
|
||||||
|
.menu-category.menu-disable {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
.menu-category img {
|
||||||
|
height: 1.2rem;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<script src="/leaflet_1.9.4.js"></script>
|
||||||
|
<script src="/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
81
public/index.js
Normal file
81
public/index.js
Normal file
|
|
@ -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: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||||
|
}).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(`<h3>${marker_info.name}</h3>${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);
|
||||||
|
});
|
||||||
3
public/leaflet_1.9.4.css
Normal file
3
public/leaflet_1.9.4.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:79cab275563428aac0ad39ee4c4271610d0b40133390bffd96331224aebbc689
|
||||||
|
size 14852
|
||||||
3
public/leaflet_1.9.4.js
Normal file
3
public/leaflet_1.9.4.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:db49d009c841f5ca34a888c96511ae936fd9f5533e90d8b2c4d57596f4e5641a
|
||||||
|
size 147552
|
||||||
3
public/marker.json
Normal file
3
public/marker.json
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d756d5b9b1fe557cd7933f60aca6e0fc46b866aa032c1933291050194b5c3b9d
|
||||||
|
size 748
|
||||||
61
pyproject.toml
Normal file
61
pyproject.toml
Normal file
|
|
@ -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/*"]
|
||||||
51
server.py
Normal file
51
server.py
Normal file
|
|
@ -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('/<int:zoom>/<int:x>/<int:y>.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()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue