Fix #9, #21: fix relations; refactor flinger.

Fix inner path and outer path drawing.  Make flinger more simple.
This commit is contained in:
Sergey Vartanov 2020-09-18 23:48:33 +03:00
parent 8d8080181e
commit f6a15e7a71
10 changed files with 478 additions and 410 deletions

View file

@ -3,55 +3,65 @@ colors:
# Entity
background: "EEEEEE"
grass: "C8DC94"
sand: "F0E0D0"
beach: "F0E0C0"
deciduous: "FCAF3E"
desert: "F0E0D0"
evergreen: "688C44"
playground: "884400"
grass: "C8DC94"
parking: "DDCC99"
playground: "884400"
sand: "F0E0D0"
tree: "98AC64"
water: "AACCFF"
water_border: "6688BB"
wood: "B8CC84"
tree: "98AC64"
direction_view_color: "E0F0FF"
direction_camera_color: "0088FF"
outline_color: "FFFFFF"
beach_color: "F0E0C0"
boundary_color: "880088"
building_border_color: "E0D0C0" # "AAAAAA"
building_color: "F8F0E8" # "D0D0C0"
building_border_color: "DDDDDD" # "AAAAAA"
construction_color: "CCCCCC"
cycle_color: "4444EE"
desert_color: "F0E0D0"
foot_color: "B89A74"
indoor_color: "E8E4E0"
indoor_border_color: "C0B8B0"
emergency_color: "DD2222"
farmland_color: "FFEEBB"
foot_border_color: "FFFFFF"
grass_color: "CFE0A8"
foot_color: "B89A74"
grass_border_color: "BFD098"
grass_color: "CFE0A8"
guide_strips_color: "228833"
indoor_border_color: "C0B8B0"
indoor_color: "E8E4E0"
meadow_border_color: "BFD078"
meadow_color: "CFE088"
orchard_color: "B8DCA4"
outline_color: "FFFFFF"
parking_color: "DDCC99"
platform_color: "CCCCCC"
platform_border_color: "AAAAAA"
platform_color: "CCCCCC"
playground_border_color: "663300"
playground_color: "884400"
primary_border_color: "888888" # "AA8800"
primary_color: "FFFFFF" # "FFDD66"
primary_border_color: "AA8800"
primary_color: "FFDD66"
private_access_color: "884444"
ridge_color: "000000"
road_border_color: "CCCCCC"
rock_color: "DDDDDD"
route_color: "FFFFFF"
sand_color: "F0E0D0"
secondary_border_color: "BB9911"
secondary_color: "FFEE77"
scree_color: "CCCCCC"
water_color: "AACCFF"
tertiary_border_color: "CCAA22"
tertiary_color: "FFFF88"
water_border_color: "6688BB"
water_color: "AACCFF"
wetland_color: "BFE0D8"
wood_color: "B8CC84"
wood_border_color: "A8BC74"
wood_color: "B8CC84"
# Colors not in W3C
@ -112,12 +122,17 @@ tags:
# Emergency
- tags: {emergency: phone}
icon: [sos_phone]
- tags: {emergency: defibrillator}
icon: [defibrillator]
color: emergency_color
- tags: {emergency: fire_extinguisher}
icon: [fire_extinguisher]
color: emergency_color
- tags: {emergency: fire_hydrant}
icon: [fire_hydrant]
- tags: {emergency: phone}
icon: [sos_phone]
color: emergency_color
# Highway
@ -152,9 +167,13 @@ tags:
icon: [pole]
- tags: {power: generator}
icon: [power_generator]
- tags: {power: generator, generator:source: solar}
icon: [solar_panel]
- tags: {power: tower}
icon: [power_tower]
- tags: {tourism: "*"}
icon: [historic]
- tags: {information: "*"}
icon: [information]
- tags: {tourism: information}
@ -602,6 +621,8 @@ tags:
# Tourism
- tags: {tourism: artwork}
icon: [picture]
- tags: {tourism: artwork, artwork_type: statue}
icon: [statue]
- tags: {tourism: artwork, artwork_type: sculpture}
@ -665,87 +686,111 @@ ways:
stroke: indoor_border_color
stroke-width: 1
fill: indoor_color
layer: 10
priority: 10
- tags: {indoor: corridor}
stroke: indoor_color
stroke-width: 1
fill: indoor_color
layer: 11
- tags: {indoor: ["yes", room, elevator]}
priority: 11
- tags: {highway: corridor}
stroke: "#00FF00"
stroke-width: 5
priority: 11
- tags: {indoor: ["yes", room, elevator], area: "yes"}
stroke: indoor_color
stroke-width: 1
fill: indoor_color
layer: 12
priority: 12
- tags: {indoor: column}
stroke: indoor_color
stroke-width: 1
fill: indoor_color
layer: 13
priority: 13
- tags: {power: line}
stroke: "#000000"
stroke-width: 1
opacity: 0.2
priority: 80
- tags: {natural: wood}
fill: wood_color
layer: 21
priority: 21
- tags: {natural: wetland}
fill: wetland_color
layer: 21
priority: 21
- tags: {natural: grassland}
fill: grass_color
stroke: grass_border_color
layer: 20
priority: 20
- tags: {natural: scrub}
fill: wood_color
layer: 21
priority: 21
- tags: {natural: sand}
fill: sand_color
layer: 20
priority: 20
- tags: {natural: beach}
fill: beach_color
layer: 20
priority: 20
- tags: {natural: desert}
fill: desert_color
layer: 20
priority: 20
- tags: {natural: forest}
fill: wood_color
layer: 21
priority: 21
- tags: {natural: tree_row}
layer: 21
priority: 21
stroke: wood_color
stroke-width: 5
- tags: {natural: water}
# fill: water_color
stroke: water_border_color
stroke-width: 1
layer: 21
fill: water_color
# stroke: water_border_color
# stroke-width: 1
priority: 21
- tags: {natural: coastline}
# fill: water_color
stroke: water_border_color
stroke-width: 1
layer: 21
priority: 21
- tags: {natural: ridge}
stroke-width: 2
opacity: 0.3
stroke: ridge_color
layer: 21
priority: 21
- tags: {natural: bare_rock}
fill: rock_color
- tags: {natural: scree}
fill: scree_color
- tags: {landuse: grass}
fill: grass_color
layer: 20
stroke: grass_border_color
- tags: {landuse: conservation}
fill: grass_color
layer: 20
- tags: {landuse: forest}
fill: wood_color
layer: 20
- tags: {landuse: garages}
fill: parking_color
layer: 21
priority: 20
- tags: {landuse: construction}
fill: construction_color
- tags: {landuse: farmland}
fill: farmland_color
priority: 20
stroke: grass_border_color
- tags: {landuse: forest}
fill: wood_color
priority: 20
- tags: {landuse: garages}
fill: parking_color
priority: 21
- tags: {landuse: grass}
fill: grass_color
priority: 20
stroke: grass_border_color
- tags: {landuse: orchard}
fill: orchard_color
priority: 21
- tags: {landuse: meadow}
fill: meadow_color
priority: 20
stroke: meadow_border_color
# Hidden land use
- tags: {landuse: residential}
fill: none
stroke: none
@ -755,10 +800,16 @@ ways:
- tags: {landuse: military}
fill: none
stroke: none
- tags: {landuse: railway}
fill: none
stroke: none
- tags: {building: "*"}
fill: building_color
stroke: building_border_color
#- tags: {building:part: "*"}
# fill: building_color
# stroke: building_border_color
- tags: {amenity: parking}
fill: parking_color
@ -782,215 +833,257 @@ ways:
- tags: {railway: subway}
stroke-width: 10
stroke: "#DDDDDD"
layer: 41
- tags: {railway: [narrow_gauge, tram]}
priority: 41
- tags: {railway: [rail, narrow_gauge, tram]}
stroke-width: 2
stroke: "#000000"
layer: 41
priority: 43
- tags: {railway: platform}
fill: platform_color
stroke-width: 1
stroke: platform_border_color
layer: 41
priority: 41
- tags: {highway: motorway}
stroke-width: 33
r2: 15
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: trunk}
stroke-width: 31
r2: 13
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: primary}
stroke-width: 29
r2: 11
stroke: primary_border_color
stroke-linecap: round
layer: 41
priority: 41.9
- tags: {highway: motorway_link}
r2: 9
stroke: road_border_color
stroke-linecap: round
priority: 41
- tags: {highway: secondary}
stroke-width: 27
stroke: road_border_color
r2: 9
stroke: secondary_border_color
stroke-linecap: round
layer: 41
priority: 41.8
- tags: {highway: tertiary}
stroke-width: 25
stroke: road_border_color
r2: 7
stroke: tertiary_border_color
stroke-linecap: round
layer: 41
priority: 41.7
- tags: {highway: unclassified}
stroke-width: 17
r2: 5
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: residential}
stroke-width: 17
r2: 5
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: living_street}
r2: 4
stroke: road_border_color
stroke-linecap: round
priority: 41
- tags: {highway: service}
no_tags: {service: parking_aisle}
stroke-width: 11
r2: 3
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: service, service: parking_aisle}
stroke-width: 7
r2: 2
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: track}
stroke-width: 3
stroke: road_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: [footway, pedestrian, cycleway]}
no_tags: {area: "yes"}
stroke-width: 3
stroke: foot_border_color
stroke-linecap: round
layer: 41
priority: 41
- tags: {highway: steps}
stroke-width: 6
stroke: foot_border_color
stroke-linecap: butt
- tags: {highway: path}
stroke-width: 3
stroke: foot_border_color
priority: 41
- tags: {highway: motorway}
stroke-width: 31
r: 15
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: trunk}
stroke-width: 29
r: 13
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: primary}
stroke-width: 27
r: 11
stroke: primary_color
stroke-linecap: round
layer: 42
priority: 42.9
- tags: {highway: secondary}
stroke-width: 25
r: 9
stroke: secondary_color
stroke-linecap: round
priority: 42.8
- tags: {highway: motorway_link}
r: 9
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: tertiary}
stroke-width: 23
stroke: "#FFFFFF"
r: 7
stroke: tertiary_color
stroke-linecap: round
layer: 42
priority: 42.7
- tags: {highway: unclassified}
stroke-width: 15
r: 5
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: residential}
stroke-width: 15
r: 5
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
- tags: {highway: service, service: parking_aisle}
stroke-width: 5
priority: 42
- tags: {highway: living_street}
r: 4
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: service}
no_tags: {service: parking_aisle}
stroke-width: 9
r: 3
stroke: "#FFFFFF"
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: service, service: parking_aisle}
r: 2
stroke: "#FFFFFF"
stroke-linecap: round
priority: 42
- tags: {highway: track}
stroke-width: 3
stroke: road_border_color
stroke-linecap: round
layer: 42
priority: 42
- tags: {highway: [footway, pedestrian]}
no_tags: {area: "yes"}
stroke-width: 1.5
stroke-dasharray: 7,3
stroke-linecap: round
stroke: foot_color
layer: 42
priority: 42
- tags: {highway: [footway, pedestrian], area: "yes"}
stroke: none
fill: "#DDDDDD"
stroke-linecap: round
layer: -55 # FIXME
priority: -55 # FIXME
- tags: {highway: cycleway}
no_tags: {area: "yes"}
stroke-width: 1
stroke: cycle_color
stroke-dasharray: 8,2
stroke-linecap: butt
layer: 42
priority: 42
- tags: {highway: steps, conveying: "*"}
stroke-width: 5
stroke-dasharray: 1.5,2
stroke-linecap: butt
stroke: "#888888"
layer: 42
priority: 42
- tags: {highway: steps}
no_tags: {conveying: "*"}
stroke-width: 5
stroke-dasharray: 1.5,2
stroke-linecap: butt
stroke: foot_color
layer: 42
priority: 42
- tags: {highway: path}
stroke-width: 1
stroke-dasharray: 5,5
stroke-width: 1.5
stroke-dasharray: 5,3
stroke-linecap: butt
stroke: foot_color
layer: 42
priority: 42
- tags: {route: ferry}
stroke-width: 1
stroke-dasharray: 3,3
stroke-linecap: butt
stroke: route_color
layer: 42
priority: 42
- tags: {leisure: garden}
fill: grass_color
priority: 21
- tags: {leisure: park}
fill: grass_color
opacity: 0.5
- tags: {leisure: pitch}
fill: playground_color
stroke: playground_border_color
stroke-width: 1
opacity: 0.2
priority: 21
- tags: {leisure: playground}
fill: playground_color
opacity: 0.2
icon: toy_horse
layer: 21
- tags: {leisure: garden}
fill: grass_color
layer: 21
- tags: {leisure: pitch}
fill: playground_color
opacity: 0.2
layer: 21
- tags: {leisure: park}
fill: grass_color
opacity: 0.5
priority: 21
- tags: {leisure: swimming_pool}
fill: water_color
stroke: water_border_color
stroke-width: 1
- tags: {barrier: hedge}
fill: none
stroke: wood_color
stroke-width: 4
layer: 40
priority: 40
- tags: {barrier: city_wall}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.6
priority: 40
- tags: {barrier: wall}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.5
priority: 40
- tags: {barrier: [fence, retaining_wall]}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.4
layer: 40
priority: 40
- tags: {barrier: handrail}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.3
layer: 40
priority: 40
- tags: {barrier: kerb}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.2
layer: 40
priority: 40
- tags: {border: "*"}
stroke: "#FF0000"
@ -1002,7 +1095,7 @@ ways:
stroke: boundary_color
stroke-width: 0.3
stroke-dasharray: 10,5
layer: 60
priority: 60
tags_to_write: [
"operator", "opening_hours", "cuisine", "network", "website",

View file

@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Set
from roentgen import ui
from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID
from roentgen.flinger import GeoFlinger
from roentgen.flinger import Flinger
from roentgen.osm_reader import OSMMember, OSMRelation, OSMWay, OSMNode
from roentgen.scheme import IconSet, Scheme
from roentgen.util import MinMax
@ -19,20 +19,47 @@ from roentgen.util import MinMax
DEBUG: bool = False
def is_clockwise(polygon: List[OSMNode]) -> bool:
"""
Are polygon nodes are in clockwise order.
"""
count: float = 0
for index in range(len(polygon)): # type: int
next_index: int = 0 if index == len(polygon) - 1 else index + 1
count += (
(polygon[next_index].position[0] - polygon[index].position[0]) *
(polygon[next_index].position[1] + polygon[index].position[1]))
return count >= 0
def make_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
if is_clockwise(polygon):
return polygon
else:
return list(reversed(polygon))
def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]:
if not is_clockwise(polygon):
return polygon
else:
return list(reversed(polygon))
class Node:
"""
Node in Röntgen terms.
"""
def __init__(
self, icon_set: IconSet, tags: Dict[str, str],
point: (float, float), path: Optional[str],
point: np.array, coordinates: np.array,
priority: int = 0, is_for_node: bool = True):
assert point is not None
self.icon_set: IconSet = icon_set
self.tags = tags
self.point = point
self.path = path
self.point: np.array = point
self.coordinates: np.array = coordinates
self.priority = priority
self.layer = 0
self.is_for_node = is_for_node
@ -48,43 +75,50 @@ class Way:
Way in Röntgen terms.
"""
def __init__(
self, kind: str, nodes: List[OSMNode], path, style: Dict[str, Any],
layer: float = 0.0, priority: float = 0, levels=None):
assert nodes or path
self, kind: str, inners, outers, style: Dict[str, Any],
layer: float = 0.0, levels=None):
self.kind = kind
self.nodes: List[OSMNode] = nodes
self.path = path
self.inners = inners
self.outers = outers
self.style: Dict[str, Any] = style
self.layer = layer
self.priority = priority
self.levels = levels
def get_float(string):
def get_path(
self, flinger: Flinger, shift: np.array = np.array((0, 0))) -> str:
"""
Try to parse float from a string.
Get SVG path commands.
:param shift: shift vector
"""
try:
return float(string)
except ValueError:
return 0
path: str = ""
for outer_nodes in self.outers:
path += get_path(
make_counter_clockwise(outer_nodes), shift, flinger) + " "
for inner_nodes in self.inners:
path += get_path(
make_clockwise(inner_nodes), shift, flinger) + " "
return path
def line_center(nodes: List[OSMNode], flinger: GeoFlinger) -> np.array:
def line_center(nodes: List[OSMNode], flinger: Flinger) -> np.array:
"""
Get geometric center of nodes set.
:param nodes: node list
:param flinger: flinger that remap geo positions
"""
x, y = MinMax(), MinMax()
boundary = [MinMax(), MinMax()]
for node in nodes: # type: OSMNode
flung = flinger.fling(node.position)
x.update(flung[0])
y.update(flung[1])
return np.array(((x.min_ + x.max_) / 2.0, (y.min_ + y.max_) / 2.0))
boundary[0].update(node.position[0])
boundary[1].update(node.position[1])
center_coordinates = np.array((boundary[0].center(), boundary[1].center()))
return flinger.fling(center_coordinates), center_coordinates
def get_user_color(text: str, seed: str):
@ -158,7 +192,7 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
return result
def get_path(nodes: List[OSMNode], shift: np.array, flinger: GeoFlinger) -> str:
def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
"""
Construct SVG path from nodes.
"""
@ -179,12 +213,15 @@ class Constructor:
"""
Röntgen node and way constructor.
"""
def __init__(self, check_level, mode, seed, map_, flinger, scheme: Scheme):
def __init__(
self, check_level, mode, seed, map_, flinger: Flinger,
scheme: Scheme):
self.check_level = check_level
self.mode = mode
self.seed = seed
self.map_ = map_
self.flinger = flinger
self.flinger: Flinger = flinger
self.scheme: Scheme = scheme
self.nodes: List[Node] = []
@ -194,44 +231,49 @@ class Constructor:
"""
Construct Röntgen ways.
"""
way_number: int = 0
for way_id in self.map_.way_map: # type: int
ui.progress_bar(
way_number, len(self.map_.way_map),
text="Constructing ways")
way_number += 1
way: OSMWay = self.map_.way_map[way_id]
if not self.check_level(way.tags):
continue
self.construct_way(way, way.tags, None)
self.construct_way(way, way.tags, [], [way.nodes])
ui.progress_bar(-1, len(self.map_.way_map), text="Constructing ways")
def construct_way(
self, way: Optional[OSMWay], tags: Dict[str, Any],
path: Optional[str]) -> None:
inners, outers) -> None:
"""
Way construction.
:param way: OSM way
:param tags: way tag dictionary
:param path: way path (if there is no nodes)
"""
assert way or path
layer: float = 0
level: 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
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
# layer = 100 * level + 0.01 * layer
nodes = None
center_point = None
center_point, center_coordinates = None, None
if way:
center_point = line_center(way.nodes, self.flinger)
center_point, center_coordinates = \
line_center(way.nodes, self.flinger)
nodes = way.nodes
if self.mode == "user-coloring":
@ -239,7 +281,7 @@ class Constructor:
return
user_color = get_user_color(way.user, self.seed)
self.ways.append(
Way("way", nodes, path,
Way("way", inners, outers,
{"fill": "none", "stroke": user_color,
"stroke-width": 1}))
return
@ -249,7 +291,7 @@ class Constructor:
return
time_color = get_time_color(way.timestamp)
self.ways.append(
Way("way", nodes, path,
Way("way", inners, outers,
{"fill": "none", "stroke": time_color,
"stroke-width": 1}))
return
@ -261,7 +303,7 @@ class Constructor:
kind: str = "way"
levels = None
if "building" in tags:
if "building" in tags: # or "building:part" in tags:
kind = "building"
if "building:levels" in tags:
try:
@ -288,21 +330,31 @@ class Constructor:
break
if matched:
style: Dict[str, Any] = {"fill": "none"}
if "layer" in element:
layer += element["layer"]
if "priority" in element:
layer = element["priority"]
for key in element: # type: str
if key not in ["tags", "no_tags", "layer", "level", "icon"]:
if key not in ["tags", "no_tags", "priority", "level", "icon", "r", "r2"]:
value = element[key]
if isinstance(value, str) and value.endswith("_color"):
value = self.scheme.get_color(value)
style[key] = value
if center_coordinates is not None:
if "r" in element:
style["stroke-width"] = \
element["r"] * \
self.flinger.get_scale(center_coordinates)
if "r2" in element:
style["stroke-width"] = \
element["r2"] * \
self.flinger.get_scale(center_coordinates) + 2
self.ways.append(
Way(kind, nodes, path, style, layer, 50, levels))
Way(kind, inners, outers, style, layer, levels))
if center_point is not None and \
(way.is_cycle() or "area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Node(
icon_set, tags, center_point, path, is_for_node=False))
icon_set, tags, center_point, center_coordinates,
is_for_node=False))
appended = True
if not appended:
@ -310,12 +362,13 @@ class Constructor:
style: Dict[str, Any] = {
"fill": "none", "stroke": "#FF0000", "stroke-width": 1}
self.ways.append(Way(
kind, nodes, path, style, layer, 50, levels))
if center_point is not None and way.is_cycle() or \
"area" in tags and tags["area"]:
kind, inners, outers, style, layer, levels))
if center_point is not None and (way.is_cycle() or
"area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Node(
icon_set, tags, center_point, path, is_for_node=False))
icon_set, tags, center_point, center_coordinates,
is_for_node=False))
def construct_relations(self) -> None:
"""
@ -327,45 +380,35 @@ class Constructor:
if not self.check_level(tags):
continue
if "type" in tags and tags["type"] == "multipolygon":
inners, outers = [], []
inner_ways: List[OSMWay] = []
outer_ways: List[OSMWay] = []
for member in relation.members: # type: OSMMember
if member.type_ == "way":
if member.role == "inner":
if member.ref in self.map_.way_map:
inners.append(self.map_.way_map[member.ref])
inner_ways.append(self.map_.way_map[member.ref])
elif member.role == "outer":
if member.ref in self.map_.way_map:
outers.append(self.map_.way_map[member.ref])
p = ""
inners_path = glue(inners)
outers_path = glue(outers)
for nodes in outers_path:
path = get_path(nodes, np.array([0, 0]), self.flinger)
p += path + " "
for nodes in inners_path:
nodes.reverse()
path = get_path(nodes, np.array([0, 0]), self.flinger)
p += path + " "
if p:
self.construct_way(None, tags, p)
outer_ways.append(self.map_.way_map[member.ref])
inners_path: List[List[OSMNode]] = glue(inner_ways)
outers_path: List[List[OSMNode]] = glue(outer_ways)
self.construct_way(None, tags, inners_path, outers_path)
def construct_nodes(self) -> None:
"""
Draw nodes.
"""
print("Draw nodes...")
start_time = datetime.now()
node_number: int = 0
s = sorted(
self.map_.node_map.keys(),
key=lambda x: -self.map_.node_map[x].position.lat)
key=lambda x: -self.map_.node_map[x].position[0])
for node_id in s: # type: int
node_number += 1
ui.progress_bar(node_number, len(self.map_.node_map))
ui.progress_bar(
node_number, len(self.map_.node_map),
text="Constructing nodes")
node: OSMNode = self.map_.node_map[node_id]
flung = self.flinger.fling(node.position)
tags = node.tags
@ -385,8 +428,6 @@ class Constructor:
if self.mode == "time":
icon_set.color = get_time_color(node.timestamp)
self.nodes.append(Node(icon_set, tags, flung, None))
self.nodes.append(Node(icon_set, tags, flung, node.position))
ui.progress_bar(-1, len(self.map_.node_map))
print("Nodes painted in " + str(datetime.now() - start_time) + ".")
ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes")

View file

@ -1,114 +1,56 @@
"""
Author: Sergey Vartanov (me@enzet.ru)
"""
import math
import numpy as np
from typing import Optional
from roentgen.util import MinMax
def get_ratio(maximum, minimum, ratio: float = 1):
return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1])
EQUATOR_LENGTH: float = 40_075_017
def map_(
value: float, current_min: float, current_max: float, target_min: float,
target_max: float):
def pseudo_mercator(coordinates: np.array) -> np.array:
"""
Map current value in bounds of current_min and current_max to bounds of
target_min and target_max.
Use spherical pseudo-Mercator projection to convert geo coordinates into
plane.
:param coordinates: geo positional in the form of (latitude, longitude)
:return: position on the plane in the form of (x, y)
"""
return \
target_min + (value - current_min) / (current_max - current_min) * \
(target_max - target_min)
return np.array((coordinates[1], 180 / np.pi * np.log(
np.tan(np.pi / 4 + coordinates[0] * (np.pi / 180) / 2))))
class Geo:
def __init__(self, lat: float, lon: float):
self.lat: float = lat
self.lon: float = lon
def __getitem__(self, item) -> Optional[float]:
if item == 0:
return self.lon
if item == 1:
return self.lat
return None
def __add__(self, other: "Geo") -> "Geo":
return Geo(self.lat + other.lat, self.lon + other.lon)
def __sub__(self, other: "Geo") -> "Geo":
return Geo(self.lat - other.lat, self.lon - other.lon)
def __repr__(self) -> str:
return f"{self.lat}, {self.lon}"
class GeoFlinger:
def __init__(
self, minimum, maximum, target_minimum, target_maximum):
class Flinger:
"""
:param minimum: minimum latitude and longitude
:param maximum: maximum latitude and longitude
:param target_minimum: minimum of the resulting image
:param target_maximum: maximum of the resulting image
Convert geo coordinates into SVG position points.
"""
self.minimum = minimum
self.maximum = maximum
# Ratio is depended of latitude. It is always <= 1. In one latitude
# degree is always 40 000 / 360 km. In one current longitude degree is
# about 40 000 / 360 * ratio km.
ratio = math.sin(
(90.0 - ((self.maximum.lat + self.minimum.lat) / 2.0))
/ 180.0 * math.pi)
# Longitude displayed as x.
# Latitude displayed as y.
# Ratio is x / y.
space: np.array = [0, 0]
current_ratio = get_ratio(self.maximum, self.minimum, ratio)
target_ratio = get_ratio(target_maximum, target_minimum)
if current_ratio >= target_ratio:
n = (target_maximum[0] - target_minimum[0]) / \
(maximum.lon - minimum.lon) / ratio
space[1] = \
((target_maximum[1] - target_minimum[1]) -
(maximum.lat - minimum.lat) * n) / 2.0
space[0] = 0
else:
n = (target_maximum[1] - target_minimum[1]) / \
(maximum.lat - minimum.lat) * ratio
space[0] = \
((target_maximum[0] - target_minimum[0]) -
(maximum.lon - minimum.lon) * n) / 2.0
space[1] = 0
self.target_minimum = np.add(target_minimum, space)
self.target_maximum = np.subtract(target_maximum, space)
meters_per_pixel = \
(self.maximum.lat - self.minimum.lat) / \
(self.target_maximum[1] - self.target_minimum[1]) * \
40000 / 360 * 1000
self.scale = 1 / meters_per_pixel
self.space = space
def fling(self, current) -> np.array:
def __init__(self, geo_boundaries: MinMax, ratio: float = 1000):
"""
:param current: vector to fling
:param geo_boundaries: minimum and maximum latitude and longitude
"""
x = map_(
current.lon, self.minimum.lon, self.maximum.lon,
self.target_minimum[0], self.target_maximum[0])
y = map_(
self.maximum.lat + self.minimum.lat - current.lat,
self.minimum.lat, self.maximum.lat,
self.target_minimum[1], self.target_maximum[1])
return np.array([x, y])
self.geo_boundaries: MinMax = geo_boundaries
self.ratio: float = ratio
self.size: np.array = self.ratio * (
pseudo_mercator(self.geo_boundaries.max_) -
pseudo_mercator(self.geo_boundaries.min_))
self.pixels_per_meter = 360 / EQUATOR_LENGTH * self.ratio
self.size: np.array = self.size.astype(int).astype(float)
def fling(self, coordinates: np.array) -> np.array:
"""
:param coordinates: vector to fling
"""
result: np.array = self.ratio * (
pseudo_mercator(coordinates) -
pseudo_mercator(self.geo_boundaries.min_))
# Invert y axis on coordinate plane.
result[1] = self.size[1] - result[1]
return result
def get_scale(self, coordinates: np.array) -> float:
scale_factor = 1 / np.cos(coordinates[0] / 180 * np.pi)
return self.pixels_per_meter * scale_factor

View file

@ -16,14 +16,15 @@ from typing import Any, Dict, List
from roentgen import ui
from roentgen.address import get_address
from roentgen.constructor import Constructor, get_path, Node, Way
from roentgen.flinger import GeoFlinger, Geo
from roentgen.constructor import Constructor, Node, Way
from roentgen.flinger import Flinger
from roentgen.grid import draw_grid
from roentgen.extract_icon import Icon, IconExtractor
from roentgen.osm_getter import get_osm
from roentgen.osm_reader import Map, OSMReader
from roentgen.scheme import Scheme
from roentgen.direction import DirectionSet, Sector
from roentgen.util import MinMax
ICONS_FILE_NAME: str = "icons/icons.svg"
TAGS_FILE_NAME: str = "data/tags.yml"
@ -40,7 +41,7 @@ class Painter:
"""
def __init__(
self, show_missing_tags: bool, overlap: int, draw_nodes: bool,
mode: str, draw_captions: str, map_: Map, flinger: GeoFlinger,
mode: str, draw_captions: str, map_: Map, flinger: Flinger,
svg: svgwrite.Drawing, icon_extractor: IconExtractor,
scheme: Scheme):
@ -51,7 +52,7 @@ class Painter:
self.draw_captions: str = draw_captions
self.map_: Map = map_
self.flinger: GeoFlinger = flinger
self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg
self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme
@ -241,50 +242,44 @@ class Painter:
else:
shift_2 = [0, -3]
if way.nodes:
for i in range(len(way.nodes) - 1):
flung_1 = self.flinger.fling(way.nodes[i].position)
flung_2 = self.flinger.fling(way.nodes[i + 1].position)
for nodes in way.inners + way.outers:
for i in range(len(nodes) - 1):
flung_1 = self.flinger.fling(nodes[i].position)
flung_2 = self.flinger.fling(nodes[i + 1].position)
self.svg.add(self.svg.path(
d=("M", np.add(flung_1, shift_1), "L",
np.add(flung_2, shift_1), np.add(flung_2, shift_2),
np.add(flung_1, shift_2), "Z"),
fill=color, stroke=color, stroke_width=1))
elif way.path:
# TODO: implement
pass
def draw(self, nodes, ways, points):
def draw(self, nodes: List[Node], ways: List[Way], points):
"""
Draw map.
"""
ways = sorted(ways, key=lambda x: x.layer)
for way in ways:
for way in ways: # type: Way
if way.kind == "way":
if way.nodes:
path = get_path(way.nodes, np.array([0, 0]), self.flinger)
path: str = way.get_path(self.flinger)
if path:
p = Path(d=path)
p.update(way.style)
self.svg.add(p)
else:
p = Path(d=way.path)
p.update(way.style)
self.svg.add(p)
# Building shade
building_shade = Group(opacity=0.1)
for way in ways: # type: Way
if way.kind != "building" or not way.nodes:
if way.kind != "building":
continue
shift = [-5, 5]
if way.levels:
shift = [-5 * way.levels, 5 * way.levels]
for i in range(len(way.nodes) - 1):
flung_1 = self.flinger.fling(way.nodes[i].position)
flung_2 = self.flinger.fling(way.nodes[i + 1].position)
for nodes11 in way.inners + way.outers:
for i in range(len(nodes11) - 1):
flung_1 = self.flinger.fling(nodes11[i].position)
flung_2 = self.flinger.fling(nodes11[i + 1].position)
building_shade.add(Path(
("M", flung_1, "L", flung_2, np.add(flung_2, shift),
np.add(flung_1, shift), "Z"),
@ -300,20 +295,27 @@ class Painter:
# Building roof
building_paths: List[(str, Dict)] = []
for way in ways: # type: Way
if way.kind != "building":
continue
if way.nodes:
shift = [0, -3]
if way.levels:
shift = np.array([0 * way.levels, min(-3, -1 * way.levels)])
path = get_path(way.nodes, shift, self.flinger)
path: str = way.get_path(self.flinger, shift)
if path:
building_paths.append((path, way.style))
for path, style in building_paths:
p = Path(d=path, opacity=1)
p.update(way.style)
p.update(style)
p.update({"stroke": "none"})
self.svg.add(p)
else:
p = Path(d=way.path, opacity=1)
p.update(way.style)
for path, style in building_paths:
p = Path(d=path, opacity=1)
p.update(style)
p.update({"fill": "none"})
self.svg.add(p)
# Trees
@ -326,12 +328,14 @@ class Painter:
if "circumference" in node.tags:
self.svg.add(self.svg.circle(
node.point,
float(node.tags["circumference"]) * self.flinger.scale / 2,
float(node.tags["circumference"]) *
self.flinger.get_scale(node.coordinates) / 2,
fill="#AAAA88", opacity=0.3))
if "diameter_crown" in node.tags:
self.svg.add(self.svg.circle(
node.point,
float(node.tags["diameter_crown"]) * self.flinger.scale / 2,
float(node.tags["diameter_crown"]) *
self.flinger.get_scale(node.coordinates) / 2,
fill=self.scheme.get_color("evergreen"), opacity=0.3))
# Directions
@ -347,16 +351,19 @@ class Painter:
angle = float(node.get_tag("camera:angle"))
if "angle" in node.tags:
angle = float(node.get_tag("angle"))
direction_radius: int = 25 * self.flinger.scale
direction_radius: float = \
25 * self.flinger.get_scale(node.coordinates)
direction_color: str = \
self.scheme.get_color("direction_camera_color")
elif node.get_tag("traffic_sign") == "stop":
direction = node.get_tag("direction")
direction_radius: int = 25 * self.flinger.scale
direction_radius: float = \
25 * self.flinger.get_scale(node.coordinates)
direction_color: str = "#FF0000"
else:
direction = node.get_tag("direction")
direction_radius: int = 100 * self.flinger.scale
direction_radius: float = \
50 * self.flinger.get_scale(node.coordinates)
direction_color: str = \
self.scheme.get_color("direction_view_color")
is_revert_gradient = True
@ -367,11 +374,9 @@ class Painter:
point = (node.point.astype(int)).astype(float)
if angle:
paths = [Sector(direction, angle)
.draw(point, direction_radius)]
paths = [Sector(direction, angle).draw(point, direction_radius)]
else:
paths = DirectionSet(direction) \
.draw(point, direction_radius)
paths = DirectionSet(direction).draw(point, direction_radius)
for path in paths:
gradient = self.svg.defs.add(self.svg.radialGradient(
@ -397,9 +402,9 @@ class Painter:
("diameter_crown" in node.tags or
"circumference" in node.tags):
continue
ui.progress_bar(index, len(nodes), step=10)
ui.progress_bar(index, len(nodes), step=10, text="Draw nodes")
self.draw_shapes(node, points)
ui.progress_bar(-1, len(nodes), step=10)
ui.progress_bar(-1, len(nodes), step=10, text="Draw nodes")
if self.draw_captions == "no":
return
@ -494,13 +499,13 @@ def check_level_overground(tags: Dict[str, Any]):
return True
def main():
if len(sys.argv) == 2:
if sys.argv[1] == "grid":
def main(argv):
if len(argv) == 2:
if argv[1] == "grid":
draw_grid()
return
options = ui.parse_options(sys.argv)
options = ui.parse_options(argv)
if not options:
sys.exit(1)
@ -538,23 +543,19 @@ def main():
map_: Map = osm_reader.map_
w, h = list(map(lambda x: float(x), options.size.split(",")))
svg: svgwrite.Drawing = \
svgwrite.Drawing(options.output_file_name, size=(w, h))
svg.add(Rect((0, 0), (w, h), fill=background_color))
min1 = Geo(boundary_box[1], boundary_box[0])
max1 = Geo(boundary_box[3], boundary_box[2])
missing_tags = {}
points = []
scheme: Scheme = Scheme(TAGS_FILE_NAME, COLORS_FILE_NAME)
flinger: GeoFlinger = \
GeoFlinger(min1, max1, np.array([0, 0]), np.array([w, h]))
min1: np.array = np.array((boundary_box[1], boundary_box[0]))
max1: np.array = np.array((boundary_box[3], boundary_box[2]))
flinger: Flinger = Flinger(MinMax(min1, max1), options.scale)
size: np.array = flinger.size
svg: svgwrite.Drawing = \
svgwrite.Drawing(options.output_file_name, size=size)
svg.add(Rect((0, 0), size, fill=background_color))
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
@ -589,21 +590,12 @@ def main():
scheme=scheme)
painter.draw(constructor.nodes, constructor.ways, points)
if flinger.space[0] == 0:
svg.add(Rect((0, 0), (w, flinger.space[1]), fill="#FFFFFF"))
svg.add(Rect(
(0, h - flinger.space[1]), (w, flinger.space[1]), fill="#FFFFFF"))
if flinger.space[1] == 0:
svg.add(Rect((0, 0), (flinger.space[0], h), fill="#FFFFFF"))
svg.add(Rect(
(w - flinger.space[0], 0), (flinger.space[0], h), fill="#FFFFFF"))
if options.show_index:
draw_index(flinger, map_, max1, min1, svg)
print("Writing output SVG...")
svg.write(open(options.output_file_name, "w"))
print("Done")
print("Done.")
top_missing_tags = \
sorted(missing_tags.keys(), key=lambda x: -missing_tags[x])
@ -615,13 +607,13 @@ def main():
def draw_index(flinger, map_, max1, min1, svg):
print(min1.lon, max1.lon)
print(min1.lat, max1.lat)
print(min1[1], max1[1])
print(min1[0], max1[0])
lon_step = 0.001
lat_step = 0.001
matrix = []
lat_number = int((max1.lat - min1.lat) / lat_step) + 1
lon_number = int((max1.lon - min1.lon) / lon_step) + 1
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):
@ -629,8 +621,8 @@ def draw_index(flinger, map_, max1, min1, svg):
matrix.append(row)
for node_id in map_.node_map: # type: int
node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step)
j = int((node.lon - min1.lon) / lon_step)
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:
@ -640,18 +632,18 @@ def draw_index(flinger, map_, max1, min1, svg):
if "tags" in way:
for node_id in way.nodes:
node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step)
j = int((node.lon - min1.lon) / lon_step)
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.tags) / float(
len(way.nodes))
for i in range(lat_number):
for j in range(lon_number):
t1 = flinger.fling(Geo(
min1.lat + i * lat_step, min1.lon + j * lon_step))
t2 = flinger.fling(Geo(
min1.lat + (i + 1) * lat_step,
min1.lon + (j + 1) * lon_step))
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),

View file

@ -3,10 +3,10 @@ Reading OpenStreetMap data from XML file.
Author: Sergey Vartanov (me@enzet.ru).
"""
import numpy as np
from datetime import datetime
from typing import Dict, List, Optional, Set, Union
from roentgen.flinger import Geo
from roentgen.ui import progress_bar
from roentgen.util import MinMax
@ -21,7 +21,7 @@ class OSMNode:
"""
def __init__(self):
self.id_: Optional[int] = None
self.position: Optional[Geo] = None
self.position: Optional[np.array] = None
self.tags: Dict[str, str] = {}
self.visible: Optional[str] = None
@ -38,8 +38,8 @@ class OSMNode:
:param is_full: if false, parse only ID, latitude and longitude
"""
self.id_ = int(get_value("id", text))
self.position = Geo(
float(get_value("lat", text)), float(get_value("lon", text)))
self.position = np.array((
float(get_value("lat", text)), float(get_value("lon", text))))
if is_full:
self.visible = get_value("visible", text)

View file

@ -77,8 +77,6 @@ class Scheme:
if color.startswith("#"):
return color
print(f"No color {color}.")
return DEFAULT_COLOR
def is_no_drawable(self, key: str) -> bool:

View file

@ -36,10 +36,11 @@ def parse_options(args):
help="geo boundary box, use \"m\" instead of \"-\" for negative values",
required=True)
parser.add_argument(
"-s", "--size",
metavar="<width>,<height>",
help="output SVG file size in pixels",
dest="size",
"-s", "--scale",
metavar="<float>",
help="map scale",
dest="scale",
type=float,
required=True)
parser.add_argument(
"-nn", "--no-draw-nodes",
@ -95,7 +96,8 @@ def parse_options(args):
def progress_bar(
number: int, total: int, length: int = 20, step: int = 1000) -> None:
number: int, total: int, length: int = 20, step: int = 1000,
text: str = "") -> None:
"""
Draw progress bar using Unicode symbols.
@ -106,7 +108,7 @@ def progress_bar(
subsequently)
"""
if number == -1:
print(f"100 % {length * ''}")
print(f"100 % {length * ''}{text}")
elif number % step == 0:
ratio: float = number / total
parts: int = int(ratio * length * BOXES_LENGTH)
@ -114,7 +116,7 @@ def progress_bar(
box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)]
print(
f"{str(int(int(ratio * 1000) / 10)):>3} % {fill_length * ''}{box}"
f"{int(length - fill_length - 1) * ' '}")
f"{int(length - fill_length - 1) * ' '}{text}")
sys.stdout.write("\033[F")

View file

@ -2,9 +2,9 @@ class MinMax:
"""
Minimum and maximum.
"""
def __init__(self):
self.min_ = None
self.max_ = None
def __init__(self, min_=None, max_=None):
self.min_ = min_
self.max_ = max_
def update(self, value):
"""
@ -18,3 +18,6 @@ class MinMax:
Difference between maximum and minimum.
"""
return self.max_ - self.min_
def center(self):
return (self.min_ + self.max_) / 2

4
run.py
View file

@ -3,7 +3,9 @@ Röntgen entry point.
Author: Sergey Vartanov (me@enzet.ru).
"""
import sys
from roentgen.mapper import main
if __name__ == "__main__":
main()
main(sys.argv)

View file

@ -2,13 +2,8 @@
Author: Sergey Vartanov (me@enzet.ru).
"""
from roentgen.flinger import map_
from roentgen.grid import draw_grid
def test_flinger_map():
assert map_(5, 0, 10, 0, 20) == 10
def test_icons():
draw_grid()