Fix code style.

This commit is contained in:
Sergey Vartanov 2020-09-24 23:55:04 +03:00
parent c9fe5e55b6
commit 2667ee9d02
16 changed files with 228 additions and 240 deletions

View file

@ -6,8 +6,13 @@ Author: Sergey Vartanov (me@enzet.ru).
from typing import List, Any, Dict from typing import List, Any, Dict
def get_address(tags: Dict[str, Any], draw_captions_mode: str): def get_address(tags: Dict[str, Any], draw_captions_mode: str) -> List[str]:
"""
Construct address text list from the tags.
:param tags: OSM node, way or relation tags
:param draw_captions_mode: captions mode ("all", "main", or "no")
"""
address: List[str] = [] address: List[str] = []
if draw_captions_mode != "main": if draw_captions_mode != "main":
@ -30,3 +35,5 @@ def get_address(tags: Dict[str, Any], draw_captions_mode: str):
if "addr:housenumber" in tags: if "addr:housenumber" in tags:
address.append(tags["addr:housenumber"]) address.append(tags["addr:housenumber"])
tags.pop("addr:housenumber", None) tags.pop("addr:housenumber", None)
return address

View file

@ -1,3 +1,8 @@
"""
Röntgen project. Color utility.
Author: Sergey Vartanov (me@enzet.ru)
"""
from typing import Any, List from typing import Any, List
from colour import Color from colour import Color
@ -10,9 +15,8 @@ def is_bright(color: Color) -> bool:
Is color bright enough to have black outline instead of white. Is color bright enough to have black outline instead of white.
""" """
return ( return (
0.2126 * color.red * 256 + 0.2126 * color.red + 0.7152 * color.green + 0.0722 * color.blue
0.7152 * color.green * 256 + > 0.78125)
0.0722 * color.blue * 256 > 200)
def get_gradient_color(value: Any, bounds: MinMax, colors: List[Color]): def get_gradient_color(value: Any, bounds: MinMax, colors: List[Color]):
@ -30,9 +34,9 @@ def get_gradient_color(value: Any, bounds: MinMax, colors: List[Color]):
0 if bounds.max_ == bounds.min_ else 0 if bounds.max_ == bounds.min_ else
(value - bounds.min_) / (bounds.max_ - bounds.min_)) (value - bounds.min_) / (bounds.max_ - bounds.min_))
coefficient = min(1.0, max(0.0, coefficient)) coefficient = min(1.0, max(0.0, coefficient))
m: int = int(coefficient * color_length) index: int = int(coefficient * color_length)
color_coefficient = (coefficient - m / color_length) * color_length color_coefficient = (coefficient - index / color_length) * color_length
return Color(rgb=[ return Color(rgb=[
scale[m].rgb[i] + color_coefficient * scale[index].rgb[i] + color_coefficient *
(scale[m + 1].rgb[i] - scale[m].rgb[i]) for i in range(3)]) (scale[index + 1].rgb[i] - scale[index].rgb[i]) for i in range(3)])

View file

@ -3,21 +3,22 @@ Construct Röntgen nodes and ways.
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
import numpy as np from dataclasses import dataclass
from colour import Color
from datetime import datetime from datetime import datetime
from hashlib import sha256 from hashlib import sha256
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set
from colour import Color
import numpy as np
from roentgen import ui from roentgen import ui
from roentgen.color import get_gradient_color
from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.osm_reader import Map, OSMMember, OSMRelation, OSMWay, OSMNode, \ from roentgen.osm_reader import (
Tagged Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged)
from roentgen.scheme import IconSet, Scheme from roentgen.scheme import IconSet, Scheme
from roentgen.util import MinMax from roentgen.util import MinMax
from roentgen.color import get_gradient_color
DEBUG: bool = False DEBUG: bool = False
TIME_COLOR_SCALE: List[Color] = [ TIME_COLOR_SCALE: List[Color] = [
@ -27,31 +28,35 @@ TIME_COLOR_SCALE: List[Color] = [
def is_clockwise(polygon: List[OSMNode]) -> bool: def is_clockwise(polygon: List[OSMNode]) -> bool:
""" """
Are polygon nodes are in clockwise order. Return true if polygon nodes are in clockwise order.
:param polygon: list of OpenStreetMap nodes
""" """
count: float = 0 count: float = 0
for index in range(len(polygon)): # type: int for index, node in enumerate(polygon): # type: int, OSMNode
next_index: int = 0 if index == len(polygon) - 1 else index + 1 next_index: int = 0 if index == len(polygon) - 1 else index + 1
count += ( count += (
(polygon[next_index].coordinates[0] - (polygon[next_index].coordinates[0] - node.coordinates[0]) *
polygon[index].coordinates[0]) * (polygon[next_index].coordinates[1] + node.coordinates[1]))
(polygon[next_index].coordinates[1] +
polygon[index].coordinates[1]))
return count >= 0 return count >= 0
def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
if is_clockwise(polygon): """
return polygon Make polygon nodes clockwise.
else:
return list(reversed(polygon)) :param polygon: list of OpenStreetMap nodes
"""
return polygon if is_clockwise(polygon) else list(reversed(polygon))
def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
if not is_clockwise(polygon): """
return polygon Make polygon nodes counter-clockwise.
else:
return list(reversed(polygon)) :param polygon: list of OpenStreetMap nodes
"""
return polygon if not is_clockwise(polygon) else list(reversed(polygon))
class Point(Tagged): class Point(Tagged):
@ -74,11 +79,6 @@ class Point(Tagged):
self.layer: float = 0 self.layer: float = 0
self.is_for_node: bool = is_for_node self.is_for_node: bool = is_for_node
def get_tag(self, key: str):
if key in self.tags:
return self.tags[key]
return None
class Figure(Tagged): class Figure(Tagged):
""" """
@ -107,6 +107,7 @@ class Figure(Tagged):
""" """
Get SVG path commands. Get SVG path commands.
:param flinger: convertor for geo coordinates
:param shift: shift vector :param shift: shift vector
""" """
path: str = "" path: str = ""
@ -121,6 +122,9 @@ class Figure(Tagged):
class Segment: class Segment:
"""
Line segment.
"""
def __init__(self, point_1: np.array, point_2: np.array): def __init__(self, point_1: np.array, point_2: np.array):
self.point_1 = point_1 self.point_1 = point_1
self.point_2 = point_2 self.point_2 = point_2
@ -142,6 +146,7 @@ class Building(Figure):
def __init__( def __init__(
self, tags: Dict[str, str], inners, outers, flinger: Flinger, self, tags: Dict[str, str], inners, outers, flinger: Flinger,
style: Dict[str, Any], layer: float): style: Dict[str, Any], layer: float):
super().__init__(tags, inners, outers, style, layer) super().__init__(tags, inners, outers, style, layer)
self.parts = [] self.parts = []
@ -154,23 +159,24 @@ class Building(Figure):
self.parts = sorted(self.parts) self.parts = sorted(self.parts)
def get_levels(self) -> float:
def get_levels(self): """
Get building level number.
"""
try: try:
return max(3, float(self.get_tag("building:levels"))) return max(3.0, float(self.get_tag("building:levels")))
except (ValueError, TypeError): except (ValueError, TypeError):
return 3 return 3
@dataclass
class TextStruct: class TextStruct:
""" """
Some label on the map with attributes. Some label on the map with attributes.
""" """
def __init__( text: str
self, text: str, fill: Color = Color("#444444"), size: float = 10): fill: Color = Color("#444444")
self.text = text size: float = 10
self.fill = fill
self.size = size
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array: def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
@ -202,6 +208,9 @@ def get_user_color(text: str, seed: str) -> Color:
def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color: def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color:
""" """
Generate color based on time. Generate color based on time.
:param time: current element creation time
:param boundaries: minimum and maximum element creation time on the map
""" """
return get_gradient_color(time, boundaries, TIME_COLOR_SCALE) return get_gradient_color(time, boundaries, TIME_COLOR_SCALE)
@ -244,11 +253,11 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str: def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
""" """
Construct SVG path from nodes. Construct SVG path commands from nodes.
""" """
path = "" path: str = ""
prev_node = None prev_node: Optional[OSMNode] = None
for node in nodes: for node in nodes: # type: OSMNode
flung = flinger.fling(node.coordinates) + shift flung = flinger.fling(node.coordinates) + shift
path += ("L" if prev_node else "M") + f" {flung[0]},{flung[1]} " path += ("L" if prev_node else "M") + f" {flung[0]},{flung[1]} "
prev_node = node prev_node = node
@ -280,11 +289,14 @@ class Constructor:
self.levels: Set[float] = {0.5, 1} self.levels: Set[float] = {0.5, 1}
def add_building(self, building: Building): def add_building(self, building: Building) -> None:
"""
Add building and update levels.
"""
self.buildings.append(building) self.buildings.append(building)
self.levels.add(building.get_levels()) self.levels.add(building.get_levels())
def construct_ways(self): def construct_ways(self) -> None:
""" """
Construct Röntgen ways. Construct Röntgen ways.
""" """
@ -303,33 +315,22 @@ class Constructor:
def construct_way( def construct_way(
self, way: Optional[OSMWay], tags: Dict[str, Any], self, way: Optional[OSMWay], tags: Dict[str, Any],
inners, outers) -> None: inners: List[List[OSMNode]], outers: List[List[OSMNode]]) -> None:
""" """
Way construction. Way construction.
:param way: OSM way :param way: OSM way
:param tags: way tag dictionary :param tags: way tag dictionary
:param inners: list of polygons that compose inner boundary
:param outers: list of polygons that compose outer boundary
""" """
layer: float = 0 layer: float = 0
# level: float = 0
#
# if "layer" in tags:
# layer = get_float(tags["layer"])
# if "level" in tags:
# try:
# levels = list(map(float, tags["level"].split(";")))
# level = sum(levels) / len(levels)
# except ValueError:
# pass
# layer = 100 * level + 0.01 * layer
center_point, center_coordinates = None, None center_point, center_coordinates = None, None
if way: if way is not None:
center_point, center_coordinates = \ center_point, center_coordinates = (
line_center(way.nodes, self.flinger) line_center(way.nodes, self.flinger))
nodes = way.nodes
if self.mode == "user-coloring": if self.mode == "user-coloring":
if not way: if not way:
@ -417,8 +418,8 @@ class Constructor:
"stroke-width": 1} "stroke-width": 1}
self.figures.append(Figure( self.figures.append(Figure(
tags, inners, outers, style, layer)) tags, inners, outers, style, layer))
if center_point is not None and (way.is_cycle() or if (center_point is not None and
"area" in tags and tags["area"]): way.is_cycle() and "area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags) icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Point( self.nodes.append(Point(
icon_set, tags, center_point, center_coordinates, icon_set, tags, center_point, center_coordinates,
@ -480,7 +481,7 @@ class Constructor:
if self.mode == "user-coloring": if self.mode == "user-coloring":
icon_set.color = get_user_color(node.user, self.seed) icon_set.color = get_user_color(node.user, self.seed)
if self.mode == "time": if self.mode == "time":
icon_set.color = get_time_color(node.timestamp) icon_set.color = get_time_color(node.timestamp, self.map_.time)
self.nodes.append(Point(icon_set, tags, flung, node.coordinates)) self.nodes.append(Point(icon_set, tags, flung, node.coordinates))

View file

@ -43,7 +43,7 @@ def parse_vector(text: str) -> Optional[np.array]:
return None return None
def rotation_matrix(angle): def rotation_matrix(angle) -> np.array:
""" """
Get a matrix to rotate 2D vector by the angle. Get a matrix to rotate 2D vector by the angle.
@ -100,7 +100,7 @@ class Sector:
return ["L", start, "A", radius, radius, 0, "0", 0, end] return ["L", start, "A", radius, radius, 0, "0", 0, end]
def __str__(self): def __str__(self) -> str:
return f"{self.start}-{self.end}" return f"{self.start}-{self.end}"
@ -115,7 +115,7 @@ class DirectionSet:
""" """
self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";")) self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";"))
def __str__(self): def __str__(self) -> str:
return ", ".join(map(str, self.sectors)) return ", ".join(map(str, self.sectors))
def draw(self, center: np.array, radius: float) -> Iterator[List[Path]]: def draw(self, center: np.array, radius: float) -> Iterator[List[Path]]:

View file

@ -6,6 +6,7 @@ Author: Sergey Vartanov (me@enzet.ru).
import re import re
import xml.dom.minidom import xml.dom.minidom
from typing import Dict from typing import Dict
from xml.dom.minidom import Element, Node
import numpy as np import numpy as np
from svgwrite import Drawing from svgwrite import Drawing
@ -14,6 +15,7 @@ from roentgen import ui
DEFAULT_SHAPE_ID: str = "default" DEFAULT_SHAPE_ID: str = "default"
DEFAULT_SMALL_SHAPE_ID: str = "default_small" DEFAULT_SMALL_SHAPE_ID: str = "default_small"
STANDARD_INKSCAPE_ID: str = "(path|rect)\\d*"
GRID_STEP: int = 16 GRID_STEP: int = 16
@ -71,25 +73,26 @@ class IconExtractor:
content = xml.dom.minidom.parse(input_file) content = xml.dom.minidom.parse(input_file)
for element in content.childNodes: for element in content.childNodes:
if element.nodeName == "svg": if element.nodeName == "svg":
for node in element.childNodes: for node in element.childNodes: # type: Node
if node.nodeName in ["g", "path"]: if isinstance(node, Element):
self.parse(node) self.parse(node)
def parse(self, node) -> None: def parse(self, node: Element) -> None:
""" """
Extract icon paths into a map. Extract icon paths into a map.
:param node: XML node that contains icon :param node: XML node that contains icon
""" """
if node.nodeName != "path": if node.nodeName == "g":
for sub_node in node.childNodes: for sub_node in node.childNodes:
if isinstance(sub_node, Element):
self.parse(sub_node) self.parse(sub_node)
return return
if "id" in node.attributes.keys() and \ if ("id" in node.attributes.keys() and
"d" in node.attributes.keys() and \ "d" in node.attributes.keys() and
node.attributes["id"].value: node.attributes["id"].value):
path = node.attributes["d"].value path: str = node.attributes["d"].value
matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path) matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path)
if not matcher: if not matcher:
ui.error(f"invalid path: {path}") ui.error(f"invalid path: {path}")
@ -104,6 +107,8 @@ class IconExtractor:
get_offset(float(matcher.group(2))))) get_offset(float(matcher.group(2)))))
id_: str = node.attributes["id"].value id_: str = node.attributes["id"].value
matcher = re.match(STANDARD_INKSCAPE_ID, id_)
if not matcher:
self.icons[id_] = Icon(node.attributes["d"].value, point, id_) self.icons[id_] = Icon(node.attributes["d"].value, point, id_)
def get_path(self, id_: str) -> (Icon, bool): def get_path(self, id_: str) -> (Icon, bool):

View file

@ -37,7 +37,7 @@ class Flinger:
""" """
Convert geo coordinates into SVG position points. Convert geo coordinates into SVG position points.
""" """
def __init__(self, geo_boundaries: MinMax, scale: float = 1000): def __init__(self, geo_boundaries: MinMax, scale: float = 18):
""" """
:param geo_boundaries: minimum and maximum latitude and longitude :param geo_boundaries: minimum and maximum latitude and longitude
:param scale: OSM zoom level :param scale: OSM zoom level

View file

@ -5,11 +5,10 @@ Author: Sergey Vartanov (me@enzet.ru).
""" """
import numpy as np import numpy as np
from svgwrite import Drawing from svgwrite import Drawing
import yaml from typing import List, Dict, Any, Set
from roentgen.extract_icon import Icon, IconExtractor from roentgen.extract_icon import Icon, IconExtractor
from roentgen.scheme import Scheme
from typing import List
def draw_grid(step: float = 24, columns: int = 16): def draw_grid(step: float = 24, columns: int = 16):
@ -19,19 +18,19 @@ def draw_grid(step: float = 24, columns: int = 16):
:param step: horizontal and vertical distance between icons :param step: horizontal and vertical distance between icons
:param columns: the number of columns in grid :param columns: the number of columns in grid
""" """
tags_file_name = "data/tags.yml" tags_file_name: str = "data/tags.yml"
scheme = yaml.load(open(tags_file_name), Loader=yaml.FullLoader) scheme: Scheme = Scheme(tags_file_name)
icons_file_name = "icons/icons.svg" icons_file_name: str = "icons/icons.svg"
icon_grid_file_name = "icon_grid.svg" icon_grid_file_name: str = "icon_grid.svg"
width: float = step * columns width: float = step * columns
point: np.array = np.array((step / 2, step / 2)) point: np.array = np.array((step / 2, step / 2))
to_draw = [] to_draw: List[Set[str]] = []
for element in scheme["nodes"]: for element in scheme.nodes: # type: Dict[str, Any]
if "icon" in element: if "icon" in element:
if set(element["icon"]) not in to_draw: if set(element["icon"]) not in to_draw:
to_draw.append(set(element["icon"])) to_draw.append(set(element["icon"]))
@ -41,23 +40,26 @@ def draw_grid(step: float = 24, columns: int = 16):
if "over_icon" not in element: if "over_icon" not in element:
continue continue
if "under_icon" in element: if "under_icon" in element:
for icon in element["under_icon"]: for icon_id in element["under_icon"]: # type: str
current_set = set([icon] + element["over_icon"]) current_set = set([icon_id] + element["over_icon"])
if current_set not in to_draw: if current_set not in to_draw:
to_draw.append(current_set) to_draw.append(current_set)
if not ("under_icon" in element and "with_icon" in element): if not ("under_icon" in element and "with_icon" in element):
continue continue
for icon in element["under_icon"]: for icon_id in element["under_icon"]: # type: str
for icon2 in element["with_icon"]: for icon_2_id in element["with_icon"]: # type: str
current_set = set([icon] + [icon2] + element["over_icon"]) current_set: Set[str] = set(
[icon_id] + [icon_2_id] + element["over_icon"])
if current_set not in to_draw: if current_set not in to_draw:
to_draw.append(current_set) to_draw.append(current_set)
for icon2 in element["with_icon"]: for icon_2_id in element["with_icon"]: # type: str
for icon3 in element["with_icon"]: for icon_3_id in element["with_icon"]: # type: str
current_set = \ current_set = set(
set([icon] + [icon2] + [icon3] + element["over_icon"]) [icon_id] + [icon_2_id] + [icon_3_id] +
if icon2 != icon3 and icon2 != icon and icon3 != icon and \ element["over_icon"])
(current_set not in to_draw): if (icon_2_id != icon_3_id and icon_2_id != icon_id and
icon_3_id != icon_id and
(current_set not in to_draw)):
to_draw.append(current_set) to_draw.append(current_set)
number: int = 0 number: int = 0
@ -70,8 +72,8 @@ def draw_grid(step: float = 24, columns: int = 16):
found: bool = False found: bool = False
icon_set: List[Icon] = [] icon_set: List[Icon] = []
for icon_id in icons_to_draw: # type: str for icon_id in icons_to_draw: # type: str
icon, got = extractor.get_path(icon_id) icon, extracted = extractor.get_path(icon_id) # type: Icon, bool
assert got assert extracted, f"no icon with ID {icon_id}"
icon_set.append(icon) icon_set.append(icon)
found = True found = True
if found: if found:
@ -84,13 +86,13 @@ def draw_grid(step: float = 24, columns: int = 16):
svg.add(svg.rect((0, 0), (width, height), fill="#FFFFFF")) svg.add(svg.rect((0, 0), (width, height), fill="#FFFFFF"))
for icon in icons: for combined_icon in icons: # type: List[Icon]
background_color, foreground_color = "#FFFFFF", "#444444" background_color, foreground_color = "#FFFFFF", "#444444"
svg.add(svg.rect( svg.add(svg.rect(
point - np.array((-10, -10)), (20, 20), point - np.array((-10, -10)), (20, 20),
fill=background_color)) fill=background_color))
for i in icon: # type: Icon for icon in combined_icon: # type: Icon
path = i.get_path(svg, point) path = icon.get_path(svg, point)
path.update({"fill": foreground_color}) path.update({"fill": foreground_color})
svg.add(path) svg.add(path)
point += np.array((step, 0)) point += np.array((step, 0))

View file

@ -3,6 +3,8 @@ Simple OpenStreetMap renderer.
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
import argparse
import numpy as np import numpy as np
import os import os
import svgwrite import svgwrite
@ -17,7 +19,8 @@ from typing import Any, Dict, List, Optional
from roentgen import ui from roentgen import ui
from roentgen.address import get_address from roentgen.address import get_address
from roentgen.constructor import Constructor, Point, Figure, TextStruct, Building from roentgen.constructor import (
Constructor, Point, Figure, TextStruct, Building, Segment)
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.grid import draw_grid from roentgen.grid import draw_grid
from roentgen.extract_icon import Icon, IconExtractor from roentgen.extract_icon import Icon, IconExtractor
@ -150,7 +153,7 @@ class Painter:
text, point, font_size=size, text_anchor="middle", text, point, font_size=size, text_anchor="middle",
font_family=DEFAULT_FONT, fill=fill.hex)) font_family=DEFAULT_FONT, fill=fill.hex))
def construct_text(self, tags, processed): def construct_text(self, tags, processed) -> List[TextStruct]:
""" """
Construct labels for not processed tags. Construct labels for not processed tags.
""" """
@ -185,7 +188,7 @@ class Painter:
alt_name = "" alt_name = ""
alt_name += "бывш. " + tags["old_name"] alt_name += "бывш. " + tags["old_name"]
address = get_address(tags, self.draw_captions) address: List[str] = get_address(tags, self.draw_captions)
if name: if name:
texts.append(TextStruct(name, Color("black"))) texts.append(TextStruct(name, Color("black")))
@ -233,11 +236,11 @@ class Painter:
ways_length: int = len(ways) ways_length: int = len(ways)
for index, way in enumerate(ways): # type: Figure for index, way in enumerate(ways): # type: Figure
ui.progress_bar(index, ways_length, step=10, text="Drawing ways") ui.progress_bar(index, ways_length, step=10, text="Drawing ways")
path: str = way.get_path(self.flinger) path_commands: str = way.get_path(self.flinger)
if path: if path_commands:
p = Path(d=path) path = Path(d=path_commands)
p.update(way.style) path.update(way.style)
self.svg.add(p) self.svg.add(path)
ui.progress_bar(-1, 0, text="Drawing ways") ui.progress_bar(-1, 0, text="Drawing ways")
# Draw building shade. # Draw building shade.
@ -262,15 +265,18 @@ class Painter:
previous_level: float = 0 previous_level: float = 0
height: float = self.flinger.get_scale() height: float = self.flinger.get_scale()
level_count: int = len(constructor.levels)
for level in sorted(constructor.levels): for index, level in enumerate(sorted(constructor.levels)):
ui.progress_bar(
index, level_count, step=1, text="Drawing buildings")
fill: Color() fill: Color()
for way in constructor.buildings: # type: Building for way in constructor.buildings: # type: Building
if way.get_levels() < level: if way.get_levels() < level:
continue continue
shift_1 = [0, -previous_level * height] shift_1 = [0, -previous_level * height]
shift_2 = [0, -level * height] shift_2 = [0, -level * height]
for segment in way.parts: for segment in way.parts: # type: Segment
if level == 0.5: if level == 0.5:
fill = Color("#AAAAAA") fill = Color("#AAAAAA")
elif level == 1: elif level == 1:
@ -280,26 +286,29 @@ class Painter:
fill = Color(rgb=(color_part, color_part, color_part)) fill = Color(rgb=(color_part, color_part, color_part))
self.svg.add(self.svg.path( self.svg.add(self.svg.path(
d=("M", np.add(segment.point_1, shift_1), "L", d=("M", segment.point_1 + shift_1, "L",
np.add(segment.point_2, shift_1), segment.point_2 + shift_1,
np.add(segment.point_2, shift_2), segment.point_2 + shift_2,
np.add(segment.point_1, shift_2), segment.point_1 + shift_2,
np.add(segment.point_1, shift_1), "Z"), segment.point_1 + shift_1, "Z"),
fill=fill.hex, stroke=fill.hex, stroke_width=1, fill=fill.hex, stroke=fill.hex, stroke_width=1,
stroke_linejoin="round")) stroke_linejoin="round"))
# Draw building roof. # Draw building roofs.
for way in constructor.buildings: # type: Building
if way.get_levels() == level: if way.get_levels() == level:
shift = np.array([0, -way.get_levels() * height]) shift = np.array([0, -way.get_levels() * height])
path: str = way.get_path(self.flinger, shift) path_commands: str = way.get_path(self.flinger, shift)
p = Path(d=path, opacity=1) path = Path(d=path_commands, opacity=1)
p.update(way.style) path.update(way.style)
p.update({"stroke-linejoin": "round"}) path.update({"stroke-linejoin": "round"})
self.svg.add(p) self.svg.add(path)
previous_level = level previous_level = level
ui.progress_bar(-1, level_count, step=1, text="Drawing buildings")
# Trees # Trees
for node in constructor.nodes: for node in constructor.nodes:
@ -412,7 +421,7 @@ class Painter:
self, icon: Icon, point: (float, float), fill: Color, self, icon: Icon, point: (float, float), fill: Color,
tags: Dict[str, str] = None) -> None: tags: Dict[str, str] = None) -> None:
point = np.array(list(map(lambda x: int(x), point))) point = np.array(list(map(int, point)))
title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags)) title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags))
path: svgwrite.path.Path = icon.get_path(self.svg, point) path: svgwrite.path.Path = icon.get_path(self.svg, point)
@ -423,7 +432,7 @@ class Painter:
def draw_point_outline( def draw_point_outline(
self, icon: Icon, point, fill: Color, mode="default"): self, icon: Icon, point, fill: Color, mode="default"):
point = np.array(list(map(lambda x: int(x), point))) point = np.array(list(map(int, point)))
opacity: float = 0.5 opacity: float = 0.5
stroke_width: float = 2.2 stroke_width: float = 2.2
@ -479,13 +488,13 @@ def check_level_overground(tags: Dict[str, Any]):
return True return True
def main(argv): def main(argv) -> None:
if len(argv) == 2: if len(argv) == 2:
if argv[1] == "grid": if argv[1] == "grid":
draw_grid() draw_grid()
return return
options = ui.parse_options(argv) options: argparse.Namespace = ui.parse_options(argv)
if not options: if not options:
sys.exit(1) sys.exit(1)
@ -570,9 +579,6 @@ def main(argv):
scheme=scheme) scheme=scheme)
painter.draw(constructor, points) painter.draw(constructor, points)
if options.show_index:
draw_index(flinger, map_, max1, min1, svg)
print("Writing output SVG...") print("Writing output SVG...")
svg.write(open(options.output_file_name, "w")) svg.write(open(options.output_file_name, "w"))
print("Done.") print("Done.")
@ -584,48 +590,3 @@ def main(argv):
missing_tags_file.write( missing_tags_file.write(
f'- {{tag: "{tag}", count: {missing_tags[tag]}}}\n') f'- {{tag: "{tag}", count: {missing_tags[tag]}}}\n')
missing_tags_file.close() missing_tags_file.close()
def draw_index(flinger, map_, max1, min1, svg):
print(min1[1], max1[1])
print(min1[0], max1[0])
lon_step = 0.001
lat_step = 0.001
matrix = []
lat_number = int((max1[0] - min1[0]) / lat_step) + 1
lon_number = int((max1[1] - min1[1]) / lon_step) + 1
for i in range(lat_number):
row = []
for j in range(lon_number):
row.append(0)
matrix.append(row)
for node_id in map_.node_map: # type: int
node = map_.node_map[node_id]
i = int((node[0] - min1[0]) / lat_step)
j = int((node[1] - min1[1]) / lon_step)
if (0 <= i < lat_number) and (0 <= j < lon_number):
matrix[i][j] += 1
if "tags" in node:
matrix[i][j] += len(node.nodes)
for way_id in map_.way_map: # type: int
way = map_.way_map[way_id]
if "tags" in way:
for node_id in way.nodes:
node = map_.node_map[node_id]
i = int((node[0] - min1[0]) / lat_step)
j = int((node[1] - min1[1]) / lon_step)
if (0 <= i < lat_number) and (0 <= j < lon_number):
matrix[i][j] += len(way.nodes) / float(
len(way.nodes))
for i in range(lat_number):
for j in range(lon_number):
t1 = flinger.fling(np.array((
min1[0] + i * lat_step, min1[1] + j * lon_step)))
t2 = flinger.fling(np.array((
min1[0] + (i + 1) * lat_step,
min1[1] + (j + 1) * lon_step)))
svg.add(Text(
str(int(matrix[i][j])),
(((t1 + t2) * 0.5)[0], ((t1 + t2) * 0.5)[1] + 40),
font_size=80, fill="440000",
opacity=0.1, align="center"))

View file

@ -6,11 +6,11 @@ Author: Sergey Vartanov (me@enzet.ru).
import os import os
import re import re
import time import time
from typing import Dict, Optional
import urllib import urllib
import urllib3 import urllib3
from typing import Dict, Optional
from roentgen.ui import error from roentgen.ui import error
@ -26,12 +26,14 @@ def get_osm(boundary_box: str, to_update: bool = False) -> Optional[str]:
if not to_update and os.path.isfile(result_file_name): if not to_update and os.path.isfile(result_file_name):
return open(result_file_name).read() return open(result_file_name).read()
matcher = re.match("(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*)," + matcher = re.match(
"(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)", boundary_box) "(?P<left>[0-9.-]*),(?P<bottom>[0-9.-]*)," +
"(?P<right>[0-9.-]*),(?P<top>[0-9.-]*)",
boundary_box)
if not matcher: if not matcher:
error("invalid boundary box") error("invalid boundary box")
return return None
try: try:
left = float(matcher.group("left")) left = float(matcher.group("left"))
@ -40,19 +42,20 @@ def get_osm(boundary_box: str, to_update: bool = False) -> Optional[str]:
top = float(matcher.group("top")) top = float(matcher.group("top"))
except ValueError: except ValueError:
error("parsing boundary box") error("parsing boundary box")
return return None
if left >= right: if left >= right:
error("negative horizontal boundary") error("negative horizontal boundary")
return return None
if bottom >= top: if bottom >= top:
error("negative vertical boundary") error("negative vertical boundary")
return return None
if right - left > 0.5 or top - bottom > 0.5: if right - left > 0.5 or top - bottom > 0.5:
error("box too big") error("box too big")
return return None
content = get_data("api.openstreetmap.org/api/0.6/map", content = get_data(
"api.openstreetmap.org/api/0.6/map",
{"bbox": boundary_box}, is_secure=True) {"bbox": boundary_box}, is_secure=True)
open(result_file_name, "w+").write(content.decode("utf-8")) open(result_file_name, "w+").write(content.decode("utf-8"))

View file

@ -3,6 +3,8 @@ Reading OpenStreetMap data from XML file.
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
from dataclasses import dataclass
import numpy as np import numpy as np
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Set, Union from typing import Dict, List, Optional, Set, Union
@ -115,20 +117,21 @@ class OSMWay(Tagged):
""" """
return self.nodes[0] == self.nodes[-1] return self.nodes[0] == self.nodes[-1]
def try_to_glue(self, other: "OSMWay"): def try_to_glue(self, other: "OSMWay") -> Optional["OSMWay"]:
""" """
Create new combined way if ways share endpoints. Create new combined way if ways share endpoints.
""" """
if self.nodes[0] == other.nodes[0]: if self.nodes[0] == other.nodes[0]:
return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes) return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes)
elif self.nodes[0] == other.nodes[-1]: if self.nodes[0] == other.nodes[-1]:
return OSMWay(nodes=other.nodes[:-1] + self.nodes) return OSMWay(nodes=other.nodes[:-1] + self.nodes)
elif self.nodes[-1] == other.nodes[-1]: if self.nodes[-1] == other.nodes[-1]:
return OSMWay(nodes=self.nodes + list(reversed(other.nodes[:-1]))) return OSMWay(nodes=self.nodes + list(reversed(other.nodes[:-1])))
elif self.nodes[-1] == other.nodes[0]: if self.nodes[-1] == other.nodes[0]:
return OSMWay(nodes=self.nodes + other.nodes[1:]) return OSMWay(nodes=self.nodes + other.nodes[1:])
return None
def __repr__(self): def __repr__(self) -> str:
return f"Way <{self.id_}> {self.nodes}" return f"Way <{self.id_}> {self.nodes}"
@ -175,6 +178,8 @@ def get_value(key: str, text: str):
value = text[end_index:text.find('"', end_index)] value = text[end_index:text.find('"', end_index)]
return value return value
return None
class Map: class Map:
""" """
@ -243,7 +248,7 @@ class OSMReader:
line = line.strip() line = line.strip()
line_number += 1 line_number += 1
progress_bar(line_number, lines_number) progress_bar(line_number, lines_number, text="Parsing")
# Node parsing. # Node parsing.
@ -251,7 +256,6 @@ class OSMReader:
if not parse_nodes: if not parse_nodes:
if parse_ways or parse_relations: if parse_ways or parse_relations:
continue continue
else:
break break
if line[-2] == "/": if line[-2] == "/":
node: OSMNode = OSMNode().parse_from_xml(line, full) node: OSMNode = OSMNode().parse_from_xml(line, full)
@ -267,7 +271,6 @@ class OSMReader:
if not parse_ways: if not parse_ways:
if parse_relations: if parse_relations:
continue continue
else:
break break
if line[-2] == "/": if line[-2] == "/":
way = OSMWay().parse_from_xml(line, full) way = OSMWay().parse_from_xml(line, full)
@ -293,15 +296,15 @@ class OSMReader:
# Elements parsing. # Elements parsing.
elif line.startswith("<tag"): elif line.startswith("<tag"):
k = get_value("k", line) key: str = get_value("k", line)
v = get_value("v", line) value = get_value("v", line)
element.tags[k] = v element.tags[key] = value
elif line.startswith("<nd"): elif line.startswith("<nd"):
element.nodes.append( element.nodes.append(
self.map_.node_map[int(get_value("ref", line))]) self.map_.node_map[int(get_value("ref", line))])
elif line.startswith("<member"): elif line.startswith("<member"):
element.members.append(OSMMember(line)) element.members.append(OSMMember(line))
progress_bar(-1, lines_number) # Complete progress bar. progress_bar(-1, lines_number, text="Parsing")
return self.map_ return self.map_

View file

@ -7,6 +7,7 @@ import copy
import yaml import yaml
from colour import Color from colour import Color
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set
from roentgen.extract_icon import DEFAULT_SHAPE_ID from roentgen.extract_icon import DEFAULT_SHAPE_ID
@ -14,23 +15,17 @@ from roentgen.extract_icon import DEFAULT_SHAPE_ID
DEFAULT_COLOR: Color = Color("#444444") DEFAULT_COLOR: Color = Color("#444444")
@dataclass
class IconSet: class IconSet:
""" """
Node representation: icons and color. Node representation: icons and color.
""" """
def __init__( icons: List[List[str]] # list of lists of shape identifiers
self, icons: List[List[str]], color: Color, processed: Set[str], color: Color # fill color of all icons
is_default: bool): # tag keys that were processed to create icon set (other
""" # tag keys should be displayed by text or ignored)
:param icons: list of lists of shape identifiers processed: Set[str]
:param color: fill color of all icons is_default: bool
:param processed: tag keys that were processed to create icon set (other
tag keys should be displayed by text or ignored)
"""
self.icons: List[List[str]] = icons
self.color: Color = color
self.processed: Set[str] = processed
self.is_default = is_default
class Scheme: class Scheme:
@ -44,8 +39,9 @@ class Scheme:
:param file_name: scheme file name with tags, colors, and tag key :param file_name: scheme file name with tags, colors, and tag key
specification specification
""" """
content: Dict[str, Any] = \ with open(file_name) as input_file:
yaml.load(open(file_name).read(), Loader=yaml.FullLoader) content: Dict[str, Any] = yaml.load(
input_file.read(), Loader=yaml.FullLoader)
self.nodes: List[Dict[str, Any]] = content["nodes"] self.nodes: List[Dict[str, Any]] = content["nodes"]
self.ways: List[Dict[str, Any]] = content["ways"] self.ways: List[Dict[str, Any]] = content["ways"]

View file

@ -10,7 +10,7 @@ BOXES: List[str] = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
BOXES_LENGTH: int = len(BOXES) BOXES_LENGTH: int = len(BOXES)
def parse_options(args): def parse_options(args) -> argparse.Namespace:
""" """
Parse Röntgen command-line options. Parse Röntgen command-line options.
""" """
@ -70,14 +70,6 @@ def parse_options(args):
dest="overlap", dest="overlap",
default=12, default=12,
type=int) type=int)
parser.add_argument(
"--show-index",
dest="show_index",
action="store_true")
parser.add_argument(
"--no-show-index",
dest="show_index",
action="store_false")
parser.add_argument( parser.add_argument(
"--mode", "--mode",
default="normal") default="normal")
@ -88,7 +80,7 @@ def parse_options(args):
"--level", "--level",
default=None) default=None)
arguments = parser.parse_args(args[1:]) arguments: argparse.Namespace = parser.parse_args(args[1:])
if arguments.boundary_box: if arguments.boundary_box:
arguments.boundary_box = arguments.boundary_box.replace(" ", "") arguments.boundary_box = arguments.boundary_box.replace(" ", "")

View file

@ -1,25 +1,34 @@
"""
Röntgen utility file.
Author: Sergey Vartanov (me@enzet.ru).
"""
from dataclasses import dataclass
from typing import Any
@dataclass
class MinMax: class MinMax:
""" """
Minimum and maximum. Minimum and maximum.
""" """
def __init__(self, min_=None, max_=None): min_: Any = None
self.min_ = min_ max_: Any = None
self.max_ = max_
def update(self, value): def update(self, value: Any) -> None:
""" """
Update minimum and maximum with new value. Update minimum and maximum with new value.
""" """
self.min_ = value if not self.min_ or value < self.min_ else self.min_ self.min_ = value if not self.min_ or value < self.min_ else self.min_
self.max_ = value if not self.max_ or value > self.max_ else self.max_ self.max_ = value if not self.max_ or value > self.max_ else self.max_
def delta(self): def delta(self) -> Any:
""" """
Difference between maximum and minimum. Difference between maximum and minimum.
""" """
return self.max_ - self.min_ return self.max_ - self.min_
def center(self): def center(self) -> Any:
""" """
Get middle point between minimum and maximum. Get middle point between minimum and maximum.
""" """

View file

@ -9,22 +9,27 @@ from roentgen.direction import parse_vector
def test_compass_points_1(): def test_compass_points_1():
""" Test north direction. """
assert np.allclose(parse_vector("N"), np.array([0, -1])) assert np.allclose(parse_vector("N"), np.array([0, -1]))
def test_compass_points_2(): def test_compass_points_2():
""" Test north-west direction. """
root: np.float64 = -np.sqrt(2) / 2 root: np.float64 = -np.sqrt(2) / 2
assert np.allclose(parse_vector("NW"), np.array([root, root])) assert np.allclose(parse_vector("NW"), np.array([root, root]))
def test_compass_points_3(): def test_compass_points_3():
""" Test south-south-west direction. """
assert np.allclose( assert np.allclose(
parse_vector("SSW"), np.array([-0.38268343, 0.92387953])) parse_vector("SSW"), np.array([-0.38268343, 0.92387953]))
def test_wrong(): def test_invalid():
""" Test invalid direction representation string. """
assert not parse_vector("O") assert not parse_vector("O")
def test_degree(): def test_degree():
""" Test east direction. """
assert np.allclose(parse_vector("90"), np.array([1, 0])) assert np.allclose(parse_vector("90"), np.array([1, 0]))

View file

@ -6,4 +6,5 @@ from roentgen.grid import draw_grid
def test_icons(): def test_icons():
""" Test grid drawing. """
draw_grid() draw_grid()

View file

@ -1,3 +1,2 @@
def test_main(): def test_main():
assert True assert True