Issue #69: preliminary support of tile collection.

This commit is contained in:
Sergey Vartanov 2021-08-05 04:02:05 +03:00
parent 1b37fa5d38
commit 088fc58870
3 changed files with 219 additions and 40 deletions

View file

@ -1,18 +1,18 @@
""" """
Getting OpenStreetMap data from the web. Getting OpenStreetMap data from the web.
""" """
import re
import time import time
import urllib import urllib
from pathlib import Path from pathlib import Path
from typing import Dict, Optional from typing import Dict, Optional
import logging
import urllib3 import urllib3
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
from roentgen.ui import BoundaryBox
def get_osm( def get_osm(
boundary_box: str, cache_path: Path, to_update: bool = False boundary_box: str, cache_path: Path, to_update: bool = False
@ -29,40 +29,13 @@ def get_osm(
if not to_update and result_file_name.is_file(): if not to_update and result_file_name.is_file():
return result_file_name.open().read() return result_file_name.open().read()
matcher = re.match(
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
+ "(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
boundary_box,
)
if not matcher:
logging.fatal("Invalid boundary box.")
return None
try:
left = float(matcher.group("left"))
bottom = float(matcher.group("bottom"))
right = float(matcher.group("right"))
top = float(matcher.group("top"))
except ValueError:
logging.fatal("Invalid boundary box.")
return None
if left >= right:
logging.fatal("Negative horizontal boundary.")
return None
if bottom >= top:
logging.error("Negative vertical boundary.")
return None
if right - left > 0.5 or top - bottom > 0.5:
logging.error("Boundary box is too big.")
return None
content = get_data( content = get_data(
"api.openstreetmap.org/api/0.6/map", "api.openstreetmap.org/api/0.6/map",
{"bbox": boundary_box}, {"bbox": boundary_box},
is_secure=True, is_secure=True,
) )
if BoundaryBox.from_text(boundary_box) is None:
return None
result_file_name.open("w+").write(content.decode("utf-8")) result_file_name.open("w+").write(content.decode("utf-8"))

View file

@ -19,8 +19,114 @@ from roentgen.icon import ShapeExtractor
from roentgen.mapper import Painter from roentgen.mapper import Painter
from roentgen.osm_getter import get_osm from roentgen.osm_getter import get_osm
from roentgen.osm_reader import Map, OSMReader from roentgen.osm_reader import Map, OSMReader
from roentgen.raster import rasterize
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
from roentgen.util import MinMax from roentgen.util import MinMax
from roentgen.ui import BoundaryBox
@dataclass
class Tiles:
"""
Collection of tiles.
"""
tiles: List["Tile"]
tile_1: "Tile"
tile_2: "Tile"
scale: int
boundary_box: BoundaryBox
@classmethod
def from_boundary_box(cls, boundary_box: BoundaryBox, scale: int):
"""Create minimal set of tiles that cover boundary box."""
tiles = []
tile_1 = Tile.from_coordinates(boundary_box.get_left_top(), scale)
tile_2 = Tile.from_coordinates(boundary_box.get_right_bottom(), scale)
print(boundary_box.left, boundary_box.right)
for x in range(tile_1.x, tile_2.x + 1):
for y in range(tile_1.y, tile_2.y + 1):
tiles.append(Tile(x, y, scale))
lat_2, lon_1 = tile_1.get_coordinates()
lat_1, lon_2 = Tile(tile_2.x + 1, tile_2.y + 1, scale).get_coordinates()
assert lon_2 > lon_1
assert lat_2 > lat_1
extended_boundary_box: BoundaryBox = BoundaryBox(
lon_1, lat_1, lon_2, lat_2
)
return cls(tiles, tile_1, tile_2, scale, extended_boundary_box)
def draw(self, directory: Path, cache_path: Path) -> None:
"""Draw set of tiles."""
content = get_osm(self.boundary_box.get_format(), cache_path)
if not content:
logging.error("Cannot download OSM data.")
return None
map_ = OSMReader().parse_osm_file(
cache_path / (self.boundary_box.get_format() + ".osm")
)
for tile in self.tiles:
file_path: Path = tile.get_file_name(directory)
if not file_path.exists():
tile.draw_for_map(map_, directory)
output_path: Path = file_path.with_suffix(".png")
if not output_path.exists():
rasterize(file_path, output_path)
def draw_image(self, cache_path: Path) -> None:
"""Draw all tiles as one picture."""
output_path: Path = cache_path / (
self.boundary_box.get_format() + ".svg"
)
if not output_path.exists():
content = get_osm(self.boundary_box.get_format(), cache_path)
if not content:
logging.error("Cannot download OSM data.")
return None
map_ = OSMReader().parse_osm_file(
cache_path / (self.boundary_box.get_format() + ".osm")
)
lat_2, lon_1 = self.tile_1.get_coordinates()
lat_1, lon_2 = Tile(
self.tile_2.x + 1, self.tile_2.y + 1, self.scale
).get_coordinates()
min_ = np.array((lat_1, lon_1))
max_ = np.array((lat_2, lon_2))
flinger: Flinger = Flinger(MinMax(min_, max_), self.scale)
icon_extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
map_, flinger, scheme, icon_extractor
)
constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size
)
painter: Painter = Painter(
map_=map_,
flinger=flinger,
svg=svg,
icon_extractor=icon_extractor,
scheme=scheme,
)
painter.draw(constructor)
with output_path.open("w+") as output_file:
svg.write(output_file)
png_path: Path = cache_path / (self.boundary_box.get_format() + ".png")
if not png_path.exists():
rasterize(output_path, png_path)
@dataclass @dataclass
@ -89,6 +195,8 @@ class Tile:
def load_map(self, cache_path: Path) -> Optional[Map]: def load_map(self, cache_path: Path) -> Optional[Map]:
""" """
Construct map data from extended boundary box. Construct map data from extended boundary box.
:param cache_path: directory to store SVG and PNG tiles
""" """
coordinates_1, coordinates_2 = self.get_extended_boundary_box() coordinates_1, coordinates_2 = self.get_extended_boundary_box()
lat1, lon1 = coordinates_1 lat1, lon1 = coordinates_1
@ -98,14 +206,14 @@ class Tile:
f"{min(lon1, lon2):.3f},{min(lat1, lat2):.3f}," f"{min(lon1, lon2):.3f},{min(lat1, lat2):.3f},"
f"{max(lon1, lon2):.3f},{max(lat1, lat2):.3f}" f"{max(lon1, lon2):.3f},{max(lat1, lat2):.3f}"
) )
content = get_osm(boundary_box, Path("cache")) content = get_osm(boundary_box, cache_path)
if not content: if not content:
logging.error("Cannot download OSM data.") logging.error("Cannot download OSM data.")
return None return None
return OSMReader().parse_osm_file(cache_path / (boundary_box + ".osm")) return OSMReader().parse_osm_file(cache_path / (boundary_box + ".osm"))
def get_map_name(self, directory_name: Path) -> Path: def get_file_name(self, directory_name: Path) -> Path:
""" """
Get tile output SVG file path. Get tile output SVG file path.
""" """
@ -124,13 +232,17 @@ class Tile:
Draw tile to SVG file. Draw tile to SVG file.
:param directory_name: output directory to storing tiles :param directory_name: output directory to storing tiles
:param cache_path: directory to store temporary files :param cache_path: directory to store SVG and PNG tiles
""" """
map_ = self.load_map(cache_path) map_ = self.load_map(cache_path)
if map_ is None: if map_ is None:
logging.fatal("No map to draw.") logging.fatal("No map to draw.")
return return
self.draw_for_map(map_, directory_name)
def draw_for_map(self, map_: Map, directory_name: Path) -> None:
"""Draw tile using existing map."""
lat1, lon1 = self.get_coordinates() lat1, lon1 = self.get_coordinates()
lat2, lon2 = Tile(self.x + 1, self.y + 1, self.scale).get_coordinates() lat2, lon2 = Tile(self.x + 1, self.y + 1, self.scale).get_coordinates()
@ -140,7 +252,7 @@ class Tile:
flinger: Flinger = Flinger(MinMax(min_, max_), self.scale) flinger: Flinger = Flinger(MinMax(min_, max_), self.scale)
size: np.array = flinger.size size: np.array = flinger.size
output_file_name: Path = self.get_map_name(directory_name) output_file_name: Path = self.get_file_name(directory_name)
svg: svgwrite.Drawing = svgwrite.Drawing( svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_file_name), size=size str(output_file_name), size=size
@ -174,17 +286,25 @@ def ui(options) -> None:
""" """
directory: Path = workspace.get_tile_path() directory: Path = workspace.get_tile_path()
tile: Tile
if options.coordinates: if options.coordinates:
coordinates: List[float] = list( coordinates: List[float] = list(
map(float, options.coordinates.strip().split(",")) map(float, options.coordinates.strip().split(","))
) )
tile = Tile.from_coordinates(np.array(coordinates), options.scale) tile: Tile = Tile.from_coordinates(np.array(coordinates), options.scale)
tile.draw(directory, Path(options.cache))
elif options.tile: elif options.tile:
scale, x, y = map(int, options.tile.split("/")) scale, x, y = map(int, options.tile.split("/"))
tile = Tile(x, y, scale) tile: Tile = Tile(x, y, scale)
tile.draw(directory, Path(options.cache))
elif options.boundary_box:
boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text(
options.boundary_box
)
if boundary_box is None:
sys.exit(1)
tiles: Tiles = Tiles.from_boundary_box(boundary_box, options.scale)
tiles.draw(directory, Path(options.cache))
tiles.draw_image(Path(options.cache))
else: else:
logging.fatal("Specify either --coordinates, or --tile.") logging.fatal("Specify either --coordinates, or --tile.")
sys.exit(1) sys.exit(1)
tile.draw(directory, Path(options.cache))

View file

@ -2,11 +2,17 @@
Command-line user interface. Command-line user interface.
""" """
import argparse import argparse
import re
import sys import sys
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
import logging
from dataclasses import dataclass
import numpy as np
BOXES: str = " ▏▎▍▌▋▊▉" BOXES: str = " ▏▎▍▌▋▊▉"
BOXES_LENGTH: int = len(BOXES) BOXES_LENGTH: int = len(BOXES)
@ -69,6 +75,13 @@ def add_tile_arguments(tile) -> None:
default="cache", default="cache",
metavar="<path>", metavar="<path>",
) )
tile.add_argument(
"-b",
"--boundary-box",
help="construct the minimum amount of tiles that cover requested "
"boundary box",
metavar="<lon1>,<lat1>,<lon2>,<lat2>",
)
def add_server_arguments(tile) -> None: def add_server_arguments(tile) -> None:
@ -183,3 +196,76 @@ def progress_bar(
f"{int(length - fill_length - 1) * ' '}{text}" f"{int(length - fill_length - 1) * ' '}{text}"
) )
sys.stdout.write("\033[F") sys.stdout.write("\033[F")
@dataclass
class BoundaryBox:
left: float
bottom: float
right: float
top: float
@classmethod
def from_text(cls, boundary_box: str):
"""
Parse boundary box string representation.
Note, that:
left < right
bottom < top
:param boundary_box: boundary box string representation in the form of
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2> or simply
<left>,<bottom>,<right>,<top>.
"""
matcher = re.match(
"(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*),"
+ "(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
boundary_box,
)
if not matcher:
logging.fatal("Invalid boundary box.")
return None
try:
left = float(matcher.group("left"))
bottom = float(matcher.group("bottom"))
right = float(matcher.group("right"))
top = float(matcher.group("top"))
except ValueError:
logging.fatal("Invalid boundary box.")
return None
if left >= right:
logging.fatal("Negative horizontal boundary.")
return None
if bottom >= top:
logging.error("Negative vertical boundary.")
return None
if right - left > 0.5 or top - bottom > 0.5:
logging.error("Boundary box is too big.")
return None
return cls(left, bottom, right, top)
def get_left_top(self) -> (np.array, np.array):
"""Get left top corner of the boundary box."""
return self.top, self.left
def get_right_bottom(self) -> (np.array, np.array):
"""Get right bottom corner of the boundary box."""
return self.bottom, self.right
def get_format(self) -> str:
"""
Get text representation of the boundary box:
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2>. Coordinates are
rounded to three digits after comma.
"""
a = (
f"{self.left - 0.001:.3f},{self.bottom - 0.001:.3f},"
f"{self.right + 0.001:.3f},{self.top + 0.001:.3f}"
)
print(self.left, a)
return a