Refactor constructor; change code style.

This commit is contained in:
Sergey Vartanov 2020-09-05 19:44:28 +03:00
parent 8509e3c0e2
commit 21be74a8a2
18 changed files with 759 additions and 683 deletions

4
.idea/Roentgen.iml generated
View file

@ -5,10 +5,6 @@
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
<component name="TestRunnerService"> <component name="TestRunnerService">
<option name="PROJECT_TEST_RUNNER" value="pytest" /> <option name="PROJECT_TEST_RUNNER" value="pytest" />
</component> </component>

View file

@ -18,6 +18,7 @@ colors:
outline_color: "FFFFFF" outline_color: "FFFFFF"
beach_color: "F0E0C0" beach_color: "F0E0C0"
boundary_color: "880088"
building_color: "F8F0E8" # "D0D0C0" building_color: "F8F0E8" # "D0D0C0"
building_border_color: "DDDDDD" # "AAAAAA" building_border_color: "DDDDDD" # "AAAAAA"
construction_color: "CCCCCC" construction_color: "CCCCCC"
@ -232,6 +233,8 @@ tags:
icon: [cupcake] icon: [cupcake]
- tags: {shop: mall} - tags: {shop: mall}
icon: [bag] icon: [bag]
- tags: {shop: alcohol}
icon: [bottle]
- tags: {shop: mall, building: 'yes'} - tags: {shop: mall, building: 'yes'}
icon: [bag] icon: [bag]
- tags: {shop: convenience} - tags: {shop: convenience}
@ -527,6 +530,291 @@ tags:
- tags: {'payment:credit_cards': 'yes'} - tags: {'payment:credit_cards': 'yes'}
add_icon: [credit_card] add_icon: [credit_card]
ways:
- tags: {indoor: area}
stroke: indoor_border_color
stroke-width: 1
fill: indoor_color
layer: 10
- tags: {indoor: corridor}
stroke: indoor_color
stroke-width: 1
fill: indoor_color
layer: 11
- tags: {indoor: ["yes", room, elevator]}
stroke: indoor_color
stroke-width: 1
fill: indoor_color
layer: 12
- tags: {indoor: column}
stroke: indoor_color
stroke-width: 1
fill: indoor_color
layer: 13
- tags: {natural: wood}
fill: wood_color
layer: 21
- tags: {natural: grassland}
fill: grass_color
stroke: grass_border_color
layer: 20
- tags: {natural: scrub}
fill: wood_color
layer: 21
- tags: {natural: sand}
fill: sand_color
layer: 20
- tags: {natural: beach}
fill: beach_color
layer: 20
- tags: {natural: desert}
fill: desert_color
layer: 20
- tags: {natural: forest}
fill: wood_color
layer: 21
- tags: {natural: tree_row}
layer: 21
stroke: wood_color
stroke-width: 5
- tags: {natural: water}
fill: water_color
stroke: water_border_color
stroke-width: 1
layer: 21
- tags: {landuse: grass}
fill: grass_color
layer: 20
stroke: grass_border_color
- tags: {landuse: conservation}
fill: grass_color
layer: 20
- tags: {landuse: forest}
fill: wood_color
layer: 20
- tags: {landuse: garages}
fill: parking_color
layer: 21
- tags: {landuse: construction}
fill: construction_color
- tags: {landuse: residential}
fill: none
stroke: none
- tags: {landuse: commercial}
fill: none
stroke: none
- tags: {building: "*"}
fill: building_color
stroke: building_border_color
- tags: {amenity: parking}
fill: parking_color
opacity: 0.5
icon: parking
- tags: {waterway: riverbank}
fill: water_color
stroke: water_border_color
stroke-width: 1
- tags: {railway: subway}
stroke-width: 10
stroke: "#DDDDDD"
layer: 41
- tags: {railway: [narrow_gauge, tram]}
stroke-width: 2
stroke: "#000000"
layer: 41
- tags: {railway: platform}
fill: platform_color
stroke-width: 1
stroke: platform_border_color
layer: 41
- tags: {highway: motorway}
stroke-width: 33
stroke: road_border_color
layer: 41
- tags: {highway: trunk}
stroke-width: 31
stroke: road_border_color
layer: 41
- tags: {highway: primary}
stroke-width: 29
stroke: primary_border_color
layer: 41
- tags: {highway: secondary}
stroke-width: 27
stroke: road_border_color
layer: 41
- tags: {highway: tertiary}
stroke-width: 25
stroke: road_border_color
layer: 41
- tags: {highway: unclassified}
stroke-width: 17
stroke: road_border_color
layer: 41
- tags: {highway: residential}
stroke-width: 17
stroke: road_border_color
layer: 41
- tags: {highway: service}
no_tags: {service: parking_aisle}
stroke-width: 11
stroke: road_border_color
layer: 41
- tags: {highway: service, service: parking_aisle}
stroke-width: 7
stroke: road_border_color
layer: 41
- tags: {highway: track}
stroke-width: 3
stroke: road_border_color
layer: 41
- tags: {highway: [footway, pedestrian, cycleway]}
no_tags: {area: "yes"}
stroke-width: 3
stroke: foot_border_color
layer: 41
- tags: {highway: steps}
stroke-width: 6
stroke: foot_border_color
- tags: {highway: motorway}
stroke-width: 31
stroke: "#FFFFFF"
layer: 42
- tags: {highway: trunk}
stroke-width: 29
stroke: "#FFFFFF"
layer: 42
- tags: {highway: primary}
stroke-width: 27
stroke: primary_color
layer: 42
- tags: {highway: secondary}
stroke-width: 25
stroke: "#FFFFFF"
layer: 42
- tags: {highway: tertiary}
stroke-width: 23
stroke: "#FFFFFF"
layer: 42
- tags: {highway: unclassified}
stroke-width: 15
stroke: "#FFFFFF"
layer: 42
- tags: {highway: residential}
stroke-width: 15
stroke: "#FFFFFF"
layer: 42
- tags: {highway: service, service: parking_aisle}
stroke-width: 5
stroke: "#FFFFFF"
layer: 42
- tags: {highway: service}
no_tags: {service: parking_aisle}
stroke-width: 9
stroke: "#FFFFFF"
layer: 42
- tags: {highway: track}
stroke-width: 3
stroke: road_border_color
layer: 42
- tags: {highway: [footway, pedestrian]}
no_tags: {area: "yes"}
stroke-width: 1.5
stroke-dasharray: 7,3
stroke-linecap: round
stroke: foot_color
layer: 42
- tags: {highway: [footway, pedestrian], area: "yes"}
stroke: none
fill: "#DDDDDD"
layer: -55 # FIXME
- tags: {highway: cycleway}
no_tags: {area: "yes"}
stroke-width: 1
stroke: cycle_color
stroke-dasharray: 8,2
stroke-linecap: butt
layer: 42
- tags: {highway: steps, conveying: "*"}
stroke-width: 5
stroke-dasharray: 1.5,2
stroke-linecap: butt
stroke: "#888888"
layer: 42
- tags: {highway: steps}
no_tags: {conveying: "*"}
stroke-width: 5
stroke-dasharray: 1.5,2
stroke-linecap: butt
stroke: foot_color
layer: 42
- tags: {highway: path}
stroke-width: 1
stroke-dasharray: 5,5
stroke-linecap: butt
stroke: foot_color
layer: 42
- tags: {leisure: playground}
fill: playground_color
opacity: 0.2
icon: toy_horse
layer: 21
- tags: {leisure: garden}
fill: grass_color
layer: 21
- tags: {leisure: pitch}
fill: playground_color
opacity: 0.2
layer: 21
- tags: {leisure: park}
fill: grass_color
opacity: 0.5
- tags: {barrier: hedge}
fill: none
stroke: wood_color
stroke-width: 4
layer: 40
- tags: {barrier: [fence, retaining_wall]}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.4
layer: 40
- tags: {barrier: handrail}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.3
layer: 40
- tags: {barrier: kerb}
fill: none
stroke: "#000000"
stroke-width: 1
opacity: 0.2
layer: 40
- tags: {border: "*"}
stroke: "#FF0000"
stroke-width: 0.5
stroke-dasharray: 10,20
- tags: {"area:highway": "*"}
- tags: {boundary: "*"}
stroke: boundary_color
stroke-width: 1
stroke-dasharray: 10,5
layer: 60
tags_to_write: [ tags_to_write: [
"operator", "opening_hours", "cuisine", "network", "website", "operator", "opening_hours", "cuisine", "network", "website",
"website_2", "STIF:zone", "opening_hours:url", "phone", "website_2", "STIF:zone", "opening_hours:url", "phone",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 416 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 410 KiB

Before After
Before After

View file

@ -5,10 +5,11 @@ from hashlib import sha256
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set
from roentgen import process, ui from roentgen import ui
from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID
from roentgen.flinger import Geo, GeoFlinger from roentgen.flinger import Geo, GeoFlinger
from roentgen.osm_reader import OSMMember, OSMRelation, OSMWay from roentgen.osm_reader import OSMMember, OSMRelation, OSMWay
from roentgen.scheme import Scheme from roentgen.scheme import IconSet, Scheme
class Node: class Node:
@ -16,17 +17,16 @@ class Node:
Node in Röntgen terms. Node in Röntgen terms.
""" """
def __init__( def __init__(
self, shapes, tags: Dict[str, str], x: float, y: float, color: str, self, icon_set: IconSet, tags: Dict[str, str],
path: Optional[str], processed, priority: int = 0): point: (float, float), path: Optional[str],
self.shapes = shapes priority: int = 0, is_for_node: bool = True):
self.icon_set: IconSet = icon_set
self.tags = tags self.tags = tags
self.x = x self.point = point
self.y = y
self.color = color
self.path = path self.path = path
self.processed = processed
self.priority = priority self.priority = priority
self.layer = 0 self.layer = 0
self.is_for_node = is_for_node
class Way: class Way:
@ -34,12 +34,12 @@ class Way:
Way in Röntgen terms. Way in Röntgen terms.
""" """
def __init__( def __init__(
self, kind: str, nodes, path, style, layer: float = 0.0, self, kind: str, nodes, path, style: Dict[str, Any],
priority: float = 0, levels=None): layer: float = 0.0, priority: float = 0, levels=None):
self.kind = kind self.kind = kind
self.nodes = nodes self.nodes = nodes
self.path = path self.path = path
self.style = style self.style: Dict[str, Any] = style
self.layer = layer self.layer = layer
self.priority = priority self.priority = priority
self.levels = levels self.levels = levels
@ -219,8 +219,10 @@ class Constructor:
nodes = None nodes = None
center_point = None
if way: if way:
c = line_center( center_point = line_center(
map(lambda x: self.map_.node_map[x], way.nodes), self.flinger) map(lambda x: self.map_.node_map[x], way.nodes), self.flinger)
nodes = way.nodes nodes = way.nodes
@ -230,8 +232,8 @@ class Constructor:
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", nodes, path,
f"fill:none;stroke:#{user_color};" {"fill": "none", "stroke": "#" + user_color,
f"stroke-width:1;")) "stroke-width": 1}))
return return
if self.mode == "time": if self.mode == "time":
@ -240,339 +242,69 @@ class Constructor:
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", nodes, path,
f"fill:none;stroke:#{time_color};" {"fill": "none", "stroke": "#" + time_color,
f"stroke-width:1;")) "stroke-width": 1}))
return return
# Indoor features if not tags:
return
if "indoor" in tags: appended = False
v = tags["indoor"] kind: str = "way"
style = \ levels = None
f"stroke:#{self.color('indoor_border_color')};" \
f"stroke-width:1;"
if v == "area":
style += f"fill:#{self.color('indoor_color')};"
layer += 10
elif v == "corridor":
style += f"fill:#{self.color('indoor_color')};"
layer += 11
elif v in ["yes", "room", "elevator"]:
style += f"fill:#{self.color('indoor_color')};"
layer += 12
elif v == "column":
style += f"fill:#{self.color('indoor_border_color')};"
layer += 13
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Natural
if "natural" in tags:
v = tags["natural"]
style = "stroke:none;"
if v == "wood":
style += f"fill:#{self.color('wood_color')};"
layer += 21
elif v == "grassland":
style = \
f"fill:#{self.color('grass_color')};" \
f"stroke:#{self.color('grass_border_color')};"
layer += 20
elif v == "scrub":
style += f"fill:#{self.color('wood_color')};"
layer += 21
elif v == "sand":
style += f"fill:#{self.color('sand_color')};"
layer += 20
elif v == "beach":
style += f"fill:#{self.color('beach_color')};"
layer += 20
elif v == "desert":
style += f"fill:#{self.color('desert_color')};"
layer += 20
elif v == "forest":
style += f"fill:#{self.color('wood_color')};"
layer += 21
elif v == "tree_row":
style += \
f"fill:none;stroke:#{self.color('wood_color')};" \
f"stroke-width:5;"
layer += 21
elif v == "water":
style = \
f"fill:#{self.color('water_color')};" \
f"stroke:#{self.color('water_border_color')};" \
f"stroke-width:1.0;"
layer += 21
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Landuse
if "landuse" in tags:
style = "fill:none;stroke:none;"
if tags["landuse"] == "grass":
style = \
f"fill:#{self.color('grass_color')};" \
f"stroke:#{self.color('grass_border_color')};"
layer += 20
elif tags["landuse"] == "conservation":
style = f"fill:#{self.color('grass_color')};stroke:none;"
layer += 20
elif tags["landuse"] == "forest":
style = f"fill:#{self.color('wood_color')};stroke:none;"
layer += 20
elif tags["landuse"] == "garages":
style = f"fill:#{self.color('parking_color')};stroke:none;"
layer += 21
shapes, fill, processed = self.scheme.get_icon(tags)
if way:
self.nodes.append(Node(
shapes, tags, c[0], c[1], fill, path, processed))
elif tags["landuse"] == "construction":
layer += 20
style = f"fill:#{self.color('construction_color')};stroke:none;"
elif tags["landuse"] in ["residential", "commercial"]:
return
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Building
if "building" in tags: if "building" in tags:
layer += 40 kind = "building"
levels = 1 if "building:levels" in tags:
if "building:levels" in tags: levels = float(tags["building:levels"])
levels = float(tags["building:levels"])
style = \
f"fill:#{self.color('building_color')};" \
f"stroke:#{self.color('building_border_color')};" \
f"opacity:1.0;"
shapes, fill, processed = self.scheme.get_icon(tags)
if "height" in tags:
try:
layer += float(tags["height"])
except ValueError:
pass
if way:
self.nodes.append(
Node(shapes, tags, c[0], c[1], fill, path, processed, 1))
self.ways.append(Way(
"building", nodes, path, style, layer, 50, levels))
# Amenity for element in self.scheme.ways: # type: Dict[str, Any]
matched: bool = True
if "amenity" in tags: for config_tag_key in element["tags"]: # type: str
style = "fill:none;stroke:none;" matcher = element["tags"][config_tag_key]
layer += 21 if config_tag_key not in tags or \
if tags["amenity"] == "parking": (matcher != "*" and
style = \ tags[config_tag_key] != matcher and
f"fill:#{self.color('parking_color')};" \ tags[config_tag_key] not in matcher):
f"stroke:none;opacity:0.5;" matched = False
shapes, fill, processed = self.scheme.get_icon(tags) break
if way: 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 "layer" in element:
layer += element["layer"]
for key in element: # type: str
if key not in ["tags", "no_tags", "layer", "level", "icon"]:
value = element[key]
if isinstance(value, str) and value.endswith("_color"):
value = "#" + self.scheme.get_color(value)
style[key] = value
self.ways.append(
Way(kind, nodes, path, style, layer, 50, levels))
if center_point and way.is_cycle() or \
"area" in tags and tags["area"]:
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Node( self.nodes.append(Node(
shapes, tags, c[0], c[1], fill, path, processed, 1)) icon_set, tags, center_point, path, is_for_node=False))
self.ways.append(Way("way", nodes, path, style, layer, 50)) appended = True
# Waterway if not appended:
style: Dict[str, Any] = {
"fill": "none", "stroke": "#FF0000", "stroke-width": 1}
self.ways.append(Way(kind, nodes, path, style, layer, 50, levels))
if center_point and way.is_cycle() or \
"area" in tags and tags["area"]:
icon_set: IconSet = self.scheme.get_icon(tags)
self.nodes.append(Node(
icon_set, tags, center_point, path, is_for_node=False))
if "waterway" in tags: def construct_relations(self) -> None:
style = "fill:none;stroke:none;"
layer += 21
if tags["waterway"] == "riverbank":
style = \
f"fill:#{self.color('water_color')};" \
f"stroke:#{self.color('water_border_color')};" \
f"stroke-width:1.0;"
elif tags["waterway"] == "river":
style = \
f"fill:none;stroke:#{self.color('water_color')};" \
f"stroke-width:10.0;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Railway
if "railway" in tags:
layer += 41
v = tags["railway"]
style = \
"fill:none;stroke-dasharray:none;stroke-linejoin:round;" \
"stroke-linecap:round;stroke-width:"
if v == "subway":
style += "10;stroke:#DDDDDD;"
if v in ["narrow_gauge", "tram"]:
style += "2;stroke:#000000;"
if v == "platform":
style = \
f"fill:#{self.color('platform_color')};" \
f"stroke:#{self.color('platform_border_color')};" \
f"stroke-width:1;"
else:
return
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Highway
if "highway" in tags:
layer += 42
v = tags["highway"]
style = \
f"fill:none;stroke:#{self.color('road_border_color')};" \
f"stroke-dasharray:none;stroke-linejoin:round;" \
f"stroke-linecap:round;stroke-width:"
# Highway outline
if v == "motorway":
style += "33"
elif v == "trunk":
style += "31"
elif v == "primary":
style += f"29;stroke:#{self.color('primary_border_color')};"
elif v == "secondary":
style += "27"
elif v == "tertiary":
style += "25"
elif v == "unclassified":
style += "17"
elif v == "residential":
style += "17"
elif v == "service":
if "service" in tags and tags["service"] == "parking_aisle":
style += "7"
else:
style += "11"
elif v == "track":
style += "3"
elif v in ["footway", "pedestrian", "cycleway"]:
if not ("area" in tags and tags["area"] == "yes"):
style += f"3;stroke:#{self.color('foot_border_color')};"
elif v in ["steps"]:
style += \
f"6;stroke:#{self.color('foot_border_color')};" \
f"stroke-linecap:butt;"
else:
style = None
if style:
style += ";"
self.ways.append(Way(
"way", nodes, path, style, layer + 41, 50))
# Highway main shape
style = "fill:none;stroke:#FFFFFF;stroke-linecap:round;" + \
"stroke-linejoin:round;stroke-width:"
if v == "motorway":
style += "31"
elif v == "trunk":
style += "29"
elif v == "primary":
style += "27;stroke:#" + self.color('primary_color')
elif v == "secondary":
style += "25"
elif v == "tertiary":
style += "23"
elif v == "unclassified":
style += "15"
elif v == "residential":
style += "15"
elif v == "service":
if "service" in tags and tags["service"] == "parking_aisle":
style += "5"
else:
style += "9"
elif v == "cycleway":
style += \
f"1;stroke-dasharray:8,2;istroke-linecap:butt;" \
f"stroke:#{self.color('cycle_color')}"
elif v in ["footway", "pedestrian"]:
if "area" in tags and tags["area"] == "yes":
style += "1;stroke:none;fill:#DDDDDD"
layer -= 55 # FIXME!
else:
style += \
"1.5;stroke-dasharray:7,3;stroke-linecap:round;stroke:#"
if "guide_strips" in tags and tags["guide_strips"] == "yes":
style += self.color('guide_strips_color')
else:
style += self.color('foot_color')
elif v == "steps":
style += "5;stroke-dasharray:1.5,2;stroke-linecap:butt;" + \
"stroke:#"
if "conveying" in tags:
style += "888888"
else:
style += self.color('foot_color')
elif v == "path":
style += "1;stroke-dasharray:5,5;stroke-linecap:butt;" + \
"stroke:#" + self.color('foot_color')
style += ";"
self.ways.append(Way("way", nodes, path, style, layer + 42, 50))
if "oneway" in tags and tags["oneway"] == "yes" or \
"conveying" in tags and tags["conveying"] == "forward":
for k in range(7):
self.ways.append(Way(
"way", nodes, path,
f"fill:none;stroke:#EEEEEE;stroke-linecap:butt;"
f"stroke-width:{7 - k};stroke-dasharray:{k},{40 - k};",
layer + 43, 50))
if "access" in tags and tags["access"] == "private":
self.ways.append(Way(
"way", nodes, path,
f"fill:none;stroke:#{self.color('private_access_color')};"
f"stroke-linecap:butt;stroke-width:10;stroke-dasharray:1,5;"
f"opacity:0.4;", layer + 0.1, 50))
# Leisure
if "leisure" in tags:
layer += 21
if tags["leisure"] == "playground":
style = f"fill:#{self.color('playground_color')};opacity:0.2;"
# FIXME!!!!!!!!!!!!!!!!!!!!!
# if nodes:
# self.draw_point_shape("toy_horse", c[0], c[1], "444444")
elif tags["leisure"] == "garden":
style = f"fill:#{self.color('grass_color')};"
elif tags["leisure"] == "pitch":
style = f"fill:#{self.color('playground_color')};opacity:0.2;"
elif tags["leisure"] == "park":
return
else:
style = "fill:#FF0000;opacity:0.2;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Barrier
if "barrier" in tags:
style = "fill:none;stroke:none;"
layer += 40
if tags["barrier"] == "hedge":
style += \
f"fill:none;stroke:#{self.color('wood_color')};" \
f"stroke-width:4;"
elif tags["barrier"] == "fense":
style += "fill:none;stroke:#000000;stroke-width:1;opacity:0.4;"
elif tags["barrier"] == "kerb":
style += "fill:none;stroke:#000000;stroke-width:1;opacity:0.2;"
else:
style += "fill:none;stroke:#000000;stroke-width:1;opacity:0.3;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
# Border
if "border" in tags:
style = "fill:none;stroke:none;"
style += "fill:none;stroke:#FF0000;stroke-width:0.5;" + \
"stroke-dahsarray:10,20;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
if "area:highway" in tags:
style = "fill:none;stroke:none;"
if tags["area:highway"] == "yes":
style += "fill:#FFFFFF;stroke:#DDDDDD;stroke-width:1;"
self.ways.append(Way("way", nodes, path, style, layer, 50))
def construct_relations(self):
""" """
Construct Röntgen ways from OSM relations. Construct Röntgen ways from OSM relations.
""" """
@ -603,7 +335,7 @@ class Constructor:
p += path + " " p += path + " "
self.construct_way(None, tags, p) self.construct_way(None, tags, p)
def construct_nodes(self): def construct_nodes(self) -> None:
""" """
Draw nodes. Draw nodes.
""" """
@ -618,33 +350,27 @@ class Constructor:
for node_id in s: # type: int for node_id in s: # type: int
node_number += 1 node_number += 1
ui.write_line(node_number, len(self.map_.node_map)) ui.progress_bar(node_number, len(self.map_.node_map))
node = self.map_.node_map[node_id] node = self.map_.node_map[node_id]
flung = self.flinger.fling(Geo(node.lat, node.lon)) flung = self.flinger.fling(Geo(node.lat, node.lon))
x = flung[0]
y = flung[1]
tags = node.tags tags = node.tags
if not self.check_level(tags): if not self.check_level(tags):
continue continue
shapes, fill, processed = self.scheme.get_icon(tags) icon_set: IconSet = self.scheme.get_icon(tags)
if self.mode in ["time", "user-coloring"]: if self.mode in ["time", "user-coloring"]:
if not tags: if not tags:
continue continue
shapes = ["small"] icon_set.icons = [[DEFAULT_SMALL_SHAPE_ID]]
if self.mode == "user-coloring": if self.mode == "user-coloring":
fill = 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":
fill = get_time_color(node.timestamp) icon_set.color = get_time_color(node.timestamp)
if shapes == [] and tags != {}: self.nodes.append(Node(icon_set, tags, flung, None))
shapes = [["no"]]
self.nodes.append(Node( ui.progress_bar(-1, len(self.map_.node_map))
shapes, tags, x, y, fill, None, processed))
ui.write_line(-1, len(self.map_.node_map))
print("Nodes painted in " + str(datetime.now() - start_time) + ".") print("Nodes painted in " + str(datetime.now() - start_time) + ".")

View file

@ -5,11 +5,52 @@ Author: Sergey Vartanov (me@enzet.ru).
""" """
import re import re
import xml.dom.minidom import xml.dom.minidom
from typing import Dict from typing import Dict
import numpy as np
from svgwrite import Drawing
from roentgen import ui from roentgen import ui
DEFAULT_SHAPE_ID: str = "default"
DEFAULT_SMALL_SHAPE_ID: str = "default_small"
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
"""
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: class IconExtractor:
""" """
@ -22,7 +63,7 @@ class IconExtractor:
:param svg_file_name: input SVG file name with icons. File may contain :param svg_file_name: input SVG file name with icons. File may contain
any other irrelevant graphics. any other irrelevant graphics.
""" """
self.icons: Dict[str, (str, float, float)] = {} self.icons: Dict[str, Icon] = {}
with open(svg_file_name) as input_file: with open(svg_file_name) as input_file:
content = xml.dom.minidom.parse(input_file) content = xml.dom.minidom.parse(input_file)
@ -38,35 +79,39 @@ class IconExtractor:
:param node: XML node that contains icon :param node: XML node that contains icon
""" """
if node.nodeName == "path": if node.nodeName != "path":
if "id" in node.attributes.keys() and \
"d" in node.attributes.keys() and \
node.attributes["id"].value:
path = node.attributes["d"].value
m = re.match("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)", path)
if not m:
ui.error(f"invalid path: {path}")
else:
x = int(float(m.group(1)) / 16)
y = int(float(m.group(2)) / 16)
self.icons[node.attributes["id"].value] = \
(node.attributes["d"].value, x, y)
else:
for sub_node in node.childNodes: for sub_node in node.childNodes:
self.parse(sub_node) self.parse(sub_node)
return
def get_path(self, id_: str) -> (str, float, float, bool): if "id" in node.attributes.keys() and \
"d" in node.attributes.keys() and \
node.attributes["id"].value:
path = 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
self.icons[id_] = Icon(node.attributes["d"].value, point, id_)
def get_path(self, id_: str) -> (Icon, bool):
""" """
Get SVG path of the icon. Get SVG path of the icon.
:param id_: string icon ID :param id_: string icon identifier
""" """
if id_ in self.icons: if id_ in self.icons:
return list(self.icons[id_]) + [True] return self.icons[id_], True
else:
if id_ == "no": ui.error(f"no such icon ID {id_}")
return "M 4,4 L 4,10 10,10 10,4 z", 0, 0, False return self.icons[DEFAULT_SHAPE_ID], False
if id_ == "small":
return "M 6,6 L 6,8 8,8 8,6 z", 0, 0, False
ui.error(f"no such icon ID {id_}")
return "M 4,4 L 4,10 10,10 10,4 z", 0, 0, False

View file

@ -1,98 +1,46 @@
#!/usr/bin/env python
""" """
Author: Sergey Vartanov (me@enzet.ru) Author: Sergey Vartanov (me@enzet.ru)
""" """
import math import math
import numpy as np import numpy as np
from typing import Optional
def get_ratio(maximum, minimum, ratio: float = 1): def get_ratio(maximum, minimum, ratio: float = 1):
return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1]) return (maximum[0] - minimum[0]) * ratio / (maximum[1] - minimum[1])
class Flinger: def map_(
value: float, current_min: float, current_max: float, target_min: float,
target_max: float):
""" """
Flinger. Coordinates repositioning. Map current value in bounds of current_min and current_max to bounds of
target_min and target_max.
""" """
def __init__( return \
self, minimum, maximum, target_minimum=None, target_maximum=None, target_min + (value - current_min) / (current_max - current_min) * \
ratio=None): (target_max - target_min)
self.minimum = minimum
self.maximum = maximum
if not target_minimum:
target_minimum = [0, 0]
if not target_maximum:
target_maximum = maximum - minimum
space = [0, 0]
if ratio:
if ratio == "geo":
ratio = math.sin(
(90.0 - ((self.maximum[1] + self.minimum[1]) / 2.0))
/ 180.0 * math.pi)
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[0] - minimum[0]) / ratio
space[1] = \
((target_maximum[1] - target_minimum[1]) -
(maximum[1] - minimum[1]) * n) / 2.0
space[0] = 0
else:
n = (target_maximum[1] - target_minimum[1]) / \
(maximum[1] - minimum[1])
space[0] = \
((target_maximum[0] - target_minimum[0]) -
(maximum[0] - minimum[0]) * n) / 2.0
space[1] = 0
target_minimum[0] += space
target_maximum[0] += space
self.target_minimum = target_minimum
self.target_maximum = target_maximum
def fling(self, current):
"""
Fling current point to the surface.
:param current: vector to fling
"""
x = map_(
current[0], self.minimum[0], self.maximum[0],
self.target_minimum[0], self.target_maximum[0])
y = map_(
current[1], self.minimum[1], self.maximum[1],
self.target_minimum[1], self.target_maximum[1])
return [x, y]
class Geo: class Geo:
def __init__(self, lat, lon): def __init__(self, lat: float, lon: float):
self.lat = lat self.lat: float = lat
self.lon = lon self.lon: float = lon
def __getitem__(self, item): def __getitem__(self, item) -> Optional[float]:
if item == 0: if item == 0:
return self.lon return self.lon
if item == 1: if item == 1:
return self.lat return self.lat
return None
def __add__(self, other): def __add__(self, other: "Geo") -> "Geo":
return Geo(self.lat + other.lat, self.lon + other.lon) return Geo(self.lat + other.lat, self.lon + other.lon)
def __sub__(self, other): def __sub__(self, other: "Geo") -> "Geo":
return Geo(self.lat - other.lat, self.lon - other.lon) return Geo(self.lat - other.lat, self.lon - other.lon)
def __repr__(self): def __repr__(self) -> str:
return f"{self.lat}, {self.lon}" return f"{self.lat}, {self.lon}"
@ -157,15 +105,3 @@ class GeoFlinger:
self.minimum.lat, self.maximum.lat, self.minimum.lat, self.maximum.lat,
self.target_minimum[1], self.target_maximum[1]) self.target_minimum[1], self.target_maximum[1])
return [x, y] return [x, y]
def map_(
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
target_min and target_max.
"""
return \
target_min + (value - current_min) / (current_max - current_min) * \
(target_max - target_min)

View file

@ -1,24 +1,15 @@
""" """
Author: Sergey Vartanov (me@enzet.ru). Author: Sergey Vartanov (me@enzet.ru).
""" """
import numpy as np
import os import os
import random import random
import svgwrite import svgwrite
import yaml import yaml
from roentgen import extract_icon from roentgen.extract_icon import Icon, IconExtractor
from typing import Any, Dict from typing import List
def draw_icon(
svg, icon: Dict[str, Any], x: float, y: float,
color: str = "444444"):
svg.add(svg.path(
d=icon["path"], fill=f"#{color}", stroke="none",
transform=f'translate({icon["x"] + x},{icon["y"] + y})'))
def draw_grid(): def draw_grid():
@ -47,8 +38,7 @@ def draw_grid():
width: float = 24 * 16 width: float = 24 * 16
x: float = step / 2 point: np.array = np.array((step / 2, step / 2))
y: float = step / 2
to_draw = [] to_draw = []
@ -83,20 +73,19 @@ def draw_grid():
number: int = 0 number: int = 0
icons = [] icons: List[List[Icon]] = []
extractor = extract_icon.IconExtractor(icons_file_name) extractor: IconExtractor = IconExtractor(icons_file_name)
for icons_to_draw in to_draw: for icons_to_draw in to_draw:
drawed = False found: bool = False
icon_set = {"icons": []} icon_set: List[Icon] = []
for icon in icons_to_draw: for icon_id in icons_to_draw: # type: str
path, xx, yy, _ = extractor.get_path(icon) icon, got = extractor.get_path(icon_id)
icon_set["icons"].append({"path": path, assert got
"x": (- 8.0 - xx * 16), icon_set.append(icon)
"y": (- 8.0 - yy * 16)}) found = True
drawed = True if found:
if drawed:
icons.append(icon_set) icons.append(icon_set)
number += 1 number += 1
@ -109,13 +98,16 @@ def draw_grid():
for icon in icons: for icon in icons:
background_color, foreground_color = random.choice(icon_colors) background_color, foreground_color = random.choice(icon_colors)
svg.add(svg.rect( svg.add(svg.rect(
(x - 2 - 8, y - 2 - 8), (20, 20), fill=f"#{background_color}")) point - np.array((-10, -10)), (20, 20),
for i in icon["icons"]: fill=f"#{background_color}"))
draw_icon(svg, i, x, y, foreground_color) for i in icon: # type: Icon
x += step path = i.get_path(svg, point)
if x > width - 8: path.update({"fill": f"#{foreground_color}"})
x = step / 2 svg.add(path)
y += step point += np.array((step, 0))
if point[0] > width - 8:
point[0] = step / 2
point += np.array((0, step))
height += step height += step
print(f"Icons: {number}.") print(f"Icons: {number}.")

View file

@ -7,19 +7,18 @@ import numpy as np
import os import os
import svgwrite import svgwrite
import sys import sys
import yaml
from svgwrite.container import Group from svgwrite.container import Group
from svgwrite.path import Path from svgwrite.path import Path
from svgwrite.shapes import Circle, Rect from svgwrite.shapes import Circle, Rect
from svgwrite.text import Text from svgwrite.text import Text
from typing import List from typing import Dict, List
from roentgen import extract_icon
from roentgen import ui from roentgen import ui
from roentgen.constructor import Constructor, get_path from roentgen.constructor import Constructor, get_path, Node, Way
from roentgen.flinger import GeoFlinger, Geo from roentgen.flinger import GeoFlinger, Geo
from roentgen.grid import draw_grid from roentgen.grid import draw_grid
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
@ -29,70 +28,97 @@ TAGS_FILE_NAME: str = "data/tags.yml"
COLORS_FILE_NAME: str = "data/colors.yml" COLORS_FILE_NAME: str = "data/colors.yml"
MISSING_TAGS_FILE_NAME: str = "missing_tags.yml" MISSING_TAGS_FILE_NAME: str = "missing_tags.yml"
AUTHOR_MODE = "user-coloring"
CREATION_TIME_MODE = "time"
class Painter: class Painter:
"""
Map drawing.
"""
def __init__( def __init__(
self, show_missing_tags, overlap, draw_nodes, mode, draw_captions, self, show_missing_tags: bool, overlap: int, draw_nodes: bool,
map_, flinger, svg: svgwrite.Drawing, icons, scheme: Scheme): mode: str, draw_captions: bool, map_: Map, flinger: GeoFlinger,
svg: svgwrite.Drawing, icon_extractor: IconExtractor,
scheme: Scheme):
self.show_missing_tags = show_missing_tags self.show_missing_tags: bool = show_missing_tags
self.overlap = overlap self.overlap: int = overlap
self.draw_nodes = draw_nodes self.draw_nodes: bool = draw_nodes
self.mode = mode self.mode: str = mode
self.draw_captions = draw_captions self.draw_captions = draw_captions
self.map_ = map_ self.map_: Map = map_
self.flinger = flinger self.flinger: GeoFlinger = flinger
self.svg: svgwrite.Drawing = svg self.svg: svgwrite.Drawing = svg
self.icons = icons self.icon_extractor = icon_extractor
self.scheme: Scheme = scheme self.scheme: Scheme = scheme
def draw_shapes(self, shapes, points, x, y, fill, tags, processed): def draw_shapes(self, node: Node, points: List[List[float]]):
"""
Draw shapes for one node.
"""
if node.icon_set.is_default and not node.is_for_node:
return
xxx = -(len(shapes) - 1) * 8 left: float = -(len(node.icon_set.icons) - 1) * 8
if self.overlap != 0: if self.overlap != 0:
for shape in shapes: for shape_ids in node.icon_set.icons:
has_space = True has_space = True
for p in points[-1000:]: for p in points[-1000:]:
if x + xxx - self.overlap <= p[0] <= x + xxx + self.overlap and \ if node.point[0] + left - self.overlap <= p[0] \
y - self.overlap <= p[1] <= y + self.overlap: <= node.point[0] + left + self.overlap and \
node.point[1] - self.overlap <= p[1] \
<= node.point[1] + self.overlap:
has_space = False has_space = False
break break
if has_space: if has_space:
self.draw_point_shape(shape, x + xxx, y, fill, tags=tags) self.draw_point_shape(
points.append([x + xxx, y]) shape_ids, (node.point[0] + left, node.point[1]),
xxx += 16 node.icon_set.color, tags=node.tags)
points.append([node.point[0] + left, node.point[1]])
left += 16
else: else:
for shape in shapes: for shape_ids in node.icon_set.icons:
self.draw_point_shape(shape, x + xxx, y, fill, tags=tags) self.draw_point_shape(
xxx += 16 shape_ids, (node.point[0] + left, node.point[1]),
node.icon_set.color, tags=node.tags)
def draw_texts(self, shapes, points, x, y, fill, tags, processed): left += 16
if self.draw_captions == "no":
return
def draw_texts(self, node: Node):
"""
Draw all labels.
"""
text_y: float = 0 text_y: float = 0
write_tags = self.construct_text(tags, processed) write_tags = self.construct_text(node.tags, node.icon_set.processed)
for text_struct in write_tags: for text_struct in write_tags:
fill = text_struct["fill"] if "fill" in text_struct else "444444" fill = text_struct["fill"] if "fill" in text_struct else "444444"
size = text_struct["size"] if "size" in text_struct else 10 size = text_struct["size"] if "size" in text_struct else 10
text_y += size + 1 text_y += size + 1
self.wr(text_struct["text"], x, y, fill, text_y, size=size) text = text_struct["text"]
text = text.replace("&quot;", '"')
text = text.replace("&amp;", '&')
text = text[:26] + ("..." if len(text) > 26 else "")
self.draw_text(
text, (node.point[0], node.point[1] + text_y + 8),
fill, size=size)
if self.show_missing_tags: if self.show_missing_tags:
for k in tags: for tag in node.tags: # type: str
if not self.no_draw(k) and k not in processed: if not self.scheme.is_no_drawable(tag) and \
text = k + ": " + tags[k] tag not in node.icon_set.processed:
self.draw_text(text, x, float(y) + text_y + 18, "734A08") text = f"{tag}: {node.tags[tag]}"
self.draw_text(
text, (node.point[0], node.point[1] + text_y + 18),
"734A08")
text_y += 10 text_y += 10
def draw_text(self, text: str, x, y, fill, size=10, out_fill="FFFFFF", def draw_text(
out_opacity=1.0, out_fill_2=None, out_opacity_2=1.0): self, text: str, point, fill, size=10, out_fill="FFFFFF",
out_opacity=1.0, out_fill_2=None, out_opacity_2=1.0):
""" """
Drawing text. Drawing text.
@ -104,27 +130,24 @@ class Painter:
""" """
if out_fill_2: if out_fill_2:
self.svg.add(Text( self.svg.add(Text(
text, (x, y), font_size=size, text_anchor="middle", text, point, font_size=size, text_anchor="middle",
font_family="Roboto", fill=f"#{out_fill_2}", font_family="Roboto", fill=f"#{out_fill_2}",
stroke_linejoin="round", stroke_width=5, stroke_linejoin="round", stroke_width=5,
stroke=f"#{out_fill_2}", opacity=out_opacity_2)) stroke=f"#{out_fill_2}", opacity=out_opacity_2))
if out_fill: if out_fill:
self.svg.add(Text( self.svg.add(Text(
text, (x, y), font_size=size, text_anchor="middle", text, point, font_size=size, text_anchor="middle",
font_family="Roboto", fill=f"#{out_fill}", font_family="Roboto", fill=f"#{out_fill}",
stroke_linejoin="round", stroke_width=3, stroke_linejoin="round", stroke_width=3,
stroke=f"#{out_fill}", opacity=out_opacity)) stroke=f"#{out_fill}", opacity=out_opacity))
self.svg.add(Text( self.svg.add(Text(
text, (x, y), font_size=size, text_anchor="middle", text, point, font_size=size, text_anchor="middle",
font_family="Roboto", fill=f"#{fill}")) font_family="Roboto", fill=f"#{fill}"))
def wr(self, text, x, y, fill, text_y, size=10):
text = text[:26] + ("..." if len(text) > 26 else "")
self.draw_text(text, x, float(y) + text_y + 8, fill, size=size)
def construct_text(self, tags, processed): def construct_text(self, tags, processed):
for key in tags: """
tags[key] = tags[key].replace("&quot;", '"') Construct labels for not processed tags.
"""
texts = [] texts = []
address: List[str] = [] address: List[str] = []
name = None name = None
@ -207,8 +230,7 @@ class Painter:
texts.append({"text": tags[k], "fill": "444444"}) texts.append({"text": tags[k], "fill": "444444"})
tags.pop(k) tags.pop(k)
for tag in tags: for tag in tags:
if self.to_write(tag) and not (tag in processed): if self.scheme.is_writable(tag) and not (tag in processed):
# texts.append({"text": tag + ": " + tags[tag]})
texts.append({"text": tags[tag]}) texts.append({"text": tags[tag]})
return texts return texts
@ -250,21 +272,27 @@ class Painter:
pass pass
def draw(self, nodes, ways, points): def draw(self, nodes, ways, points):
"""
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:
if way.kind == "way": if way.kind == "way":
if way.nodes: if way.nodes:
path = get_path(way.nodes, [0, 0], self.map_, self.flinger) path = get_path(way.nodes, [0, 0], self.map_, self.flinger)
self.svg.add(Path(d=path, style=way.style)) p = Path(d=path)
p.update(way.style)
self.svg.add(p)
else: else:
self.svg.add(Path(d=way.path, style=way.style)) 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: for way in ways: # type: Way
if way.kind != "building" or not way.nodes: if way.kind != "building" or not way.nodes:
continue continue
shift = [-5, 5] shift = [-5, 5]
@ -290,7 +318,7 @@ class Painter:
# Building roof # Building roof
for way in ways: for way in ways: # type: Way
if way.kind != "building": if way.kind != "building":
continue continue
if way.nodes: if way.nodes:
@ -298,9 +326,13 @@ class Painter:
if way.levels: if way.levels:
shift = [0 * way.levels, min(-3, -1 * way.levels)] shift = [0 * way.levels, min(-3, -1 * way.levels)]
path = get_path(way.nodes, shift, self.map_, self.flinger) path = get_path(way.nodes, shift, self.map_, self.flinger)
self.svg.add(Path(d=path, style=way.style, opacity=1)) p = Path(d=path, opacity=1)
p.update(way.style)
self.svg.add(p)
else: else:
self.svg.add(Path(d=way.path, style=way.style, opacity=1)) p = Path(d=way.path, opacity=1)
p.update(way.style)
self.svg.add(p)
# Trees # Trees
@ -310,58 +342,59 @@ class Painter:
"diameter_crown" in node.tags): "diameter_crown" in node.tags):
continue continue
self.svg.add(Circle( self.svg.add(Circle(
(float(node.x), float(node.y)), node.point, float(node.tags["diameter_crown"]) * 1.2,
float(node.tags["diameter_crown"]) * 1.2,
fill="#688C44", stroke="#688C44", opacity=0.3)) fill="#688C44", stroke="#688C44", opacity=0.3))
# All other nodes # All other nodes
nodes = sorted(nodes, key=lambda x: x.layer) nodes = sorted(nodes, key=lambda x: x.layer)
for node in nodes: for node in nodes: # type: Node
if "natural" in node.tags and \ if "natural" in node.tags and \
node.tags["natural"] == "tree" and \ node.tags["natural"] == "tree" and \
"diameter_crown" in node.tags: "diameter_crown" in node.tags:
continue continue
self.draw_shapes( self.draw_shapes(node, points)
node.shapes, points, node.x, node.y, node.color, node.tags,
node.processed)
for node in nodes: if self.draw_captions == "no":
if self.mode not in ["time", "user-coloring"]: return
self.draw_texts(
node.shapes, points, node.x, node.y, node.color,
node.tags, node.processed)
def draw_point_shape(self, name, x, y, fill, tags=None): for node in nodes: # type: Node
if not isinstance(name, list): if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
name = [name] self.draw_texts(node)
if self.mode not in ["time", "user-coloring"]:
for one_name in name:
shape, xx, yy, _ = self.icons.get_path(one_name)
self.draw_point_outline(
shape, x, y, fill, mode=self.mode, size=16, xx=xx, yy=yy)
for one_name in name:
shape, xx, yy, _ = self.icons.get_path(one_name)
self.draw_point(shape, x, y, fill, size=16, xx=xx, yy=yy, tags=tags)
def draw_point(self, shape, x, y, fill, size=16, xx=0, yy=0, tags=None): def draw_point_shape(self, shape_ids: List[str], point, fill, tags=None):
x = int(float(x)) """
y = int(float(y)) Draw one icon.
path = self.svg.path( """
d=shape, fill=f"#{fill}", fill_opacity=1, if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
transform=f"translate({x - size / 2.0 - xx * 16}," for shape_id in shape_ids: # type: str
f"{y - size / 2.0 - yy * 16})") icon, _ = self.icon_extractor.get_path(shape_id)
path.set_desc(title="\n".join(map(lambda x: x + ": " + tags[x], tags))) self.draw_point_outline(icon, point, fill, mode=self.mode)
for shape_id in shape_ids: # type: str
icon, _ = self.icon_extractor.get_path(shape_id)
self.draw_point(icon, point, fill, tags=tags)
def draw_point(
self, icon: Icon, point: (float, float), fill: str,
tags: Dict[str, str] = None) -> None:
point = np.array(list(map(lambda x: int(x), point)))
title: str = "\n".join(map(lambda x: x + ": " + tags[x], tags))
path = icon.get_path(self.svg, point)
path.update({"fill": f"#{fill}"})
path.set_desc(title=title)
self.svg.add(path) self.svg.add(path)
def draw_point_outline( def draw_point_outline(
self, shape, x, y, fill, mode="default", size=16, xx=0, yy=0): self, icon: Icon, point, fill, mode="default", size=16):
x = int(float(x))
y = int(float(y)) point = np.array(list(map(lambda x: int(x), point)))
opacity = 0.5 opacity = 0.5
stroke_width = 2.2 stroke_width = 2.2
outline_fill = self.scheme.get_color("outline_color") outline_fill = self.scheme.get_color("outline_color")
if mode not in ["user-coloring", "time"]: if mode not in [AUTHOR_MODE, CREATION_TIME_MODE]:
r = int(fill[0:2], 16) r = int(fill[0:2], 16)
g = int(fill[2:4], 16) g = int(fill[2:4], 16)
b = int(fill[4:6], 16) b = int(fill[4:6], 16)
@ -369,12 +402,13 @@ class Painter:
if Y > 200: if Y > 200:
outline_fill = "000000" outline_fill = "000000"
opacity = 0.7 opacity = 0.7
self.svg.add(self.svg.path(
d=shape, fill=f"#{outline_fill}", opacity=opacity, path = icon.get_path(self.svg, point)
stroke=f"#{outline_fill}", stroke_width=stroke_width, path.update({
stroke_linejoin="round", "fill": f"#{outline_fill}", "opacity": opacity,
transform=f"translate({x - size / 2.0 - xx * 16}," "stroke": f"#{outline_fill}", "stroke-width": stroke_width,
f"{y - size / 2.0 - yy * 16})")) "stroke-linejoin": "round"})
self.svg.add(path)
def check_level_number(tags, level): def check_level_number(tags, level):
@ -395,6 +429,14 @@ def check_level_overground(tags):
for level in levels: for level in levels:
if level <= 0: if level <= 0:
return False return False
if "layer" in tags:
levels = \
map(lambda x: float(x), tags["layer"].replace(",", ".").split(";"))
for level in levels:
if level <= 0:
return False
if "parking" in tags and tags["parking"] == "underground":
return False
return True return True
@ -410,7 +452,7 @@ def main():
sys.exit(1) sys.exit(1)
background_color = "#EEEEEE" background_color = "#EEEEEE"
if options.mode in ["user-coloring", "time"]: if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
background_color = "#111111" background_color = "#111111"
if options.input_file_name: if options.input_file_name:
@ -426,7 +468,7 @@ def main():
full = False # Full keys getting full = False # Full keys getting
if options.mode in ["user-coloring", "time"]: if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
full = True full = True
osm_reader = OSMReader() osm_reader = OSMReader()
@ -460,7 +502,7 @@ def main():
flinger = GeoFlinger(min1, max1, [0, 0], [w, h]) flinger = GeoFlinger(min1, max1, [0, 0], [w, h])
icons = extract_icon.IconExtractor(ICONS_FILE_NAME) icon_extractor = IconExtractor(ICONS_FILE_NAME)
def check_level(x): def check_level(x):
""" Draw objects on all levels. """ """ Draw objects on all levels. """
@ -489,7 +531,7 @@ def main():
show_missing_tags=options.show_missing_tags, overlap=options.overlap, show_missing_tags=options.show_missing_tags, overlap=options.overlap,
draw_nodes=options.draw_nodes, mode=options.mode, draw_nodes=options.draw_nodes, mode=options.mode,
draw_captions=options.draw_captions, draw_captions=options.draw_captions,
map_=map_, flinger=flinger, svg=svg, icons=icons, map_=map_, flinger=flinger, svg=svg, icon_extractor=icon_extractor,
scheme=scheme) scheme=scheme)
painter.draw(constructor.nodes, constructor.ways, points) painter.draw(constructor.nodes, constructor.ways, points)

View file

@ -1,3 +1,8 @@
"""
Getting OpenStreetMap data from the web.
Author: Sergey Vartanov (me@enzet.ru).
"""
import os import os
import re import re
import time import time

View file

@ -83,9 +83,15 @@ class OSMWay:
return self return self
def is_cycle(self) -> bool: def is_cycle(self) -> bool:
"""
Is way a cycle way or an area boundary.
"""
return self.nodes[0] == self.nodes[-1] return self.nodes[0] == self.nodes[-1]
def try_to_glue(self, other: "OSMWay"): def try_to_glue(self, other: "OSMWay"):
"""
Create new combined way if ways share endpoints.
"""
if self.nodes[0] == other.nodes[0]: if self.nodes[0] == other.nodes[0]:
return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes) return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes)
elif self.nodes[0] == other.nodes[-1]: elif self.nodes[0] == other.nodes[-1]:
@ -142,6 +148,9 @@ def get_value(key: str, text: str):
class Map: class Map:
"""
The whole OpenStreetMap information about nodes, ways, and relations.
"""
def __init__(self): def __init__(self):
self.node_map: Dict[int, OSMNode] = {} self.node_map: Dict[int, OSMNode] = {}
self.way_map: Dict[int, OSMWay] = {} self.way_map: Dict[int, OSMWay] = {}
@ -162,19 +171,18 @@ class OSMReader:
""" """
Parse OSM XML representation. Parse OSM XML representation.
""" """
print(f"Line number counting for {file_name}...") lines_number: int = sum(1 for _ in open(file_name))
with open(file_name) as f:
for lines_number, _ in enumerate(f):
pass
print("Done.")
print(f"Parsing OSM file {file_name}...") print(f"Parsing OSM file {file_name}...")
input_file = open(file_name) input_file = open(file_name)
line = input_file.readline() line = input_file.readline()
line_number = 0 line_number = 0
element = None
while line != "": while line != "":
line_number += 1 line_number += 1
ui.write_line(line_number, lines_number) ui.progress_bar(line_number, lines_number)
# Node parsing. # Node parsing.
@ -237,6 +245,6 @@ class OSMReader:
line = input_file.readline() line = input_file.readline()
input_file.close() input_file.close()
ui.write_line(-1, lines_number) # Complete progress bar. ui.progress_bar(-1, lines_number) # Complete progress bar.
return self.map_ return self.map_

View file

@ -1,11 +1,37 @@
"""
Röntgen drawing scheme.
Author: Sergey Vartanov (me@enzet.ru).
"""
import copy import copy
import yaml import yaml
from typing import Any, Dict, List from typing import Any, Dict, List, Optional, Set
from roentgen.extract_icon import DEFAULT_SHAPE_ID
DEFAULT_COLOR: str = "444444" DEFAULT_COLOR: str = "444444"
class IconSet:
"""
Node representation: icons and color.
"""
def __init__(
self, icons: List[List[str]], color: str, processed: Set[str],
is_default: bool):
"""
:param icons: list of lists of shape identifiers
:param color: fill color of all icons
:param processed: tag keys that were processed to create icon set (other
tag keys should be displayed by text or ignored)
"""
self.icons: List[List[str]] = icons
self.color: str = color
self.processed: Set[str] = processed
self.is_default = is_default
class Scheme: class Scheme:
""" """
Map style. Map style.
@ -22,6 +48,7 @@ class Scheme:
yaml.load(open(file_name).read(), Loader=yaml.FullLoader) yaml.load(open(file_name).read(), Loader=yaml.FullLoader)
self.tags: List[Dict[str, Any]] = content["tags"] self.tags: List[Dict[str, Any]] = content["tags"]
self.ways: List[Dict[str, Any]] = content["ways"]
self.colors: Dict[str, str] = content["colors"] self.colors: Dict[str, str] = content["colors"]
w3c_colors: Dict[str, str] = \ w3c_colors: Dict[str, str] = \
@ -34,7 +61,8 @@ class Scheme:
self.tags_to_skip: List[str] = content["tags_to_skip"] self.tags_to_skip: List[str] = content["tags_to_skip"]
self.prefix_to_skip: List[str] = content["prefix_to_skip"] self.prefix_to_skip: List[str] = content["prefix_to_skip"]
self.cache = {} # Storage for created icon sets.
self.cache: Dict[str, IconSet] = {}
def get_color(self, color: str) -> str: def get_color(self, color: str) -> str:
""" """
@ -81,16 +109,24 @@ class Scheme:
return True return True
return False return False
def get_icon(self, tags: Dict[str, Any]): def get_icon(self, tags: Dict[str, Any]) -> IconSet:
"""
Construct icon set.
:param tags: OpenStreetMap element tags dictionary
"""
tags_hash: str = \
",".join(tags.keys()) + ":" + \
",".join(map(lambda x: str(x), tags.values()))
tags_hash = ",".join(tags.keys()) + ":" + \
",".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]
main_icon = None
extra_icons = [] main_icon: Optional[List[str]] = None
extra_icons: List[List[str]] = []
processed = set() processed = set()
fill = DEFAULT_COLOR fill = DEFAULT_COLOR
for matcher in self.tags: for matcher in self.tags:
matched = True matched = True
for key in matcher["tags"]: for key in matcher["tags"]:
@ -118,7 +154,7 @@ class Scheme:
for key in matcher["tags"].keys(): for key in matcher["tags"].keys():
processed.add(key) processed.add(key)
if "add_icon" in matcher: if "add_icon" in matcher:
extra_icons += matcher["add_icon"] extra_icons += [matcher["add_icon"]]
for key in matcher["tags"].keys(): for key in matcher["tags"].keys():
processed.add(key) processed.add(key)
if "color" in matcher: if "color" in matcher:
@ -126,17 +162,24 @@ class Scheme:
for key in matcher["tags"].keys(): for key in matcher["tags"].keys():
processed.add(key) processed.add(key)
for color_name in ["color", "colour", "building:colour"]: for tag_key in tags: # type: str
if color_name in tags: if tag_key in ["color", "colour"] or tag_key.endswith(":color") or \
fill = self.get_color(tags[color_name]) tag_key.endswith(":colour"):
processed.add(color_name) fill = self.get_color(tags[tag_key])
processed.add(tag_key)
if main_icon: if main_icon:
returned = [main_icon] + extra_icons, fill, processed result_set: List[List[str]] = [main_icon] + extra_icons
else: else:
returned = extra_icons, fill, processed result_set: List[List[str]] = extra_icons
is_default: bool = False
if not result_set and tags:
result_set = [[DEFAULT_SHAPE_ID]]
is_default = True
returned: IconSet = IconSet(result_set, fill, processed, is_default)
self.cache[tags_hash] = returned self.cache[tags_hash] = returned
return returned return returned

View file

@ -4,7 +4,10 @@ Author: Sergey Vartanov (me@enzet.ru).
import argparse import argparse
import sys import sys
from typing import Optional from typing import List, Optional
BOXES: List[str] = [" ", "", "", "", "", "", "", ""]
BOXES_LENGTH: int = len(BOXES)
def parse_options(args): def parse_options(args):
@ -43,24 +46,44 @@ def parse_options(args):
action="store_false", action="store_false",
default=True) default=True)
parser.add_argument( parser.add_argument(
"-nw", "--no-draw-ways", dest="draw_ways", action="store_false", "-nw", "--no-draw-ways",
dest="draw_ways",
action="store_false",
default=True) default=True)
parser.add_argument( parser.add_argument(
"--captions", "--no-draw-captions", dest="draw_captions", "--captions", "--no-draw-captions",
dest="draw_captions",
default="main") default="main")
parser.add_argument( parser.add_argument(
"--show-missing-tags", dest="show_missing_tags", action="store_true") "--show-missing-tags",
dest="show_missing_tags",
action="store_true")
parser.add_argument( parser.add_argument(
"--no-show-missing-tags", dest="show_missing_tags", "--no-show-missing-tags",
dest="show_missing_tags",
action="store_false") action="store_false")
parser.add_argument("--overlap", dest="overlap", default=12, type=int)
parser.add_argument( parser.add_argument(
"--show-index", dest="show_index", action="store_true") "--overlap",
dest="overlap",
default=12,
type=int)
parser.add_argument( parser.add_argument(
"--no-show-index", dest="show_index", action="store_false") "--show-index",
parser.add_argument("--mode", default="normal") dest="show_index",
parser.add_argument("--seed", default="") action="store_true")
parser.add_argument("--level", default=None) parser.add_argument(
"--no-show-index",
dest="show_index",
action="store_false")
parser.add_argument(
"--mode",
default="normal")
parser.add_argument(
"--seed",
default="")
parser.add_argument(
"--level",
default=None)
arguments = parser.parse_args(args[1:]) arguments = parser.parse_args(args[1:])
@ -70,20 +93,27 @@ def parse_options(args):
return arguments return arguments
def write_line(number, total): def progress_bar(
length = 20 number: int, total: int, length: int = 20, step: int = 1000) -> None:
parts = length * 8 """
boxes = [" ", "", "", "", "", "", "", ""] Draw progress bar using Unicode symbols.
:param number: current value
:param total: maximum value
:param length: progress bar length.
:param step: frequency of progress bar updating (assuming that numbers go
subsequently)
"""
if number == -1: if number == -1:
print("%3s" % "100" + " %" + (length * "") + "") print(f"100 % {length * ''}")
elif number % 1000 == 0: elif number % step == 0:
p = number / float(total) ratio: float = number / total
l = int(p * parts) parts: int = int(ratio * length * BOXES_LENGTH)
fl = int(l / 8) fill_length: int = int(parts / BOXES_LENGTH)
pr = int(l - fl * 8) box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)]
print(("%3s" % str(int(p * 1000) / 10)) + " %" + (fl * "") + print(
boxes[pr] + ((length - fl - 1) * " ") + "") f"{str(int(int(ratio * 1000) / 10)):>3} % {fill_length * ''}{box}"
f"{int(length - fill_length - 1) * ' '}")
sys.stdout.write("\033[F") sys.stdout.write("\033[F")

5
run.py
View file

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

44
test.py
View file

@ -6,8 +6,8 @@ import os
import random import random
import yaml import yaml
from roentgen import extract_icon
from roentgen.flinger import map_ from roentgen.flinger import map_
from roentgen.grid import draw_grid
def test_flinger_map(): def test_flinger_map():
@ -15,44 +15,4 @@ def test_flinger_map():
def test_icons(): def test_icons():
tags_file_name = 'data/tags.yml' draw_grid()
icons_file_name = 'icons/icons.svg'
scheme = yaml.load(open(tags_file_name))
extracter = extract_icon.IconExtractor(icons_file_name)
to_draw = []
for element in scheme['tags']:
if 'icon' in element:
if not (set(element['icon']) in to_draw):
to_draw.append(set(element['icon']))
if 'add_icon' in element:
if not (set(element['add_icon']) in to_draw):
to_draw.append(set(element['add_icon']))
if 'over_icon' in element:
with_icons = []
if 'under_icon' in element:
for icon in element['under_icon']:
if not (set([icon] + element['over_icon']) in to_draw):
to_draw.append(set([icon] + element['over_icon']))
if 'under_icon' in element and 'with_icon' in element:
for icon in element['under_icon']:
for icon2 in element['with_icon']:
if not (set([icon] + [icon2] + element['over_icon']) in to_draw):
to_draw.append(set([icon] + [icon2] + element['over_icon']))
for icon2 in element['with_icon']:
for icon3 in element['with_icon']:
if icon2 != icon3 and icon2 != icon and icon3 != icon:
if not (set([icon] + [icon2] + [icon3] + element['over_icon']) in to_draw):
to_draw.append(set([icon] + [icon2] + [icon3] + element['over_icon']))
for icons_to_draw in to_draw:
icon_set = {'icons': []}
for icon in icons_to_draw:
path, xx, yy, is_shape = extracter.get_path(icon)
assert is_shape, icon
icon_set['icons'].append({'path': path,
'x': (- 8.0 - xx * 16),
'y': (- 8.0 - yy * 16)})