Issue #69: fix tile scale.

This commit is contained in:
Sergey Vartanov 2021-08-22 14:00:45 +03:00
parent 9015cbe6b3
commit e3fa142860
3 changed files with 173 additions and 166 deletions

View file

@ -187,7 +187,7 @@ python roentgen.py tile \
--scale ${OSM_ZOOM_LEVEL}
```
Tile will be stored as SVG file `out/tiles/tile_<zoom level>_<x>_<y>.svg` and PNG file `out/tiles/tile_<zoom level>_<x>_<y>.svg`, where `x` and `y` are tile coordinates.
Tile will be stored as SVG file `out/tiles/tile_<zoom level>_<x>_<y>.svg` and PNG file `out/tiles/tile_<zoom level>_<x>_<y>.svg`, where `x` and `y` are tile coordinates. `--scale` option will be ignored if it is used with `--tile` option.
Example:
@ -203,10 +203,11 @@ Specify boundary box to get the minimal set of tiles that covers the area:
```bash
python roentgen.py tile \
--boundary-box ${LONGITUDE_1},${LATITUDE_1},${LONGITUDE_2},${LATITUDE_2}
--boundary-box ${LONGITUDE_1},${LATITUDE_1},${LONGITUDE_2},${LATITUDE_2} \
--scale ${OSM_ZOOM_LEVEL}
```
Boundary box will be extended to the boundaries of the minimal tile set that covers the area, then it will be extended a bit more to avoid some artifacts on the edges, and finally boundary box coordinates will be rounded to 3 digits after the decimal point. Map with new boundary box coordinates will be written to the cache directory as SVG and PNG files. All tiles will be stored as SVG files `out/tiles/tile_<zoom level>_<x>_<y>.svg` and PNG files `out/tiles/tile_<zoom level>_<x>_<y>.svg`, where `x` and `y` are tile coordinates.
Boundary box will be extended to the boundaries of the minimal tile set that covers the area, then it will be extended a bit more to avoid some artifacts on the edges rounded to 3 digits after the decimal point. Map with new boundary box coordinates will be written to the cache directory as SVG and PNG files. All tiles will be stored as SVG files `out/tiles/tile_<zoom level>_<x>_<y>.svg` and PNG files `out/tiles/tile_<zoom level>_<x>_<y>.svg`, where `x` and `y` are tile coordinates.
MapCSS 0.2 generation
---------------------

View file

@ -205,7 +205,7 @@ or specify any geographical coordinates inside a tile\:
--coordinates $\{LATITUDE\},$\{LONGITUDE\} \\
--scale $\{OSM_ZOOM_LEVEL\}} {bash}
Tile will be stored as SVG file \m {out/tiles/tile_<zoom level>_<x>_<y>.svg} and PNG file \m {out/tiles/tile_<zoom level>_<x>_<y>.svg}, where \m {x} and \m {y} are tile coordinates.
Tile will be stored as SVG file \m {out/tiles/tile_<zoom level>_<x>_<y>.svg} and PNG file \m {out/tiles/tile_<zoom level>_<x>_<y>.svg}, where \m {x} and \m {y} are tile coordinates. \m {--scale} option will be ignored if it is used with \m {--tile} option.
Example\:

View file

@ -31,163 +31,6 @@ __email__ = "me@enzet.ru"
TILE_WIDTH, TILE_HEIGHT = 256, 256
@dataclass
class Tiles:
"""
Collection of tiles.
"""
tiles: list["Tile"]
tile_1: "Tile" # Left top tile.
tile_2: "Tile" # Right bottom tile.
scale: int # OpenStreetMap zoom level.
boundary_box: BoundaryBox
@classmethod
def from_boundary_box(cls, boundary_box: BoundaryBox, scale: int):
"""
Create minimal set of tiles that cover boundary box.
:param boundary_box: area to be covered by tiles
:param scale: OpenStreetMap zoom level
"""
tiles: list["Tile"] = []
tile_1 = Tile.from_coordinates(boundary_box.get_left_top(), scale)
tile_2 = Tile.from_coordinates(boundary_box.get_right_bottom(), scale)
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))
latitude_2, longitude_1 = tile_1.get_coordinates()
latitude_1, longitude_2 = Tile(
tile_2.x + 1, tile_2.y + 1, scale
).get_coordinates()
assert longitude_2 > longitude_1
assert latitude_2 > latitude_1
extended_boundary_box: BoundaryBox = BoundaryBox(
longitude_1, latitude_1, longitude_2, latitude_2
).round()
return cls(tiles, tile_1, tile_2, scale, extended_boundary_box)
def draw_separately(self, directory: Path, cache_path: Path) -> None:
"""
Draw set of tiles as SVG file separately and rasterize them into a set
of PNG files with cairosvg.
:param directory: directory for tiles
:param cache_path: directory for temporary OSM files
"""
cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm"
)
get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
for tile in self.tiles:
file_path: Path = tile.get_file_name(directory)
if not file_path.exists():
tile.draw_with_osm_data(osm_data, directory)
else:
logging.debug(f"File {file_path} already exists.")
output_path: Path = file_path.with_suffix(".png")
if not output_path.exists():
with file_path.open() as input_file:
cairosvg.svg2png(
file_obj=input_file, write_to=str(output_path)
)
logging.info(f"SVG file is rasterized to {output_path}.")
else:
logging.debug(f"File {output_path} already exists.")
def draw(self, directory: Path, cache_path: Path) -> None:
"""
Draw one PNG image with all tiles and split it into a set of separate
PNG file with Pillow.
:param directory: directory for tiles
:param cache_path: directory for temporary OSM files
"""
input_path: Path = cache_path / (
self.boundary_box.get_format() + ".png"
)
self.draw_image(cache_path)
with input_path.open("rb") as input_file:
image = Image.open(input_file)
for tile in self.tiles:
x = tile.x - self.tile_1.x
y = tile.y - self.tile_1.y
area = (
x * TILE_WIDTH,
y * TILE_HEIGHT,
(x + 1) * TILE_WIDTH,
(y + 1) * TILE_HEIGHT,
)
cropped = image.crop(area)
cropped.crop((0, 0, TILE_WIDTH, TILE_HEIGHT)).save(
tile.get_file_name(directory).with_suffix(".png")
)
logging.info(f"Tile 18/{tile.x}/{tile.y} is created.")
def draw_image(self, cache_path: Path) -> None:
"""
Draw all tiles as one picture.
:param cache_path: directory for temporary SVG file and OSM files
"""
output_path: Path = cache_path / (
self.boundary_box.get_format() + ".svg"
)
if not output_path.exists():
cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm"
)
get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
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)
extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
osm_data, flinger, scheme, extractor
)
constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size
)
map_: Map = Map(flinger=flinger, svg=svg, scheme=scheme)
map_.draw(constructor)
logging.info(f"Writing output SVG {output_path}...")
with output_path.open("w+") as output_file:
svg.write(output_file)
else:
logging.debug(f"File {output_path} already exists.")
png_path: Path = cache_path / f"{self.boundary_box.get_format()}.png"
if not png_path.exists():
with output_path.open() as input_file:
cairosvg.svg2png(file_obj=input_file, write_to=str(png_path))
logging.info(f"SVG file is rasterized to {png_path}.")
else:
logging.debug(f"File {png_path} already exists.")
@dataclass
class Tile:
"""
@ -287,12 +130,17 @@ class Tile:
self, osm_data: OSMData, directory_name: Path
) -> None:
"""Draw SVG and PNG tile using OpenStreetMap data."""
lat1, lon1 = self.get_coordinates()
lat2, lon2 = Tile(self.x + 1, self.y + 1, self.scale).get_coordinates()
min_: np.array = np.array((min(lat1, lat2), min(lon1, lon2)))
max_: np.array = np.array((max(lat1, lat2), max(lon1, lon2)))
latitude_1, longitude_1 = self.get_coordinates()
latitude_2, longitude_2 = Tile(
self.x + 1, self.y + 1, self.scale
).get_coordinates()
min_: np.array = np.array(
(min(latitude_1, latitude_2), min(longitude_1, longitude_2))
)
max_: np.array = np.array(
(max(latitude_1, latitude_2), max(longitude_1, longitude_2))
)
flinger: Flinger = Flinger(MinMax(min_, max_), self.scale)
size: np.array = flinger.size
@ -323,6 +171,164 @@ class Tile:
logging.info(f"SVG file is rasterized to {output_path}.")
@dataclass
class Tiles:
"""
Collection of tiles.
"""
tiles: list[Tile]
tile_1: Tile # Left top tile.
tile_2: Tile # Right bottom tile.
scale: int # OpenStreetMap zoom level.
boundary_box: BoundaryBox
@classmethod
def from_boundary_box(cls, boundary_box: BoundaryBox, scale: int):
"""
Create minimal set of tiles that cover boundary box.
:param boundary_box: area to be covered by tiles
:param scale: OpenStreetMap zoom level
"""
tiles: list[Tile] = []
tile_1: Tile = Tile.from_coordinates(boundary_box.get_left_top(), scale)
tile_2: Tile = Tile.from_coordinates(
boundary_box.get_right_bottom(), scale
)
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))
latitude_2, longitude_1 = tile_1.get_coordinates()
latitude_1, longitude_2 = Tile(
tile_2.x + 1, tile_2.y + 1, scale
).get_coordinates()
assert longitude_2 > longitude_1
assert latitude_2 > latitude_1
extended_boundary_box: BoundaryBox = BoundaryBox(
longitude_1, latitude_1, longitude_2, latitude_2
).round()
return cls(tiles, tile_1, tile_2, scale, extended_boundary_box)
def draw_separately(self, directory: Path, cache_path: Path) -> None:
"""
Draw set of tiles as SVG file separately and rasterize them into a set
of PNG files with cairosvg.
:param directory: directory for tiles
:param cache_path: directory for temporary OSM files
"""
cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm"
)
get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
for tile in self.tiles:
file_path: Path = tile.get_file_name(directory)
if not file_path.exists():
tile.draw_with_osm_data(osm_data, directory)
else:
logging.debug(f"File {file_path} already exists.")
output_path: Path = file_path.with_suffix(".png")
if not output_path.exists():
with file_path.open() as input_file:
cairosvg.svg2png(
file_obj=input_file, write_to=str(output_path)
)
logging.info(f"SVG file is rasterized to {output_path}.")
else:
logging.debug(f"File {output_path} already exists.")
def draw(self, directory: Path, cache_path: Path) -> None:
"""
Draw one PNG image with all tiles and split it into a set of separate
PNG file with Pillow.
:param directory: directory for tiles
:param cache_path: directory for temporary OSM files
"""
input_path: Path = cache_path / (
self.boundary_box.get_format() + ".png"
)
self.draw_image(cache_path)
with input_path.open("rb") as input_file:
image: Image = Image.open(input_file)
for tile in self.tiles:
x: int = tile.x - self.tile_1.x
y: int = tile.y - self.tile_1.y
area: tuple[int, int, int, int] = (
x * TILE_WIDTH,
y * TILE_HEIGHT,
(x + 1) * TILE_WIDTH,
(y + 1) * TILE_HEIGHT,
)
cropped: Image = image.crop(area)
cropped.crop((0, 0, TILE_WIDTH, TILE_HEIGHT)).save(
tile.get_file_name(directory).with_suffix(".png")
)
logging.info(f"Tile {tile.scale}/{tile.x}/{tile.y} is created.")
def draw_image(self, cache_path: Path) -> None:
"""
Draw all tiles as one picture.
:param cache_path: directory for temporary SVG file and OSM files
"""
output_path: Path = cache_path / (
self.boundary_box.get_format() + ".svg"
)
if not output_path.exists():
cache_file_path: Path = (
cache_path / f"{self.boundary_box.get_format()}.osm"
)
get_osm(self.boundary_box, cache_file_path)
osm_data: OSMData = OSMReader().parse_osm_file(cache_file_path)
latitude_2, longitude_1 = self.tile_1.get_coordinates()
latitude_1, longitude_2 = Tile(
self.tile_2.x + 1, self.tile_2.y + 1, self.scale
).get_coordinates()
min_ = np.array((latitude_1, longitude_1))
max_ = np.array((latitude_2, longitude_2))
flinger: Flinger = Flinger(MinMax(min_, max_), self.scale)
extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor(
osm_data, flinger, scheme, extractor
)
constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size
)
map_: Map = Map(flinger=flinger, svg=svg, scheme=scheme)
map_.draw(constructor)
logging.info(f"Writing output SVG {output_path}...")
with output_path.open("w+") as output_file:
svg.write(output_file)
else:
logging.debug(f"File {output_path} already exists.")
png_path: Path = cache_path / f"{self.boundary_box.get_format()}.png"
if not png_path.exists():
with output_path.open() as input_file:
cairosvg.svg2png(file_obj=input_file, write_to=str(png_path))
logging.info(f"SVG file is rasterized to {png_path}.")
else:
logging.debug(f"File {png_path} already exists.")
def ui(options) -> None:
"""Simple user interface for tile generation."""
directory: Path = workspace.get_tile_path()