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 # Entity
background: "EEEEEE" background: "EEEEEE"
grass: "C8DC94"
sand: "F0E0D0"
beach: "F0E0C0" beach: "F0E0C0"
deciduous: "FCAF3E" deciduous: "FCAF3E"
desert: "F0E0D0" desert: "F0E0D0"
evergreen: "688C44" evergreen: "688C44"
playground: "884400" grass: "C8DC94"
parking: "DDCC99" parking: "DDCC99"
playground: "884400"
sand: "F0E0D0"
tree: "98AC64"
water: "AACCFF" water: "AACCFF"
water_border: "6688BB" water_border: "6688BB"
wood: "B8CC84" wood: "B8CC84"
tree: "98AC64"
direction_view_color: "E0F0FF" direction_view_color: "E0F0FF"
direction_camera_color: "0088FF" direction_camera_color: "0088FF"
outline_color: "FFFFFF"
beach_color: "F0E0C0" beach_color: "F0E0C0"
boundary_color: "880088" boundary_color: "880088"
building_border_color: "E0D0C0" # "AAAAAA"
building_color: "F8F0E8" # "D0D0C0" building_color: "F8F0E8" # "D0D0C0"
building_border_color: "DDDDDD" # "AAAAAA"
construction_color: "CCCCCC" construction_color: "CCCCCC"
cycle_color: "4444EE" cycle_color: "4444EE"
desert_color: "F0E0D0" desert_color: "F0E0D0"
foot_color: "B89A74" emergency_color: "DD2222"
indoor_color: "E8E4E0" farmland_color: "FFEEBB"
indoor_border_color: "C0B8B0"
foot_border_color: "FFFFFF" foot_border_color: "FFFFFF"
grass_color: "CFE0A8" foot_color: "B89A74"
grass_border_color: "BFD098" grass_border_color: "BFD098"
grass_color: "CFE0A8"
guide_strips_color: "228833" 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" parking_color: "DDCC99"
platform_color: "CCCCCC"
platform_border_color: "AAAAAA" platform_border_color: "AAAAAA"
platform_color: "CCCCCC"
playground_border_color: "663300"
playground_color: "884400" playground_color: "884400"
primary_border_color: "888888" # "AA8800" primary_border_color: "AA8800"
primary_color: "FFFFFF" # "FFDD66" primary_color: "FFDD66"
private_access_color: "884444" private_access_color: "884444"
ridge_color: "000000" ridge_color: "000000"
road_border_color: "CCCCCC" road_border_color: "CCCCCC"
rock_color: "DDDDDD" rock_color: "DDDDDD"
route_color: "FFFFFF" route_color: "FFFFFF"
sand_color: "F0E0D0" sand_color: "F0E0D0"
secondary_border_color: "BB9911"
secondary_color: "FFEE77"
scree_color: "CCCCCC" scree_color: "CCCCCC"
water_color: "AACCFF" tertiary_border_color: "CCAA22"
tertiary_color: "FFFF88"
water_border_color: "6688BB" water_border_color: "6688BB"
water_color: "AACCFF"
wetland_color: "BFE0D8" wetland_color: "BFE0D8"
wood_color: "B8CC84"
wood_border_color: "A8BC74" wood_border_color: "A8BC74"
wood_color: "B8CC84"
# Colors not in W3C # Colors not in W3C
@ -112,12 +122,17 @@ tags:
# Emergency # Emergency
- tags: {emergency: phone} - tags: {emergency: defibrillator}
icon: [sos_phone] icon: [defibrillator]
color: emergency_color
- tags: {emergency: fire_extinguisher} - tags: {emergency: fire_extinguisher}
icon: [fire_extinguisher] icon: [fire_extinguisher]
color: emergency_color
- tags: {emergency: fire_hydrant} - tags: {emergency: fire_hydrant}
icon: [fire_hydrant] icon: [fire_hydrant]
- tags: {emergency: phone}
icon: [sos_phone]
color: emergency_color
# Highway # Highway
@ -152,9 +167,13 @@ tags:
icon: [pole] icon: [pole]
- tags: {power: generator} - tags: {power: generator}
icon: [power_generator] icon: [power_generator]
- tags: {power: generator, generator:source: solar}
icon: [solar_panel]
- tags: {power: tower} - tags: {power: tower}
icon: [power_tower] icon: [power_tower]
- tags: {tourism: "*"}
icon: [historic]
- tags: {information: "*"} - tags: {information: "*"}
icon: [information] icon: [information]
- tags: {tourism: information} - tags: {tourism: information}
@ -602,6 +621,8 @@ tags:
# Tourism # Tourism
- tags: {tourism: artwork}
icon: [picture]
- tags: {tourism: artwork, artwork_type: statue} - tags: {tourism: artwork, artwork_type: statue}
icon: [statue] icon: [statue]
- tags: {tourism: artwork, artwork_type: sculpture} - tags: {tourism: artwork, artwork_type: sculpture}
@ -665,87 +686,111 @@ ways:
stroke: indoor_border_color stroke: indoor_border_color
stroke-width: 1 stroke-width: 1
fill: indoor_color fill: indoor_color
layer: 10 priority: 10
- tags: {indoor: corridor} - tags: {indoor: corridor}
stroke: indoor_color stroke: indoor_color
stroke-width: 1 stroke-width: 1
fill: indoor_color fill: indoor_color
layer: 11 priority: 11
- tags: {indoor: ["yes", room, elevator]} - tags: {highway: corridor}
stroke: "#00FF00"
stroke-width: 5
priority: 11
- tags: {indoor: ["yes", room, elevator], area: "yes"}
stroke: indoor_color stroke: indoor_color
stroke-width: 1 stroke-width: 1
fill: indoor_color fill: indoor_color
layer: 12 priority: 12
- tags: {indoor: column} - tags: {indoor: column}
stroke: indoor_color stroke: indoor_color
stroke-width: 1 stroke-width: 1
fill: indoor_color fill: indoor_color
layer: 13 priority: 13
- tags: {power: line}
stroke: "#000000"
stroke-width: 1
opacity: 0.2
priority: 80
- tags: {natural: wood} - tags: {natural: wood}
fill: wood_color fill: wood_color
layer: 21 priority: 21
- tags: {natural: wetland} - tags: {natural: wetland}
fill: wetland_color fill: wetland_color
layer: 21 priority: 21
- tags: {natural: grassland} - tags: {natural: grassland}
fill: grass_color fill: grass_color
stroke: grass_border_color stroke: grass_border_color
layer: 20 priority: 20
- tags: {natural: scrub} - tags: {natural: scrub}
fill: wood_color fill: wood_color
layer: 21 priority: 21
- tags: {natural: sand} - tags: {natural: sand}
fill: sand_color fill: sand_color
layer: 20 priority: 20
- tags: {natural: beach} - tags: {natural: beach}
fill: beach_color fill: beach_color
layer: 20 priority: 20
- tags: {natural: desert} - tags: {natural: desert}
fill: desert_color fill: desert_color
layer: 20 priority: 20
- tags: {natural: forest} - tags: {natural: forest}
fill: wood_color fill: wood_color
layer: 21 priority: 21
- tags: {natural: tree_row} - tags: {natural: tree_row}
layer: 21 priority: 21
stroke: wood_color stroke: wood_color
stroke-width: 5 stroke-width: 5
- tags: {natural: water} - tags: {natural: water}
# fill: water_color fill: water_color
stroke: water_border_color # stroke: water_border_color
stroke-width: 1 # stroke-width: 1
layer: 21 priority: 21
- tags: {natural: coastline} - tags: {natural: coastline}
# fill: water_color # fill: water_color
stroke: water_border_color stroke: water_border_color
stroke-width: 1 stroke-width: 1
layer: 21 priority: 21
- tags: {natural: ridge} - tags: {natural: ridge}
stroke-width: 2 stroke-width: 2
opacity: 0.3 opacity: 0.3
stroke: ridge_color stroke: ridge_color
layer: 21 priority: 21
- tags: {natural: bare_rock} - tags: {natural: bare_rock}
fill: rock_color fill: rock_color
- tags: {natural: scree} - tags: {natural: scree}
fill: scree_color fill: scree_color
- tags: {landuse: grass}
fill: grass_color
layer: 20
stroke: grass_border_color
- tags: {landuse: conservation} - tags: {landuse: conservation}
fill: grass_color fill: grass_color
layer: 20 priority: 20
- tags: {landuse: forest}
fill: wood_color
layer: 20
- tags: {landuse: garages}
fill: parking_color
layer: 21
- tags: {landuse: construction} - tags: {landuse: construction}
fill: construction_color 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} - tags: {landuse: residential}
fill: none fill: none
stroke: none stroke: none
@ -755,10 +800,16 @@ ways:
- tags: {landuse: military} - tags: {landuse: military}
fill: none fill: none
stroke: none stroke: none
- tags: {landuse: railway}
fill: none
stroke: none
- tags: {building: "*"} - tags: {building: "*"}
fill: building_color fill: building_color
stroke: building_border_color stroke: building_border_color
#- tags: {building:part: "*"}
# fill: building_color
# stroke: building_border_color
- tags: {amenity: parking} - tags: {amenity: parking}
fill: parking_color fill: parking_color
@ -782,215 +833,257 @@ ways:
- tags: {railway: subway} - tags: {railway: subway}
stroke-width: 10 stroke-width: 10
stroke: "#DDDDDD" stroke: "#DDDDDD"
layer: 41 priority: 41
- tags: {railway: [narrow_gauge, tram]} - tags: {railway: [rail, narrow_gauge, tram]}
stroke-width: 2 stroke-width: 2
stroke: "#000000" stroke: "#000000"
layer: 41 priority: 43
- tags: {railway: platform} - tags: {railway: platform}
fill: platform_color fill: platform_color
stroke-width: 1 stroke-width: 1
stroke: platform_border_color stroke: platform_border_color
layer: 41 priority: 41
- tags: {highway: motorway} - tags: {highway: motorway}
stroke-width: 33 r2: 15
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: trunk} - tags: {highway: trunk}
stroke-width: 31 r2: 13
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: primary} - tags: {highway: primary}
stroke-width: 29 r2: 11
stroke: primary_border_color stroke: primary_border_color
stroke-linecap: round 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} - tags: {highway: secondary}
stroke-width: 27 r2: 9
stroke: road_border_color stroke: secondary_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41.8
- tags: {highway: tertiary} - tags: {highway: tertiary}
stroke-width: 25 r2: 7
stroke: road_border_color stroke: tertiary_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41.7
- tags: {highway: unclassified} - tags: {highway: unclassified}
stroke-width: 17 r2: 5
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: residential} - tags: {highway: residential}
stroke-width: 17 r2: 5
stroke: road_border_color stroke: road_border_color
stroke-linecap: round 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} - tags: {highway: service}
no_tags: {service: parking_aisle} no_tags: {service: parking_aisle}
stroke-width: 11 r2: 3
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: service, service: parking_aisle} - tags: {highway: service, service: parking_aisle}
stroke-width: 7 r2: 2
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: track} - tags: {highway: track}
stroke-width: 3 stroke-width: 3
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: [footway, pedestrian, cycleway]} - tags: {highway: [footway, pedestrian, cycleway]}
no_tags: {area: "yes"} no_tags: {area: "yes"}
stroke-width: 3 stroke-width: 3
stroke: foot_border_color stroke: foot_border_color
stroke-linecap: round stroke-linecap: round
layer: 41 priority: 41
- tags: {highway: steps} - tags: {highway: steps}
stroke-width: 6 stroke-width: 6
stroke: foot_border_color stroke: foot_border_color
stroke-linecap: butt stroke-linecap: butt
- tags: {highway: path}
stroke-width: 3
stroke: foot_border_color
priority: 41
- tags: {highway: motorway} - tags: {highway: motorway}
stroke-width: 31 r: 15
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: trunk} - tags: {highway: trunk}
stroke-width: 29 r: 13
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: primary} - tags: {highway: primary}
stroke-width: 27 r: 11
stroke: primary_color stroke: primary_color
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42.9
- tags: {highway: secondary} - 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: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: tertiary} - tags: {highway: tertiary}
stroke-width: 23 r: 7
stroke: "#FFFFFF" stroke: tertiary_color
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42.7
- tags: {highway: unclassified} - tags: {highway: unclassified}
stroke-width: 15 r: 5
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: residential} - tags: {highway: residential}
stroke-width: 15 r: 5
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: service, service: parking_aisle} - tags: {highway: living_street}
stroke-width: 5 r: 4
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: service} - tags: {highway: service}
no_tags: {service: parking_aisle} no_tags: {service: parking_aisle}
stroke-width: 9 r: 3
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round 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} - tags: {highway: track}
stroke-width: 3 stroke-width: 3
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
layer: 42 priority: 42
- tags: {highway: [footway, pedestrian]} - tags: {highway: [footway, pedestrian]}
no_tags: {area: "yes"} no_tags: {area: "yes"}
stroke-width: 1.5 stroke-width: 1.5
stroke-dasharray: 7,3 stroke-dasharray: 7,3
stroke-linecap: round stroke-linecap: round
stroke: foot_color stroke: foot_color
layer: 42 priority: 42
- tags: {highway: [footway, pedestrian], area: "yes"} - tags: {highway: [footway, pedestrian], area: "yes"}
stroke: none stroke: none
fill: "#DDDDDD" fill: "#DDDDDD"
stroke-linecap: round stroke-linecap: round
layer: -55 # FIXME priority: -55 # FIXME
- tags: {highway: cycleway} - tags: {highway: cycleway}
no_tags: {area: "yes"} no_tags: {area: "yes"}
stroke-width: 1 stroke-width: 1
stroke: cycle_color stroke: cycle_color
stroke-dasharray: 8,2 stroke-dasharray: 8,2
stroke-linecap: butt stroke-linecap: butt
layer: 42 priority: 42
- tags: {highway: steps, conveying: "*"} - tags: {highway: steps, conveying: "*"}
stroke-width: 5 stroke-width: 5
stroke-dasharray: 1.5,2 stroke-dasharray: 1.5,2
stroke-linecap: butt stroke-linecap: butt
stroke: "#888888" stroke: "#888888"
layer: 42 priority: 42
- tags: {highway: steps} - tags: {highway: steps}
no_tags: {conveying: "*"} no_tags: {conveying: "*"}
stroke-width: 5 stroke-width: 5
stroke-dasharray: 1.5,2 stroke-dasharray: 1.5,2
stroke-linecap: butt stroke-linecap: butt
stroke: foot_color stroke: foot_color
layer: 42 priority: 42
- tags: {highway: path} - tags: {highway: path}
stroke-width: 1 stroke-width: 1.5
stroke-dasharray: 5,5 stroke-dasharray: 5,3
stroke-linecap: butt stroke-linecap: butt
stroke: foot_color stroke: foot_color
layer: 42 priority: 42
- tags: {route: ferry} - tags: {route: ferry}
stroke-width: 1 stroke-width: 1
stroke-dasharray: 3,3 stroke-dasharray: 3,3
stroke-linecap: butt stroke-linecap: butt
stroke: route_color 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} - tags: {leisure: playground}
fill: playground_color fill: playground_color
opacity: 0.2 opacity: 0.2
icon: toy_horse icon: toy_horse
layer: 21 priority: 21
- tags: {leisure: garden} - tags: {leisure: swimming_pool}
fill: grass_color fill: water_color
layer: 21 stroke: water_border_color
- tags: {leisure: pitch} stroke-width: 1
fill: playground_color
opacity: 0.2
layer: 21
- tags: {leisure: park}
fill: grass_color
opacity: 0.5
- tags: {barrier: hedge} - tags: {barrier: hedge}
fill: none fill: none
stroke: wood_color stroke: wood_color
stroke-width: 4 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]} - tags: {barrier: [fence, retaining_wall]}
fill: none fill: none
stroke: "#000000" stroke: "#000000"
stroke-width: 1 stroke-width: 1
opacity: 0.4 opacity: 0.4
layer: 40 priority: 40
- tags: {barrier: handrail} - tags: {barrier: handrail}
fill: none fill: none
stroke: "#000000" stroke: "#000000"
stroke-width: 1 stroke-width: 1
opacity: 0.3 opacity: 0.3
layer: 40 priority: 40
- tags: {barrier: kerb} - tags: {barrier: kerb}
fill: none fill: none
stroke: "#000000" stroke: "#000000"
stroke-width: 1 stroke-width: 1
opacity: 0.2 opacity: 0.2
layer: 40 priority: 40
- tags: {border: "*"} - tags: {border: "*"}
stroke: "#FF0000" stroke: "#FF0000"
@ -1002,7 +1095,7 @@ ways:
stroke: boundary_color stroke: boundary_color
stroke-width: 0.3 stroke-width: 0.3
stroke-dasharray: 10,5 stroke-dasharray: 10,5
layer: 60 priority: 60
tags_to_write: [ tags_to_write: [
"operator", "opening_hours", "cuisine", "network", "website", "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 import ui
from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID 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.osm_reader import OSMMember, OSMRelation, OSMWay, OSMNode
from roentgen.scheme import IconSet, Scheme from roentgen.scheme import IconSet, Scheme
from roentgen.util import MinMax from roentgen.util import MinMax
@ -19,20 +19,47 @@ from roentgen.util import MinMax
DEBUG: bool = False 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: class Node:
""" """
Node in Röntgen terms. Node in Röntgen terms.
""" """
def __init__( def __init__(
self, icon_set: IconSet, tags: Dict[str, str], 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): priority: int = 0, is_for_node: bool = True):
assert point is not None assert point is not None
self.icon_set: IconSet = icon_set self.icon_set: IconSet = icon_set
self.tags = tags self.tags = tags
self.point = point self.point: np.array = point
self.path = path self.coordinates: np.array = coordinates
self.priority = priority self.priority = priority
self.layer = 0 self.layer = 0
self.is_for_node = is_for_node self.is_for_node = is_for_node
@ -48,43 +75,50 @@ class Way:
Way in Röntgen terms. Way in Röntgen terms.
""" """
def __init__( def __init__(
self, kind: str, nodes: List[OSMNode], path, style: Dict[str, Any], self, kind: str, inners, outers, style: Dict[str, Any],
layer: float = 0.0, priority: float = 0, levels=None): layer: float = 0.0, levels=None):
assert nodes or path
self.kind = kind self.kind = kind
self.nodes: List[OSMNode] = nodes self.inners = inners
self.path = path self.outers = outers
self.style: Dict[str, Any] = style self.style: Dict[str, Any] = style
self.layer = layer self.layer = layer
self.priority = priority
self.levels = levels self.levels = levels
def get_path(
self, flinger: Flinger, shift: np.array = np.array((0, 0))) -> str:
"""
Get SVG path commands.
def get_float(string): :param shift: shift vector
""" """
Try to parse float from a string. path: str = ""
"""
try: for outer_nodes in self.outers:
return float(string) path += get_path(
except ValueError: make_counter_clockwise(outer_nodes), shift, flinger) + " "
return 0
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. Get geometric center of nodes set.
:param nodes: node list :param nodes: node list
:param flinger: flinger that remap geo positions :param flinger: flinger that remap geo positions
""" """
x, y = MinMax(), MinMax() boundary = [MinMax(), MinMax()]
for node in nodes: # type: OSMNode for node in nodes: # type: OSMNode
flung = flinger.fling(node.position) boundary[0].update(node.position[0])
x.update(flung[0]) boundary[1].update(node.position[1])
y.update(flung[1]) center_coordinates = np.array((boundary[0].center(), boundary[1].center()))
return np.array(((x.min_ + x.max_) / 2.0, (y.min_ + y.max_) / 2.0))
return flinger.fling(center_coordinates), center_coordinates
def get_user_color(text: str, seed: str): def get_user_color(text: str, seed: str):
@ -158,7 +192,7 @@ def glue(ways: List[OSMWay]) -> List[List[OSMNode]]:
return result 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. Construct SVG path from nodes.
""" """
@ -179,12 +213,15 @@ class Constructor:
""" """
Röntgen node and way 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.check_level = check_level
self.mode = mode self.mode = mode
self.seed = seed self.seed = seed
self.map_ = map_ self.map_ = map_
self.flinger = flinger self.flinger: Flinger = flinger
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
self.nodes: List[Node] = [] self.nodes: List[Node] = []
@ -194,44 +231,49 @@ class Constructor:
""" """
Construct Röntgen ways. Construct Röntgen ways.
""" """
way_number: int = 0
for way_id in self.map_.way_map: # type: int 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] way: OSMWay = self.map_.way_map[way_id]
if not self.check_level(way.tags): if not self.check_level(way.tags):
continue 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( def construct_way(
self, way: Optional[OSMWay], tags: Dict[str, Any], self, way: Optional[OSMWay], tags: Dict[str, Any],
path: Optional[str]) -> None: inners, outers) -> None:
""" """
Way construction. Way construction.
:param way: OSM way :param way: OSM way
:param tags: way tag dictionary :param tags: way tag dictionary
:param path: way path (if there is no nodes)
""" """
assert way or path
layer: float = 0 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 = 100 * level + 0.01 * layer
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
nodes = None nodes = None
center_point = None center_point, center_coordinates = None, None
if way: if way:
center_point = line_center(way.nodes, self.flinger) center_point, center_coordinates = \
line_center(way.nodes, self.flinger)
nodes = way.nodes nodes = way.nodes
if self.mode == "user-coloring": if self.mode == "user-coloring":
@ -239,7 +281,7 @@ class Constructor:
return return
user_color = get_user_color(way.user, self.seed) user_color = get_user_color(way.user, self.seed)
self.ways.append( self.ways.append(
Way("way", nodes, path, Way("way", inners, outers,
{"fill": "none", "stroke": user_color, {"fill": "none", "stroke": user_color,
"stroke-width": 1})) "stroke-width": 1}))
return return
@ -249,7 +291,7 @@ class Constructor:
return return
time_color = get_time_color(way.timestamp) time_color = get_time_color(way.timestamp)
self.ways.append( self.ways.append(
Way("way", nodes, path, Way("way", inners, outers,
{"fill": "none", "stroke": time_color, {"fill": "none", "stroke": time_color,
"stroke-width": 1})) "stroke-width": 1}))
return return
@ -261,7 +303,7 @@ class Constructor:
kind: str = "way" kind: str = "way"
levels = None levels = None
if "building" in tags: if "building" in tags: # or "building:part" in tags:
kind = "building" kind = "building"
if "building:levels" in tags: if "building:levels" in tags:
try: try:
@ -288,21 +330,31 @@ class Constructor:
break break
if matched: if matched:
style: Dict[str, Any] = {"fill": "none"} style: Dict[str, Any] = {"fill": "none"}
if "layer" in element: if "priority" in element:
layer += element["layer"] layer = element["priority"]
for key in element: # type: str 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] value = element[key]
if isinstance(value, str) and value.endswith("_color"): if isinstance(value, str) and value.endswith("_color"):
value = self.scheme.get_color(value) value = self.scheme.get_color(value)
style[key] = 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( 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 \ if center_point is not None and \
(way.is_cycle() or "area" in tags and tags["area"]): (way.is_cycle() or "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(Node( 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 appended = True
if not appended: if not appended:
@ -310,12 +362,13 @@ class Constructor:
style: Dict[str, Any] = { style: Dict[str, Any] = {
"fill": "none", "stroke": "#FF0000", "stroke-width": 1} "fill": "none", "stroke": "#FF0000", "stroke-width": 1}
self.ways.append(Way( self.ways.append(Way(
kind, nodes, path, style, layer, 50, levels)) kind, inners, outers, style, layer, levels))
if center_point is not None and way.is_cycle() or \ if center_point is not None and (way.is_cycle() or
"area" in tags and tags["area"]: "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(Node( 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: def construct_relations(self) -> None:
""" """
@ -327,45 +380,35 @@ class Constructor:
if not self.check_level(tags): if not self.check_level(tags):
continue continue
if "type" in tags and tags["type"] == "multipolygon": 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 for member in relation.members: # type: OSMMember
if member.type_ == "way": if member.type_ == "way":
if member.role == "inner": if member.role == "inner":
if member.ref in self.map_.way_map: 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": elif member.role == "outer":
if member.ref in self.map_.way_map: if member.ref in self.map_.way_map:
outers.append(self.map_.way_map[member.ref]) outer_ways.append(self.map_.way_map[member.ref])
p = "" inners_path: List[List[OSMNode]] = glue(inner_ways)
inners_path = glue(inners) outers_path: List[List[OSMNode]] = glue(outer_ways)
outers_path = glue(outers) self.construct_way(None, tags, inners_path, outers_path)
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)
def construct_nodes(self) -> None: def construct_nodes(self) -> None:
""" """
Draw nodes. Draw nodes.
""" """
print("Draw nodes...")
start_time = datetime.now()
node_number: int = 0 node_number: int = 0
s = sorted( s = sorted(
self.map_.node_map.keys(), 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 for node_id in s: # type: int
node_number += 1 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] node: OSMNode = self.map_.node_map[node_id]
flung = self.flinger.fling(node.position) flung = self.flinger.fling(node.position)
tags = node.tags tags = node.tags
@ -385,8 +428,6 @@ class Constructor:
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.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)) ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes")
print("Nodes painted in " + str(datetime.now() - start_time) + ".")

View file

@ -1,114 +1,56 @@
""" """
Author: Sergey Vartanov (me@enzet.ru) Author: Sergey Vartanov (me@enzet.ru)
""" """
import math
import numpy as np import numpy as np
from typing import Optional
from roentgen.util import MinMax
def get_ratio(maximum, minimum, ratio: float = 1): EQUATOR_LENGTH: float = 40_075_017
return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1])
def map_( def pseudo_mercator(coordinates: np.array) -> np.array:
value: float, current_min: float, current_max: float, target_min: float,
target_max: float):
""" """
Map current value in bounds of current_min and current_max to bounds of Use spherical pseudo-Mercator projection to convert geo coordinates into
target_min and target_max. plane.
:param coordinates: geo positional in the form of (latitude, longitude)
:return: position on the plane in the form of (x, y)
""" """
return \ return np.array((coordinates[1], 180 / np.pi * np.log(
target_min + (value - current_min) / (current_max - current_min) * \ np.tan(np.pi / 4 + coordinates[0] * (np.pi / 180) / 2))))
(target_max - target_min)
class Geo: class Flinger:
def __init__(self, lat: float, lon: float): """
self.lat: float = lat Convert geo coordinates into SVG position points.
self.lon: float = lon """
def __init__(self, geo_boundaries: MinMax, ratio: float = 1000):
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):
""" """
:param minimum: minimum latitude and longitude :param geo_boundaries: minimum and maximum 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
""" """
self.minimum = minimum self.geo_boundaries: MinMax = geo_boundaries
self.maximum = maximum 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
# Ratio is depended of latitude. It is always <= 1. In one latitude self.size: np.array = self.size.astype(int).astype(float)
# degree is always 40 000 / 360 km. In one current longitude degree is
# about 40 000 / 360 * ratio km.
ratio = math.sin( def fling(self, coordinates: np.array) -> np.array:
(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:
""" """
:param current: vector to fling :param coordinates: vector to fling
""" """
x = map_( result: np.array = self.ratio * (
current.lon, self.minimum.lon, self.maximum.lon, pseudo_mercator(coordinates) -
self.target_minimum[0], self.target_maximum[0]) pseudo_mercator(self.geo_boundaries.min_))
y = map_(
self.maximum.lat + self.minimum.lat - current.lat, # Invert y axis on coordinate plane.
self.minimum.lat, self.maximum.lat, result[1] = self.size[1] - result[1]
self.target_minimum[1], self.target_maximum[1])
return np.array([x, y]) 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 import ui
from roentgen.address import get_address from roentgen.address import get_address
from roentgen.constructor import Constructor, get_path, Node, Way from roentgen.constructor import Constructor, Node, Way
from roentgen.flinger import GeoFlinger, Geo 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
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.scheme import Scheme from roentgen.scheme import Scheme
from roentgen.direction import DirectionSet, Sector from roentgen.direction import DirectionSet, Sector
from roentgen.util import MinMax
ICONS_FILE_NAME: str = "icons/icons.svg" ICONS_FILE_NAME: str = "icons/icons.svg"
TAGS_FILE_NAME: str = "data/tags.yml" TAGS_FILE_NAME: str = "data/tags.yml"
@ -40,7 +41,7 @@ class Painter:
""" """
def __init__( def __init__(
self, show_missing_tags: bool, overlap: int, draw_nodes: bool, 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, svg: svgwrite.Drawing, icon_extractor: IconExtractor,
scheme: Scheme): scheme: Scheme):
@ -51,7 +52,7 @@ class Painter:
self.draw_captions: str = draw_captions self.draw_captions: str = draw_captions
self.map_: Map = map_ self.map_: Map = map_
self.flinger: GeoFlinger = flinger self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg self.svg: svgwrite.Drawing = svg
self.icon_extractor = icon_extractor self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
@ -241,54 +242,48 @@ class Painter:
else: else:
shift_2 = [0, -3] shift_2 = [0, -3]
if way.nodes: for nodes in way.inners + way.outers:
for i in range(len(way.nodes) - 1): for i in range(len(nodes) - 1):
flung_1 = self.flinger.fling(way.nodes[i].position) flung_1 = self.flinger.fling(nodes[i].position)
flung_2 = self.flinger.fling(way.nodes[i + 1].position) flung_2 = self.flinger.fling(nodes[i + 1].position)
self.svg.add(self.svg.path( self.svg.add(self.svg.path(
d=("M", np.add(flung_1, shift_1), "L", d=("M", np.add(flung_1, shift_1), "L",
np.add(flung_2, shift_1), np.add(flung_2, shift_2), np.add(flung_2, shift_1), np.add(flung_2, shift_2),
np.add(flung_1, shift_2), "Z"), np.add(flung_1, shift_2), "Z"),
fill=color, stroke=color, stroke_width=1)) 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. Draw map.
""" """
ways = sorted(ways, key=lambda x: x.layer) ways = sorted(ways, key=lambda x: x.layer)
for way in ways: for way in ways: # type: Way
if way.kind == "way": if way.kind == "way":
if way.nodes: path: str = way.get_path(self.flinger)
path = get_path(way.nodes, np.array([0, 0]), self.flinger) if path:
p = Path(d=path) p = Path(d=path)
p.update(way.style) p.update(way.style)
self.svg.add(p) self.svg.add(p)
else:
p = Path(d=way.path)
p.update(way.style)
self.svg.add(p)
# Building shade # Building shade
building_shade = Group(opacity=0.1) building_shade = Group(opacity=0.1)
for way in ways: # type: Way for way in ways: # type: Way
if way.kind != "building" or not way.nodes: if way.kind != "building":
continue continue
shift = [-5, 5] shift = [-5, 5]
if way.levels: if way.levels:
shift = [-5 * way.levels, 5 * way.levels] shift = [-5 * way.levels, 5 * way.levels]
for i in range(len(way.nodes) - 1): for nodes11 in way.inners + way.outers:
flung_1 = self.flinger.fling(way.nodes[i].position) for i in range(len(nodes11) - 1):
flung_2 = self.flinger.fling(way.nodes[i + 1].position) flung_1 = self.flinger.fling(nodes11[i].position)
building_shade.add(Path( flung_2 = self.flinger.fling(nodes11[i + 1].position)
("M", flung_1, "L", flung_2, np.add(flung_2, shift), building_shade.add(Path(
np.add(flung_1, shift), "Z"), ("M", flung_1, "L", flung_2, np.add(flung_2, shift),
fill="#000000", stroke="#000000", stroke_width=1)) np.add(flung_1, shift), "Z"),
fill="#000000", stroke="#000000", stroke_width=1))
self.svg.add(building_shade) self.svg.add(building_shade)
@ -300,21 +295,28 @@ class Painter:
# Building roof # Building roof
building_paths: List[(str, Dict)] = []
for way in ways: # type: Way for way in ways: # type: Way
if way.kind != "building": if way.kind != "building":
continue continue
if way.nodes: shift = [0, -3]
shift = [0, -3] if way.levels:
if way.levels: shift = np.array([0 * way.levels, min(-3, -1 * way.levels)])
shift = np.array([0 * way.levels, min(-3, -1 * way.levels)]) path: str = way.get_path(self.flinger, shift)
path = get_path(way.nodes, shift, self.flinger) if path:
p = Path(d=path, opacity=1) building_paths.append((path, way.style))
p.update(way.style)
self.svg.add(p) for path, style in building_paths:
else: p = Path(d=path, opacity=1)
p = Path(d=way.path, opacity=1) p.update(style)
p.update(way.style) p.update({"stroke": "none"})
self.svg.add(p) self.svg.add(p)
for path, style in building_paths:
p = Path(d=path, opacity=1)
p.update(style)
p.update({"fill": "none"})
self.svg.add(p)
# Trees # Trees
@ -326,12 +328,14 @@ class Painter:
if "circumference" in node.tags: if "circumference" in node.tags:
self.svg.add(self.svg.circle( self.svg.add(self.svg.circle(
node.point, 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)) fill="#AAAA88", opacity=0.3))
if "diameter_crown" in node.tags: if "diameter_crown" in node.tags:
self.svg.add(self.svg.circle( self.svg.add(self.svg.circle(
node.point, 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)) fill=self.scheme.get_color("evergreen"), opacity=0.3))
# Directions # Directions
@ -347,16 +351,19 @@ class Painter:
angle = float(node.get_tag("camera:angle")) angle = float(node.get_tag("camera:angle"))
if "angle" in node.tags: if "angle" in node.tags:
angle = float(node.get_tag("angle")) 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 = \ direction_color: str = \
self.scheme.get_color("direction_camera_color") self.scheme.get_color("direction_camera_color")
elif node.get_tag("traffic_sign") == "stop": elif node.get_tag("traffic_sign") == "stop":
direction = node.get_tag("direction") 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" direction_color: str = "#FF0000"
else: else:
direction = node.get_tag("direction") 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 = \ direction_color: str = \
self.scheme.get_color("direction_view_color") self.scheme.get_color("direction_view_color")
is_revert_gradient = True is_revert_gradient = True
@ -367,11 +374,9 @@ class Painter:
point = (node.point.astype(int)).astype(float) point = (node.point.astype(int)).astype(float)
if angle: if angle:
paths = [Sector(direction, angle) paths = [Sector(direction, angle).draw(point, direction_radius)]
.draw(point, direction_radius)]
else: else:
paths = DirectionSet(direction) \ paths = DirectionSet(direction).draw(point, direction_radius)
.draw(point, direction_radius)
for path in paths: for path in paths:
gradient = self.svg.defs.add(self.svg.radialGradient( gradient = self.svg.defs.add(self.svg.radialGradient(
@ -397,9 +402,9 @@ class Painter:
("diameter_crown" in node.tags or ("diameter_crown" in node.tags or
"circumference" in node.tags): "circumference" in node.tags):
continue 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) 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": if self.draw_captions == "no":
return return
@ -494,13 +499,13 @@ def check_level_overground(tags: Dict[str, Any]):
return True return True
def main(): def main(argv):
if len(sys.argv) == 2: if len(argv) == 2:
if sys.argv[1] == "grid": if argv[1] == "grid":
draw_grid() draw_grid()
return return
options = ui.parse_options(sys.argv) options = ui.parse_options(argv)
if not options: if not options:
sys.exit(1) sys.exit(1)
@ -538,23 +543,19 @@ def main():
map_: Map = osm_reader.map_ 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 = {} missing_tags = {}
points = [] points = []
scheme: Scheme = Scheme(TAGS_FILE_NAME, COLORS_FILE_NAME) scheme: Scheme = Scheme(TAGS_FILE_NAME, COLORS_FILE_NAME)
flinger: GeoFlinger = \ min1: np.array = np.array((boundary_box[1], boundary_box[0]))
GeoFlinger(min1, max1, np.array([0, 0]), np.array([w, h])) 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) icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
@ -589,21 +590,12 @@ def main():
scheme=scheme) scheme=scheme)
painter.draw(constructor.nodes, constructor.ways, points) 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: if options.show_index:
draw_index(flinger, map_, max1, min1, svg) 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.")
top_missing_tags = \ top_missing_tags = \
sorted(missing_tags.keys(), key=lambda x: -missing_tags[x]) sorted(missing_tags.keys(), key=lambda x: -missing_tags[x])
@ -615,13 +607,13 @@ def main():
def draw_index(flinger, map_, max1, min1, svg): def draw_index(flinger, map_, max1, min1, svg):
print(min1.lon, max1.lon) print(min1[1], max1[1])
print(min1.lat, max1.lat) print(min1[0], max1[0])
lon_step = 0.001 lon_step = 0.001
lat_step = 0.001 lat_step = 0.001
matrix = [] matrix = []
lat_number = int((max1.lat - min1.lat) / lat_step) + 1 lat_number = int((max1[0] - min1[0]) / lat_step) + 1
lon_number = int((max1.lon - min1.lon) / lon_step) + 1 lon_number = int((max1[1] - min1[1]) / lon_step) + 1
for i in range(lat_number): for i in range(lat_number):
row = [] row = []
for j in range(lon_number): for j in range(lon_number):
@ -629,8 +621,8 @@ def draw_index(flinger, map_, max1, min1, svg):
matrix.append(row) matrix.append(row)
for node_id in map_.node_map: # type: int for node_id in map_.node_map: # type: int
node = map_.node_map[node_id] node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step) i = int((node[0] - min1[0]) / lat_step)
j = int((node.lon - min1.lon) / lon_step) j = int((node[1] - min1[1]) / lon_step)
if (0 <= i < lat_number) and (0 <= j < lon_number): if (0 <= i < lat_number) and (0 <= j < lon_number):
matrix[i][j] += 1 matrix[i][j] += 1
if "tags" in node: if "tags" in node:
@ -640,18 +632,18 @@ def draw_index(flinger, map_, max1, min1, svg):
if "tags" in way: if "tags" in way:
for node_id in way.nodes: for node_id in way.nodes:
node = map_.node_map[node_id] node = map_.node_map[node_id]
i = int((node.lat - min1.lat) / lat_step) i = int((node[0] - min1[0]) / lat_step)
j = int((node.lon - min1.lon) / lon_step) j = int((node[1] - min1[1]) / lon_step)
if (0 <= i < lat_number) and (0 <= j < lon_number): if (0 <= i < lat_number) and (0 <= j < lon_number):
matrix[i][j] += len(way.tags) / float( matrix[i][j] += len(way.tags) / float(
len(way.nodes)) len(way.nodes))
for i in range(lat_number): for i in range(lat_number):
for j in range(lon_number): for j in range(lon_number):
t1 = flinger.fling(Geo( t1 = flinger.fling(np.array((
min1.lat + i * lat_step, min1.lon + j * lon_step)) min1[0] + i * lat_step, min1[1] + j * lon_step)))
t2 = flinger.fling(Geo( t2 = flinger.fling(np.array((
min1.lat + (i + 1) * lat_step, min1[0] + (i + 1) * lat_step,
min1.lon + (j + 1) * lon_step)) min1[1] + (j + 1) * lon_step)))
svg.add(Text( svg.add(Text(
str(int(matrix[i][j])), str(int(matrix[i][j])),
(((t1 + t2) * 0.5)[0], ((t1 + t2) * 0.5)[1] + 40), (((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). Author: Sergey Vartanov (me@enzet.ru).
""" """
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
from roentgen.flinger import Geo
from roentgen.ui import progress_bar from roentgen.ui import progress_bar
from roentgen.util import MinMax from roentgen.util import MinMax
@ -21,7 +21,7 @@ class OSMNode:
""" """
def __init__(self): def __init__(self):
self.id_: Optional[int] = None self.id_: Optional[int] = None
self.position: Optional[Geo] = None self.position: Optional[np.array] = None
self.tags: Dict[str, str] = {} self.tags: Dict[str, str] = {}
self.visible: Optional[str] = None self.visible: Optional[str] = None
@ -38,8 +38,8 @@ class OSMNode:
:param is_full: if false, parse only ID, latitude and longitude :param is_full: if false, parse only ID, latitude and longitude
""" """
self.id_ = int(get_value("id", text)) self.id_ = int(get_value("id", text))
self.position = Geo( self.position = np.array((
float(get_value("lat", text)), float(get_value("lon", text))) float(get_value("lat", text)), float(get_value("lon", text))))
if is_full: if is_full:
self.visible = get_value("visible", text) self.visible = get_value("visible", text)

View file

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

View file

@ -2,9 +2,9 @@ class MinMax:
""" """
Minimum and maximum. Minimum and maximum.
""" """
def __init__(self): def __init__(self, min_=None, max_=None):
self.min_ = None self.min_ = min_
self.max_ = None self.max_ = max_
def update(self, value): def update(self, value):
""" """
@ -18,3 +18,6 @@ class MinMax:
Difference between maximum and minimum. Difference between maximum and minimum.
""" """
return self.max_ - self.min_ 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). Author: Sergey Vartanov (me@enzet.ru).
""" """
import sys
from roentgen.mapper import main from roentgen.mapper import main
if __name__ == "__main__": if __name__ == "__main__":
main() main(sys.argv)

View file

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