Refactor way and relation processing.

This commit is contained in:
Sergey Vartanov 2020-09-25 23:38:25 +03:00
parent bbc28b4c61
commit 5506eb3082
7 changed files with 256 additions and 310 deletions

View file

@ -19,6 +19,7 @@ colors:
direction_view_color: "E0F0FF" direction_view_color: "E0F0FF"
direction_camera_color: "0088FF" direction_camera_color: "0088FF"
allotments_color: "D0E0D0"
beach_color: "F0E0C0" beach_color: "F0E0C0"
boundary_color: "880088" boundary_color: "880088"
building_border_color: "E0D0C0" # "AAAAAA" building_border_color: "E0D0C0" # "AAAAAA"
@ -28,11 +29,13 @@ colors:
desert_color: "F0E0D0" desert_color: "F0E0D0"
emergency_color: "DD2222" emergency_color: "DD2222"
farmland_color: "FFEEBB" farmland_color: "FFEEBB"
ferry_terminal_color: "AABBDD"
foot_border_color: "FFFFFF" foot_border_color: "FFFFFF"
foot_color: "B89A74" foot_color: "B89A74"
grass_border_color: "BFD098" grass_border_color: "BFD098"
grass_color: "CFE0A8" grass_color: "CFE0A8"
guide_strips_color: "228833" guide_strips_color: "228833"
hidden_color: "000000"
indoor_border_color: "C0B8B0" indoor_border_color: "C0B8B0"
indoor_color: "E8E4E0" indoor_color: "E8E4E0"
meadow_border_color: "BFD078" meadow_border_color: "BFD078"
@ -40,6 +43,8 @@ colors:
orchard_color: "B8DCA4" orchard_color: "B8DCA4"
outline_color: "FFFFFF" outline_color: "FFFFFF"
parking_color: "DDCC99" parking_color: "DDCC99"
pitch_color: "AADDCC"
pitch_border_color: "88BBAA"
platform_border_color: "AAAAAA" platform_border_color: "AAAAAA"
platform_color: "CCCCCC" platform_color: "CCCCCC"
playground_border_color: "663300" playground_border_color: "663300"
@ -69,7 +74,7 @@ colors:
"rose": "FF007F" # Wikipedia "rose": "FF007F" # Wikipedia
"slate_blue": "6A5ACD" # W3C slateblue "slate_blue": "6A5ACD" # W3C slateblue
nodes: node_icons:
# No draw # No draw
@ -281,6 +286,8 @@ nodes:
icon: [garages] icon: [garages]
- tags: {tourism: hotel} - tags: {tourism: hotel}
icon: [bed] icon: [bed]
- tags: {building: hotel}
icon: [bed]
- tags: {tourism: hostel} - tags: {tourism: hostel}
icon: [two_beds] icon: [two_beds]
- tags: {tourism: motel} # Tourism? - tags: {tourism: motel} # Tourism?
@ -295,12 +302,14 @@ nodes:
icon: [toy_horse] icon: [toy_horse]
- tags: {building: kindergarten, amenity: kindergarten} - tags: {building: kindergarten, amenity: kindergarten}
icon: [toy_horse] icon: [toy_horse]
- tags: {amenity: school, __country: Japan} - tags: {building: office}
icon: [japan_elementary_school] icon: [briefcase]
- tags: {amenity: post_office} - tags: {amenity: post_office}
icon: [letter] icon: [letter]
- tags: {amenity: post_office, __country: Japan} - tags: {amenity: post_office, __country: Japan}
icon: [japan_post] icon: [japan_post]
- tags: {amenity: school, __country: Japan}
icon: [japan_elementary_school]
- tags: {office: telecommunication} - tags: {office: telecommunication}
icon: [telephone] icon: [telephone]
@ -692,6 +701,22 @@ nodes:
- tags: {"payment:credit_cards": "yes"} - tags: {"payment:credit_cards": "yes"}
add_icon: [credit_card] add_icon: [credit_card]
line_icons:
- tags: {building: hotel}
icon: [bed]
- tags: {building: office}
icon: [briefcase]
- tags: {leisure: playground}
icon: [toy_horse]
- tags: {amenity: ferry_terminal}
icon: [anchor]
- tags: {amenity: parking}
icon: [parking]
- tags: {aeroway: landingpad}
icon: [booster_landing]
- tags: {aeroway: helipad}
icon: [h]
ways: ways:
- tags: {indoor: area} - tags: {indoor: area}
stroke: indoor_border_color stroke: indoor_border_color
@ -753,6 +778,8 @@ ways:
priority: 21 priority: 21
stroke: wood_color stroke: wood_color
stroke-width: 5 stroke-width: 5
stroke-linecap: round
stroke-linejoin: round
- tags: {natural: water} - tags: {natural: water}
fill: water_color fill: water_color
# stroke: water_border_color # stroke: water_border_color
@ -773,6 +800,9 @@ ways:
- tags: {natural: scree} - tags: {natural: scree}
fill: scree_color fill: scree_color
- tags: {landuse: allotments}
fill: allotments_color
priority: 20
- tags: {landuse: conservation} - tags: {landuse: conservation}
fill: grass_color fill: grass_color
priority: 20 priority: 20
@ -803,17 +833,20 @@ ways:
# Hidden land use # Hidden land use
- tags: {landuse: residential} - tags: {landuse: residential}
fill: none fill: hidden_color
stroke: none opacity: 0.05
- tags: {landuse: commercial} - tags: {landuse: commercial}
fill: none fill: hidden_color
stroke: none opacity: 0.05
- tags: {landuse: military} - tags: {landuse: military}
fill: none fill: hidden_color
stroke: none opacity: 0.05
- tags: {landuse: railway} - tags: {landuse: railway}
fill: none fill: hidden_color
stroke: none opacity: 0.05
- tags: {landuse: industrial}
fill: hidden_color
opacity: 0.05
- tags: {building: "*"} - tags: {building: "*"}
fill: building_color fill: building_color
@ -822,15 +855,19 @@ ways:
# fill: building_color # fill: building_color
# stroke: building_border_color # stroke: building_border_color
- tags: {amenity: ferry_terminal}
fill: ferry_terminal_color
priority: 50
- tags: {amenity: parking} - tags: {amenity: parking}
fill: parking_color fill: parking_color
opacity: 0.5 opacity: 0.5
icon: parking
- tags: {aeroway: landingpad} - tags: {aeroway: landingpad}
fill: "#000000" fill: "#000000"
opacity: 0.1 opacity: 0.1
icon: parking - tags: {aeroway: helipad}
fill: "#440044"
opacity: 0.1
- tags: {waterway: riverbank} - tags: {waterway: riverbank}
fill: none # water_color fill: none # water_color
@ -859,68 +896,81 @@ ways:
r2: 15 r2: 15
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: trunk} - tags: {highway: trunk}
r2: 13 r2: 13
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: primary} - tags: {highway: primary}
r2: 11 r2: 11
stroke: primary_border_color stroke: primary_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41.9 priority: 41.9
- tags: {highway: motorway_link} - tags: {highway: motorway_link}
r2: 9 r2: 9
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: secondary} - tags: {highway: secondary}
r2: 9 r2: 9
stroke: secondary_border_color stroke: secondary_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41.8 priority: 41.8
- tags: {highway: tertiary} - tags: {highway: tertiary}
r2: 7 r2: 7
stroke: tertiary_border_color stroke: tertiary_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41.7 priority: 41.7
- tags: {highway: unclassified} - tags: {highway: unclassified}
r2: 5 r2: 5
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: residential} - tags: {highway: residential}
r2: 5 r2: 5
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: living_street} - tags: {highway: living_street}
r2: 4 r2: 4
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: service} - tags: {highway: service}
no_tags: {service: parking_aisle} no_tags: {service: parking_aisle}
r2: 3 r2: 3
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: service, service: parking_aisle} - tags: {highway: service, service: parking_aisle}
r2: 2 r2: 2
stroke: road_border_color stroke: road_border_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: track} - tags: {highway: track}
stroke-width: 1.5 stroke-width: 1.5
stroke: track_color stroke: track_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 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
stroke-linejoin: round
priority: 41 priority: 41
- tags: {highway: steps} - tags: {highway: steps}
stroke-width: 6 stroke-width: 6
@ -935,69 +985,82 @@ ways:
r: 15 r: 15
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: trunk} - tags: {highway: trunk}
r: 13 r: 13
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: primary} - tags: {highway: primary}
r: 11 r: 11
stroke: primary_color stroke: primary_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42.9 priority: 42.9
- tags: {highway: secondary} - tags: {highway: secondary}
r: 9 r: 9
stroke: secondary_color stroke: secondary_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42.8 priority: 42.8
- tags: {highway: motorway_link} - tags: {highway: motorway_link}
r: 9 r: 9
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: tertiary} - tags: {highway: tertiary}
r: 7 r: 7
stroke: tertiary_color stroke: tertiary_color
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42.7 priority: 42.7
- tags: {highway: unclassified} - tags: {highway: unclassified}
r: 5 r: 5
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: residential} - tags: {highway: residential}
r: 5 r: 5
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: living_street} - tags: {highway: living_street}
r: 4 r: 4
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: service} - tags: {highway: service}
no_tags: {service: parking_aisle} no_tags: {service: parking_aisle}
r: 3 r: 3
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 42 priority: 42
- tags: {highway: service, service: parking_aisle} - tags: {highway: service, service: parking_aisle}
r: 2 r: 2
stroke: "#FFFFFF" stroke: "#FFFFFF"
stroke-linecap: round stroke-linecap: round
stroke-linejoin: round
priority: 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-linejoin: round
stroke: foot_color stroke: foot_color
priority: 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
stroke-linejoin: round
priority: -55 # FIXME priority: -55 # FIXME
- tags: {highway: cycleway} - tags: {highway: cycleway}
no_tags: {area: "yes"} no_tags: {area: "yes"}
@ -1040,15 +1103,13 @@ ways:
fill: grass_color fill: grass_color
opacity: 0.5 opacity: 0.5
- tags: {leisure: pitch} - tags: {leisure: pitch}
fill: playground_color fill: pitch_color
stroke: playground_border_color stroke: pitch_border_color
stroke-width: 1 stroke-width: 1
opacity: 0.2
priority: 21 priority: 21
- tags: {leisure: playground} - tags: {leisure: playground}
fill: playground_color fill: playground_color
opacity: 0.2 opacity: 0.2
icon: toy_horse
priority: 21 priority: 21
- tags: {leisure: swimming_pool} - tags: {leisure: swimming_pool}
fill: water_color fill: water_color
@ -1064,31 +1125,31 @@ ways:
fill: none fill: none
stroke: "#000000" stroke: "#000000"
stroke-width: 1 stroke-width: 1
opacity: 0.6 opacity: 0.35
priority: 40 priority: 40
- tags: {barrier: wall} - tags: {barrier: wall}
fill: none fill: none
stroke: "#000000" stroke: "#000000"
stroke-width: 1 stroke-width: 1
opacity: 0.5 opacity: 0.3
priority: 40 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.25
priority: 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.2
priority: 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.15
priority: 40 priority: 40
- tags: {border: "*"} - tags: {border: "*"}
@ -1138,9 +1199,7 @@ tags_to_skip: [
"FIXME", "source_ref", "naptan:verified:note", "fixme", "FIXME", "source_ref", "naptan:verified:note", "fixme",
"building:levels", "ref:opendataparis:adresse", "indoor", "building:levels", "ref:opendataparis:adresse", "indoor",
"level:ref", "ref:opendataparis:geo_point_2d", "created_by", "level:ref", "ref:opendataparis:geo_point_2d", "created_by",
"mapillary", "diameter_crown", "attribution"] "mapillary", "diameter_crown", "attribution", "curve_geometry"]
prefix_to_skip: [ prefix_to_skip: [
"source", "mapillary"] "source", "mapillary"]

View file

@ -3,6 +3,7 @@ Construct Röntgen nodes and ways.
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
from collections import Counter
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from hashlib import sha256 from hashlib import sha256
@ -13,14 +14,14 @@ import numpy as np
from roentgen import ui from roentgen import ui
from roentgen.color import get_gradient_color from roentgen.color import get_gradient_color
from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID from roentgen.icon import DEFAULT_SMALL_SHAPE_ID
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.osm_reader import ( from roentgen.osm_reader import (
Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged) Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged)
from roentgen.scheme import IconSet, Scheme from roentgen.scheme import IconSet, Scheme, LineStyle
from roentgen.util import MinMax from roentgen.util import MinMax
DEBUG: bool = False DEBUG: bool = True
TIME_COLOR_SCALE: List[Color] = [ TIME_COLOR_SCALE: List[Color] = [
Color("#581845"), Color("#900C3F"), Color("#C70039"), Color("#FF5733"), Color("#581845"), Color("#900C3F"), Color("#C70039"), Color("#FF5733"),
Color("#FFC300"), Color("#DAF7A6")] Color("#FFC300"), Color("#DAF7A6")]
@ -86,16 +87,14 @@ class Figure(Tagged):
""" """
def __init__( def __init__(
self, tags: Dict[str, str], inners: List[List[OSMNode]], self, tags: Dict[str, str], inners: List[List[OSMNode]],
outers: List[List[OSMNode]], style: Dict[str, Any], outers: List[List[OSMNode]], line_style: LineStyle):
layer: float = 0.0):
super().__init__() super().__init__()
self.tags: Dict[str, str] = tags self.tags: Dict[str, str] = tags
self.inners: List[List[OSMNode]] = [] self.inners: List[List[OSMNode]] = []
self.outers: List[List[OSMNode]] = [] self.outers: List[List[OSMNode]] = []
self.style: Dict[str, Any] = style self.line_style = line_style
self.layer: float = layer
for inner_nodes in inners: for inner_nodes in inners:
self.inners.append(make_clockwise(inner_nodes)) self.inners.append(make_clockwise(inner_nodes))
@ -145,9 +144,9 @@ class Building(Figure):
""" """
def __init__( def __init__(
self, tags: Dict[str, str], inners, outers, flinger: Flinger, self, tags: Dict[str, str], inners, outers, flinger: Flinger,
style: Dict[str, Any], layer: float): line_style: LineStyle):
super().__init__(tags, inners, outers, style, layer) super().__init__(tags, inners, outers, line_style)
self.parts = [] self.parts = []
@ -268,6 +267,13 @@ def get_path(nodes: List[OSMNode], shift: np.array, flinger: Flinger) -> str:
return path return path
def is_cycle(nodes) -> bool:
"""
Is way a cycle way or an area boundary.
"""
return nodes[0] == nodes[-1]
class Constructor: class Constructor:
""" """
Röntgen node and way constructor. Röntgen node and way constructor.
@ -309,120 +315,74 @@ class Constructor:
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, [], [way.nodes]) self.construct_line(way, [], [way.nodes])
ui.progress_bar(-1, len(self.map_.way_map), text="Constructing ways") ui.progress_bar(-1, len(self.map_.way_map), text="Constructing ways")
def construct_way( def construct_line(
self, way: Optional[OSMWay], tags: Dict[str, Any], self, line: Optional[Tagged],
inners: List[List[OSMNode]], outers: List[List[OSMNode]]) -> None: inners: List[List[OSMNode]], outers: List[List[OSMNode]]) -> None:
""" """
Way construction. Way or relation construction.
:param way: OSM way :param line: OpenStreetMap way or relation
:param tags: way tag dictionary
:param inners: list of polygons that compose inner boundary :param inners: list of polygons that compose inner boundary
:param outers: list of polygons that compose outer boundary :param outers: list of polygons that compose outer boundary
""" """
layer: float = 0 assert len(outers) >= 1
center_point, center_coordinates = None, None line_is_cycle: bool = is_cycle(outers[0])
if way is not None:
center_point, center_coordinates = ( center_point, center_coordinates = (
line_center(way.nodes, self.flinger)) line_center(outers[0], self.flinger))
if self.mode == "user-coloring": if self.mode == "user-coloring":
if not way: user_color = get_user_color(line.user, self.seed)
return self.figures.append(Figure(
user_color = get_user_color(way.user, self.seed) line.tags, inners, outers,
self.figures.append( LineStyle({
Figure( "fill": "none", "stroke": user_color.hex,
way.tags, inners, outers, "stroke-width": 1})))
{"fill": "none", "stroke": user_color.hex,
"stroke-width": 1}))
return return
if self.mode == "time": if self.mode == "time":
if not way: time_color = get_time_color(line.timestamp, self.map_.time)
return self.figures.append(Figure(
time_color = get_time_color(way.timestamp, self.map_.time) line.tags, inners, outers,
self.figures.append( LineStyle({
Figure( "fill": "none", "stroke": time_color.hex,
way.tags, inners, outers, "stroke-width": 1})))
{"fill": "none", "stroke": time_color.hex,
"stroke-width": 1}))
return return
if not tags: if not line.tags:
return return
appended: bool = False scale: float = self.flinger.get_scale(center_coordinates)
for element in self.scheme.ways: # type: Dict[str, Any] line_styles: List[LineStyle] = self.scheme.get_style(line.tags, scale)
matched: bool = True
for config_tag_key in element["tags"]: # type: str for line_style in line_styles: # type: LineStyle
matcher = element["tags"][config_tag_key] if "building" in line.tags:
if config_tag_key not in tags or \
(matcher != "*" and
tags[config_tag_key] != matcher and
tags[config_tag_key] not in matcher):
matched = False
break
if "no_tags" in element:
for config_tag_key in element["no_tags"]: # type: str
if (config_tag_key in tags and
tags[config_tag_key] ==
element["no_tags"][config_tag_key]):
matched = False
break
if matched:
style: Dict[str, Any] = {"fill": "none"}
if "priority" in element:
layer = element["priority"]
for key in element: # type: str
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
if "building" in tags:
self.add_building(Building( self.add_building(Building(
tags, inners, outers, self.flinger, style, layer)) line.tags, inners, outers, self.flinger, line_style))
else: else:
self.figures.append( self.figures.append(
Figure(tags, inners, outers, style, layer)) Figure(line.tags, inners, outers, line_style))
if center_point is not None and \ icon_set: IconSet = self.scheme.get_icon(line.tags, for_="line")
(way.is_cycle() and "area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Point( self.nodes.append(Point(
icon_set, tags, center_point, center_coordinates, icon_set, line.tags, center_point, center_coordinates,
is_for_node=False)) is_for_node=False))
appended = True
if not appended: if not line_styles:
if DEBUG: if DEBUG:
style: Dict[str, Any] = { style: Dict[str, Any] = {
"fill": "none", "stroke": Color("red").hex, "fill": "none", "stroke": Color("red").hex,
"stroke-width": 1} "stroke-width": 1}
self.figures.append(Figure( self.figures.append(Figure(
tags, inners, outers, style, layer)) line.tags, inners, outers, LineStyle(style, 1000)))
if (center_point is not None and icon_set: IconSet = self.scheme.get_icon(line.tags)
way.is_cycle() and "area" in tags and tags["area"]):
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Point( self.nodes.append(Point(
icon_set, tags, center_point, center_coordinates, icon_set, line.tags, center_point, center_coordinates,
is_for_node=False)) is_for_node=False))
def construct_relations(self) -> None: def construct_relations(self) -> None:
@ -447,7 +407,7 @@ class Constructor:
outer_ways.append(self.map_.way_map[member.ref]) outer_ways.append(self.map_.way_map[member.ref])
inners_path: List[List[OSMNode]] = glue(inner_ways) inners_path: List[List[OSMNode]] = glue(inner_ways)
outers_path: List[List[OSMNode]] = glue(outer_ways) outers_path: List[List[OSMNode]] = glue(outer_ways)
self.construct_way(None, tags, inners_path, outers_path) self.construct_line(relation, inners_path, outers_path)
def construct_nodes(self) -> None: def construct_nodes(self) -> None:
""" """
@ -459,6 +419,8 @@ class Constructor:
self.map_.node_map.keys(), self.map_.node_map.keys(),
key=lambda x: -self.map_.node_map[x].coordinates[0]) key=lambda x: -self.map_.node_map[x].coordinates[0])
missing_tags = Counter()
for node_id in s: # type: int for node_id in s: # type: int
node_number += 1 node_number += 1
ui.progress_bar( ui.progress_bar(
@ -477,7 +439,6 @@ class Constructor:
if not tags: if not tags:
continue continue
icon_set.icons = [[DEFAULT_SMALL_SHAPE_ID]] icon_set.icons = [[DEFAULT_SMALL_SHAPE_ID]]
break
if self.mode == "user-coloring": if self.mode == "user-coloring":
icon_set.color = get_user_color(node.user, self.seed) icon_set.color = get_user_color(node.user, self.seed)
if self.mode == "time": if self.mode == "time":
@ -485,4 +446,11 @@ class Constructor:
self.nodes.append(Point(icon_set, tags, flung, node.coordinates)) self.nodes.append(Point(icon_set, tags, flung, node.coordinates))
missing_tags.update(
f"{key}: {tags[key]}" for key in tags
if key not in icon_set.processed)
for t in missing_tags.most_common():
print(t)
ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes") ui.progress_bar(-1, len(self.map_.node_map), text="Constructing nodes")

View file

@ -1,124 +0,0 @@
"""
Extract icons from SVG file.
Author: Sergey Vartanov (me@enzet.ru).
"""
import re
import xml.dom.minidom
from typing import Dict
from xml.dom.minidom import Element, Node
import numpy as np
from svgwrite import Drawing
from roentgen import ui
DEFAULT_SHAPE_ID: str = "default"
DEFAULT_SMALL_SHAPE_ID: str = "default_small"
STANDARD_INKSCAPE_ID: str = "(path|rect)\\d*"
GRID_STEP: int = 16
class Icon:
"""
SVG icon path description.
"""
def __init__(self, path: str, offset: np.array, id_: str):
"""
:param path: SVG icon path
:param offset: vector that should be used to shift the path
:param id_: shape identifier
"""
assert path
self.path: str = path
self.offset: np.array = offset
self.id_: str = id_
def is_default(self) -> bool:
"""
Return true if icon is has a default shape that doesn't represent
anything.
"""
return self.id_ in [DEFAULT_SHAPE_ID, DEFAULT_SMALL_SHAPE_ID]
def get_path(self, svg: Drawing, point: np.array):
"""
Draw icon into SVG file.
:param svg: SVG file to draw to
:param point: icon position
"""
shift: np.array = self.offset + point
return svg.path(
d=self.path, transform=f"translate({shift[0]},{shift[1]})")
class IconExtractor:
"""
Extract icons from SVG file.
Icon is a single path with "id" attribute that aligned to 16×16 grid.
"""
def __init__(self, svg_file_name: str):
"""
:param svg_file_name: input SVG file name with icons. File may contain
any other irrelevant graphics.
"""
self.icons: Dict[str, Icon] = {}
with open(svg_file_name) as input_file:
content = xml.dom.minidom.parse(input_file)
for element in content.childNodes:
if element.nodeName == "svg":
for node in element.childNodes: # type: Node
if isinstance(node, Element):
self.parse(node)
def parse(self, node: Element) -> None:
"""
Extract icon paths into a map.
:param node: XML node that contains icon
"""
if node.nodeName == "g":
for sub_node in node.childNodes:
if isinstance(sub_node, Element):
self.parse(sub_node)
return
if ("id" in node.attributes.keys() and
"d" in node.attributes.keys() and
node.attributes["id"].value):
path: str = node.attributes["d"].value
matcher = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path)
if not matcher:
ui.error(f"invalid path: {path}")
return
def get_offset(value: float):
""" Get negated icon offset from the origin. """
return -int(value / GRID_STEP) * GRID_STEP - GRID_STEP / 2
point: np.array = np.array((
get_offset(float(matcher.group(1))),
get_offset(float(matcher.group(2)))))
id_: str = node.attributes["id"].value
matcher = re.match(STANDARD_INKSCAPE_ID, id_)
if not matcher:
self.icons[id_] = Icon(node.attributes["d"].value, point, id_)
def get_path(self, id_: str) -> (Icon, bool):
"""
Get SVG path of the icon.
:param id_: string icon identifier
"""
if id_ in self.icons:
return self.icons[id_], True
ui.error(f"no such icon ID {id_}")
return self.icons[DEFAULT_SHAPE_ID], False

View file

@ -7,7 +7,7 @@ import numpy as np
from svgwrite import Drawing from svgwrite import Drawing
from typing import List, Dict, Any, Set from typing import List, Dict, Any, Set
from roentgen.extract_icon import Icon, IconExtractor from roentgen.icon import Icon, IconExtractor
from roentgen.scheme import Scheme from roentgen.scheme import Scheme
@ -30,7 +30,7 @@ def draw_grid(step: float = 24, columns: int = 16):
to_draw: List[Set[str]] = [] to_draw: List[Set[str]] = []
for element in scheme.nodes: # type: Dict[str, Any] for element in scheme.node_icons + scheme.line_icons: # type: Dict[str, Any]
if "icon" in element: if "icon" in element:
if set(element["icon"]) not in to_draw: if set(element["icon"]) not in to_draw:
to_draw.append(set(element["icon"])) to_draw.append(set(element["icon"]))

View file

@ -23,7 +23,7 @@ from roentgen.constructor import (
Constructor, Point, Figure, TextStruct, Building, Segment) Constructor, Point, Figure, TextStruct, Building, Segment)
from roentgen.flinger import Flinger from roentgen.flinger import Flinger
from roentgen.grid import draw_grid from roentgen.grid import draw_grid
from roentgen.extract_icon import Icon, IconExtractor from roentgen.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
@ -114,16 +114,6 @@ class Painter:
text, (node.point[0], node.point[1] + text_y + 8), text, (node.point[0], node.point[1] + text_y + 8),
text_struct.fill, size=text_struct.size) text_struct.fill, size=text_struct.size)
if self.show_missing_tags:
for tag in node.tags: # type: str
if not self.scheme.is_no_drawable(tag) and \
tag not in node.icon_set.processed:
text = f"{tag}: {node.tags[tag]}"
self.draw_text(
text, (node.point[0], node.point[1] + text_y + 18),
Color("#734A08"))
text_y += 10
def draw_text( def draw_text(
self, text: str, point, fill: Color, size: float = 10, self, text: str, point, fill: Color, size: float = 10,
out_fill=Color("white"), out_opacity=1.0, out_fill=Color("white"), out_opacity=1.0,
@ -232,14 +222,14 @@ class Painter:
""" """
Draw map. Draw map.
""" """
ways = sorted(constructor.figures, key=lambda x: x.layer) ways = sorted(constructor.figures, key=lambda x: x.line_style.priority)
ways_length: int = len(ways) ways_length: int = len(ways)
for index, way in enumerate(ways): # type: Figure for index, way in enumerate(ways): # type: Figure
ui.progress_bar(index, ways_length, step=10, text="Drawing ways") ui.progress_bar(index, ways_length, step=10, text="Drawing ways")
path_commands: str = way.get_path(self.flinger) path_commands: str = way.get_path(self.flinger)
if path_commands: if path_commands:
path = Path(d=path_commands) path = Path(d=path_commands)
path.update(way.style) path.update(way.line_style.style)
self.svg.add(path) self.svg.add(path)
ui.progress_bar(-1, 0, text="Drawing ways") ui.progress_bar(-1, 0, text="Drawing ways")
@ -301,7 +291,7 @@ class Painter:
shift = np.array([0, -way.get_levels() * height]) shift = np.array([0, -way.get_levels() * height])
path_commands: str = way.get_path(self.flinger, shift) path_commands: str = way.get_path(self.flinger, shift)
path = Path(d=path_commands, opacity=1) path = Path(d=path_commands, opacity=1)
path.update(way.style) path.update(way.line_style.style)
path.update({"stroke-linejoin": "round"}) path.update({"stroke-linejoin": "round"})
self.svg.add(path) self.svg.add(path)
@ -463,7 +453,7 @@ def check_level_number(tags: Dict[str, Any], level: float):
return True return True
def check_level_overground(tags: Dict[str, Any]): def check_level_overground(tags: Dict[str, Any]) -> bool:
""" """
Check if element described by tags is overground. Check if element described by tags is overground.
""" """
@ -548,7 +538,7 @@ def main(argv) -> None:
icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME) icon_extractor: IconExtractor = IconExtractor(ICONS_FILE_NAME)
def check_level(x): def check_level(x) -> bool:
""" Draw objects on all levels. """ """ Draw objects on all levels. """
return True return True
@ -556,11 +546,11 @@ def main(argv) -> None:
if options.level == "overground": if options.level == "overground":
check_level = check_level_overground check_level = check_level_overground
elif options.level == "underground": elif options.level == "underground":
def check_level(x): def check_level(x) -> bool:
""" Draw underground objects. """ """ Draw underground objects. """
return not check_level_overground(x) return not check_level_overground(x)
else: else:
def check_level(x): def check_level(x) -> bool:
""" Draw objects on the specified level. """ """ Draw objects on the specified level. """
return not check_level_number(x, float(options.level)) return not check_level_number(x, float(options.level))
@ -582,11 +572,3 @@ def main(argv) -> None:
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 = \
sorted(missing_tags.keys(), key=lambda x: -missing_tags[x])
missing_tags_file = open(MISSING_TAGS_FILE_NAME, "w+")
for tag in top_missing_tags:
missing_tags_file.write(
f'- {{tag: "{tag}", count: {missing_tags[tag]}}}\n')
missing_tags_file.close()

View file

@ -146,6 +146,8 @@ class OSMRelation(Tagged):
self.id_: int = id_ self.id_: int = id_
self.members: List["OSMMember"] = [] self.members: List["OSMMember"] = []
self.user: Optional[str] = None
self.timestamp: Optional[datetime] = None
def parse_from_xml(self, text: str) -> "OSMRelation": def parse_from_xml(self, text: str) -> "OSMRelation":
""" """
@ -155,6 +157,10 @@ class OSMRelation(Tagged):
""" """
self.id_ = int(get_value("id", text)) self.id_ = int(get_value("id", text))
self.user = get_value("user", text)
self.timestamp = datetime.strptime(
get_value("timestamp", text), OSM_TIME_PATTERN)
return self return self

View file

@ -8,9 +8,9 @@ import yaml
from colour import Color from colour import Color
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set, Union
from roentgen.extract_icon import DEFAULT_SHAPE_ID from roentgen.icon import DEFAULT_SHAPE_ID
DEFAULT_COLOR: Color = Color("#444444") DEFAULT_COLOR: Color = Color("#444444")
@ -28,6 +28,12 @@ class IconSet:
is_default: bool is_default: bool
@dataclass
class LineStyle:
style: Dict[str, Union[int, float, str]]
priority: float = 0.0
class Scheme: class Scheme:
""" """
Map style. Map style.
@ -43,7 +49,9 @@ class Scheme:
content: Dict[str, Any] = yaml.load( content: Dict[str, Any] = yaml.load(
input_file.read(), Loader=yaml.FullLoader) input_file.read(), Loader=yaml.FullLoader)
self.nodes: List[Dict[str, Any]] = content["nodes"] self.node_icons: List[Dict[str, Any]] = content["node_icons"]
self.line_icons: List[Dict[str, Any]] = content["line_icons"]
self.ways: List[Dict[str, Any]] = content["ways"] self.ways: List[Dict[str, Any]] = content["ways"]
self.colors: Dict[str, str] = content["colors"] self.colors: Dict[str, str] = content["colors"]
@ -83,7 +91,7 @@ class Scheme:
if key in self.tags_to_write or key in self.tags_to_skip: if key in self.tags_to_write or key in self.tags_to_skip:
return True return True
for prefix in self.prefix_to_write + self.prefix_to_skip: # type: str for prefix in self.prefix_to_write + self.prefix_to_skip: # type: str
if key[:len(prefix) + 1] == prefix + ":": if key[:len(prefix) + 1] == f"{prefix}:":
return True return True
return False return False
@ -94,24 +102,23 @@ class Scheme:
:param key: OpenStreetMap tag key :param key: OpenStreetMap tag key
""" """
if key in self.tags_to_skip: if key in self.tags_to_skip: # type: str
return False return False
if key in self.tags_to_write: if key in self.tags_to_write: # type: str
return True return True
for prefix in self.prefix_to_write: for prefix in self.prefix_to_write: # type: str
if key[:len(prefix) + 1] == prefix + ":": if key[:len(prefix) + 1] == f"{prefix}:":
return True return True
return False return False
def get_icon(self, tags: Dict[str, Any]) -> IconSet: def get_icon(self, tags: Dict[str, Any], for_: str = "node") -> IconSet:
""" """
Construct icon set. Construct icon set.
:param tags: OpenStreetMap element tags dictionary :param tags: OpenStreetMap element tags dictionary
""" """
tags_hash: str = \ tags_hash: str = (
",".join(tags.keys()) + ":" + \ ",".join(tags.keys()) + ":" + ",".join(map(str, tags.values())))
",".join(map(lambda x: str(x), tags.values()))
if tags_hash in self.cache: if tags_hash in self.cache:
return self.cache[tags_hash] return self.cache[tags_hash]
@ -121,14 +128,16 @@ class Scheme:
processed: Set[str] = set() processed: Set[str] = set()
fill: Color = DEFAULT_COLOR fill: Color = DEFAULT_COLOR
for matcher in self.nodes: # type: Dict[str, Any] rules = self.node_icons if for_ == "node" else self.line_icons
for matcher in rules: # type: Dict[str, Any]
matched: bool = True matched: bool = True
for key in matcher["tags"]: # type: str for key in matcher["tags"]: # type: str
if key not in tags: if key not in tags:
matched = False matched = False
break break
if matcher["tags"][key] != "*" and \ if (matcher["tags"][key] != "*" and
matcher["tags"][key] != tags[key]: matcher["tags"][key] != tags[key]):
matched = False matched = False
break break
if "no_tags" in matcher: if "no_tags" in matcher:
@ -136,7 +145,8 @@ class Scheme:
if no_tag in tags.keys(): if no_tag in tags.keys():
matched = False matched = False
break break
if matched: if not matched:
continue
if "draw" in matcher and not matcher["draw"]: if "draw" in matcher and not matcher["draw"]:
processed |= set(matcher["tags"].keys()) processed |= set(matcher["tags"].keys())
if "icon" in matcher: if "icon" in matcher:
@ -157,8 +167,8 @@ class Scheme:
processed.add(key) processed.add(key)
for tag_key in tags: # type: str for tag_key in tags: # type: str
if tag_key in ["color", "colour"] or tag_key.endswith(":color") or \ if (tag_key in ["color", "colour"] or tag_key.endswith(":color") or
tag_key.endswith(":colour"): tag_key.endswith(":colour")):
fill = self.get_color(tags[tag_key]) fill = self.get_color(tags[tag_key])
processed.add(tag_key) processed.add(tag_key)
@ -181,3 +191,48 @@ class Scheme:
self.cache[tags_hash] = returned self.cache[tags_hash] = returned
return returned return returned
def get_style(self, tags: Dict[str, Any], scale):
line_styles = []
for element in self.ways: # type: Dict[str, Any]
priority = 0
matched: bool = True
for config_tag_key in element["tags"]: # type: str
matcher = element["tags"][config_tag_key]
if (config_tag_key not in tags or
(matcher != "*" and
tags[config_tag_key] != matcher and
tags[config_tag_key] not in matcher)):
matched = False
break
if "no_tags" in element:
for config_tag_key in element["no_tags"]: # type: str
if (config_tag_key in tags and
tags[config_tag_key] ==
element["no_tags"][config_tag_key]):
matched = False
break
if not matched:
continue
style: Dict[str, Any] = {"fill": "none"}
if "priority" in element:
priority = element["priority"]
for key in element: # type: str
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.get_color(value)
style[key] = value
if "r" in element:
style["stroke-width"] = (element["r"] * scale)
if "r2" in element:
style["stroke-width"] = (element["r2"] * scale + 2)
line_styles.append(LineStyle(style, priority))
return line_styles