diff --git a/.idea/Roentgen.iml b/.idea/Roentgen.iml
index 4f2c9af..8dc09e5 100644
--- a/.idea/Roentgen.iml
+++ b/.idea/Roentgen.iml
@@ -5,10 +5,6 @@
-
-
-
-
diff --git a/data/tags.yml b/data/tags.yml
index a43de11..5247fa8 100644
--- a/data/tags.yml
+++ b/data/tags.yml
@@ -18,6 +18,7 @@ colors:
outline_color: "FFFFFF"
beach_color: "F0E0C0"
+ boundary_color: "880088"
building_color: "F8F0E8" # "D0D0C0"
building_border_color: "DDDDDD" # "AAAAAA"
construction_color: "CCCCCC"
@@ -232,6 +233,8 @@ tags:
icon: [cupcake]
- tags: {shop: mall}
icon: [bag]
+- tags: {shop: alcohol}
+ icon: [bottle]
- tags: {shop: mall, building: 'yes'}
icon: [bag]
- tags: {shop: convenience}
@@ -527,6 +530,291 @@ tags:
- tags: {'payment:credit_cards': 'yes'}
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: [
"operator", "opening_hours", "cuisine", "network", "website",
"website_2", "STIF:zone", "opening_hours:url", "phone",
diff --git a/doc/buildings.png b/doc/buildings.png
index c457d53..ba0e18f 100644
Binary files a/doc/buildings.png and b/doc/buildings.png differ
diff --git a/doc/grid.png b/doc/grid.png
index e76739a..5d93c79 100644
Binary files a/doc/grid.png and b/doc/grid.png differ
diff --git a/doc/time.png b/doc/time.png
index a7fe48b..7be805e 100644
Binary files a/doc/time.png and b/doc/time.png differ
diff --git a/doc/trees.png b/doc/trees.png
index e977f45..97c98e9 100644
Binary files a/doc/trees.png and b/doc/trees.png differ
diff --git a/doc/user.png b/doc/user.png
index b88d24b..744a0ab 100644
Binary files a/doc/user.png and b/doc/user.png differ
diff --git a/roentgen/constructor.py b/roentgen/constructor.py
index 1094679..540ebfc 100644
--- a/roentgen/constructor.py
+++ b/roentgen/constructor.py
@@ -5,10 +5,11 @@ from hashlib import sha256
from datetime import datetime
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.osm_reader import OSMMember, OSMRelation, OSMWay
-from roentgen.scheme import Scheme
+from roentgen.scheme import IconSet, Scheme
class Node:
@@ -16,17 +17,16 @@ class Node:
Node in Röntgen terms.
"""
def __init__(
- self, shapes, tags: Dict[str, str], x: float, y: float, color: str,
- path: Optional[str], processed, priority: int = 0):
- self.shapes = shapes
+ self, icon_set: IconSet, tags: Dict[str, str],
+ point: (float, float), path: Optional[str],
+ priority: int = 0, is_for_node: bool = True):
+ self.icon_set: IconSet = icon_set
self.tags = tags
- self.x = x
- self.y = y
- self.color = color
+ self.point = point
self.path = path
- self.processed = processed
self.priority = priority
self.layer = 0
+ self.is_for_node = is_for_node
class Way:
@@ -34,12 +34,12 @@ class Way:
Way in Röntgen terms.
"""
def __init__(
- self, kind: str, nodes, path, style, layer: float = 0.0,
- priority: float = 0, levels=None):
+ self, kind: str, nodes, path, style: Dict[str, Any],
+ layer: float = 0.0, priority: float = 0, levels=None):
self.kind = kind
self.nodes = nodes
self.path = path
- self.style = style
+ self.style: Dict[str, Any] = style
self.layer = layer
self.priority = priority
self.levels = levels
@@ -219,8 +219,10 @@ class Constructor:
nodes = None
+ center_point = None
+
if way:
- c = line_center(
+ center_point = line_center(
map(lambda x: self.map_.node_map[x], way.nodes), self.flinger)
nodes = way.nodes
@@ -230,8 +232,8 @@ class Constructor:
user_color = get_user_color(way.user, self.seed)
self.ways.append(
Way("way", nodes, path,
- f"fill:none;stroke:#{user_color};"
- f"stroke-width:1;"))
+ {"fill": "none", "stroke": "#" + user_color,
+ "stroke-width": 1}))
return
if self.mode == "time":
@@ -240,339 +242,69 @@ class Constructor:
time_color = get_time_color(way.timestamp)
self.ways.append(
Way("way", nodes, path,
- f"fill:none;stroke:#{time_color};"
- f"stroke-width:1;"))
+ {"fill": "none", "stroke": "#" + time_color,
+ "stroke-width": 1}))
return
- # Indoor features
+ if not tags:
+ return
- if "indoor" in tags:
- v = tags["indoor"]
- style = \
- 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
+ appended = False
+ kind: str = "way"
+ levels = None
if "building" in tags:
- layer += 40
- levels = 1
- if "building:levels" in tags:
- 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))
+ kind = "building"
+ if "building:levels" in tags:
+ levels = float(tags["building:levels"])
- # Amenity
-
- if "amenity" in tags:
- style = "fill:none;stroke:none;"
- layer += 21
- if tags["amenity"] == "parking":
- style = \
- f"fill:#{self.color('parking_color')};" \
- f"stroke:none;opacity:0.5;"
- shapes, fill, processed = self.scheme.get_icon(tags)
- if way:
+ for element in self.scheme.ways: # type: Dict[str, Any]
+ 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 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(
- shapes, tags, c[0], c[1], fill, path, processed, 1))
- self.ways.append(Way("way", nodes, path, style, layer, 50))
+ icon_set, tags, center_point, path, is_for_node=False))
+ 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:
- 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):
+ def construct_relations(self) -> None:
"""
Construct Röntgen ways from OSM relations.
"""
@@ -603,7 +335,7 @@ class Constructor:
p += path + " "
self.construct_way(None, tags, p)
- def construct_nodes(self):
+ def construct_nodes(self) -> None:
"""
Draw nodes.
"""
@@ -618,33 +350,27 @@ class Constructor:
for node_id in s: # type: int
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]
flung = self.flinger.fling(Geo(node.lat, node.lon))
- x = flung[0]
- y = flung[1]
tags = node.tags
if not self.check_level(tags):
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 not tags:
continue
- shapes = ["small"]
+ icon_set.icons = [[DEFAULT_SMALL_SHAPE_ID]]
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":
- fill = get_time_color(node.timestamp)
+ icon_set.color = get_time_color(node.timestamp)
- if shapes == [] and tags != {}:
- shapes = [["no"]]
+ self.nodes.append(Node(icon_set, tags, flung, None))
- self.nodes.append(Node(
- shapes, tags, x, y, fill, None, processed))
-
- ui.write_line(-1, len(self.map_.node_map))
+ ui.progress_bar(-1, len(self.map_.node_map))
print("Nodes painted in " + str(datetime.now() - start_time) + ".")
diff --git a/roentgen/extract_icon.py b/roentgen/extract_icon.py
index c96e899..bba7f0c 100644
--- a/roentgen/extract_icon.py
+++ b/roentgen/extract_icon.py
@@ -5,11 +5,52 @@ Author: Sergey Vartanov (me@enzet.ru).
"""
import re
import xml.dom.minidom
-
from typing import Dict
+import numpy as np
+from svgwrite import Drawing
+
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:
"""
@@ -22,7 +63,7 @@ class IconExtractor:
:param svg_file_name: input SVG file name with icons. File may contain
any other irrelevant graphics.
"""
- self.icons: Dict[str, (str, float, float)] = {}
+ self.icons: Dict[str, Icon] = {}
with open(svg_file_name) as input_file:
content = xml.dom.minidom.parse(input_file)
@@ -38,35 +79,39 @@ class IconExtractor:
:param node: XML node that contains icon
"""
- 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:
+ if node.nodeName != "path":
for sub_node in node.childNodes:
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.
- :param id_: string icon ID
+ :param id_: string icon identifier
"""
if id_ in self.icons:
- return list(self.icons[id_]) + [True]
- else:
- if id_ == "no":
- return "M 4,4 L 4,10 10,10 10,4 z", 0, 0, 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
+ return self.icons[id_], True
+
+ ui.error(f"no such icon ID {id_}")
+ return self.icons[DEFAULT_SHAPE_ID], False
diff --git a/roentgen/flinger.py b/roentgen/flinger.py
index 6a20b6b..c9d8124 100644
--- a/roentgen/flinger.py
+++ b/roentgen/flinger.py
@@ -1,98 +1,46 @@
-#!/usr/bin/env python
-
"""
Author: Sergey Vartanov (me@enzet.ru)
"""
-
import math
import numpy as np
+from typing import Optional
def get_ratio(maximum, minimum, ratio: float = 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__(
- self, minimum, maximum, target_minimum=None, target_maximum=None,
- ratio=None):
-
- 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]
+ return \
+ target_min + (value - current_min) / (current_max - current_min) * \
+ (target_max - target_min)
class Geo:
- def __init__(self, lat, lon):
- self.lat = lat
- self.lon = lon
+ def __init__(self, lat: float, lon: float):
+ self.lat: float = lat
+ self.lon: float = lon
- def __getitem__(self, item):
+ def __getitem__(self, item) -> Optional[float]:
if item == 0:
return self.lon
if item == 1:
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)
- def __sub__(self, other):
+ def __sub__(self, other: "Geo") -> "Geo":
return Geo(self.lat - other.lat, self.lon - other.lon)
- def __repr__(self):
+ def __repr__(self) -> str:
return f"{self.lat}, {self.lon}"
@@ -157,15 +105,3 @@ class GeoFlinger:
self.minimum.lat, self.maximum.lat,
self.target_minimum[1], self.target_maximum[1])
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)
diff --git a/roentgen/grid.py b/roentgen/grid.py
index 2bfac0b..b230e62 100644
--- a/roentgen/grid.py
+++ b/roentgen/grid.py
@@ -1,24 +1,15 @@
"""
Author: Sergey Vartanov (me@enzet.ru).
"""
-
+import numpy as np
import os
import random
import svgwrite
import yaml
-from roentgen import extract_icon
+from roentgen.extract_icon import Icon, IconExtractor
-from typing import Any, Dict
-
-
-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})'))
+from typing import List
def draw_grid():
@@ -47,8 +38,7 @@ def draw_grid():
width: float = 24 * 16
- x: float = step / 2
- y: float = step / 2
+ point: np.array = np.array((step / 2, step / 2))
to_draw = []
@@ -83,20 +73,19 @@ def draw_grid():
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:
- drawed = False
- icon_set = {"icons": []}
- for icon in icons_to_draw:
- path, xx, yy, _ = extractor.get_path(icon)
- icon_set["icons"].append({"path": path,
- "x": (- 8.0 - xx * 16),
- "y": (- 8.0 - yy * 16)})
- drawed = True
- if drawed:
+ found: bool = False
+ icon_set: List[Icon] = []
+ for icon_id in icons_to_draw: # type: str
+ icon, got = extractor.get_path(icon_id)
+ assert got
+ icon_set.append(icon)
+ found = True
+ if found:
icons.append(icon_set)
number += 1
@@ -109,13 +98,16 @@ def draw_grid():
for icon in icons:
background_color, foreground_color = random.choice(icon_colors)
svg.add(svg.rect(
- (x - 2 - 8, y - 2 - 8), (20, 20), fill=f"#{background_color}"))
- for i in icon["icons"]:
- draw_icon(svg, i, x, y, foreground_color)
- x += step
- if x > width - 8:
- x = step / 2
- y += step
+ point - np.array((-10, -10)), (20, 20),
+ fill=f"#{background_color}"))
+ for i in icon: # type: Icon
+ path = i.get_path(svg, point)
+ path.update({"fill": f"#{foreground_color}"})
+ svg.add(path)
+ point += np.array((step, 0))
+ if point[0] > width - 8:
+ point[0] = step / 2
+ point += np.array((0, step))
height += step
print(f"Icons: {number}.")
diff --git a/roentgen/mapper.py b/roentgen/mapper.py
index 21a2758..1245547 100644
--- a/roentgen/mapper.py
+++ b/roentgen/mapper.py
@@ -7,19 +7,18 @@ import numpy as np
import os
import svgwrite
import sys
-import yaml
from svgwrite.container import Group
from svgwrite.path import Path
from svgwrite.shapes import Circle, Rect
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.constructor import Constructor, get_path
+from roentgen.constructor import Constructor, get_path, Node, Way
from roentgen.flinger import GeoFlinger, Geo
from roentgen.grid import draw_grid
+from roentgen.extract_icon import Icon, IconExtractor
from roentgen.osm_getter import get_osm
from roentgen.osm_reader import Map, OSMReader
from roentgen.scheme import Scheme
@@ -29,70 +28,97 @@ TAGS_FILE_NAME: str = "data/tags.yml"
COLORS_FILE_NAME: str = "data/colors.yml"
MISSING_TAGS_FILE_NAME: str = "missing_tags.yml"
+AUTHOR_MODE = "user-coloring"
+CREATION_TIME_MODE = "time"
+
class Painter:
-
+ """
+ Map drawing.
+ """
def __init__(
- self, show_missing_tags, overlap, draw_nodes, mode, draw_captions,
- map_, flinger, svg: svgwrite.Drawing, icons, scheme: Scheme):
+ self, show_missing_tags: bool, overlap: int, draw_nodes: bool,
+ 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.overlap = overlap
- self.draw_nodes = draw_nodes
- self.mode = mode
+ self.show_missing_tags: bool = show_missing_tags
+ self.overlap: int = overlap
+ self.draw_nodes: bool = draw_nodes
+ self.mode: str = mode
self.draw_captions = draw_captions
- self.map_ = map_
- self.flinger = flinger
+ self.map_: Map = map_
+ self.flinger: GeoFlinger = flinger
self.svg: svgwrite.Drawing = svg
- self.icons = icons
+ self.icon_extractor = icon_extractor
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:
- for shape in shapes:
+ for shape_ids in node.icon_set.icons:
has_space = True
for p in points[-1000:]:
- if x + xxx - self.overlap <= p[0] <= x + xxx + self.overlap and \
- y - self.overlap <= p[1] <= y + self.overlap:
+ if node.point[0] + left - self.overlap <= p[0] \
+ <= node.point[0] + left + self.overlap and \
+ node.point[1] - self.overlap <= p[1] \
+ <= node.point[1] + self.overlap:
has_space = False
break
if has_space:
- self.draw_point_shape(shape, x + xxx, y, fill, tags=tags)
- points.append([x + xxx, y])
- xxx += 16
+ self.draw_point_shape(
+ shape_ids, (node.point[0] + left, node.point[1]),
+ node.icon_set.color, tags=node.tags)
+ points.append([node.point[0] + left, node.point[1]])
+ left += 16
else:
- for shape in shapes:
- self.draw_point_shape(shape, x + xxx, y, fill, tags=tags)
- xxx += 16
-
- def draw_texts(self, shapes, points, x, y, fill, tags, processed):
-
- if self.draw_captions == "no":
- return
+ for shape_ids in node.icon_set.icons:
+ self.draw_point_shape(
+ shape_ids, (node.point[0] + left, node.point[1]),
+ node.icon_set.color, tags=node.tags)
+ left += 16
+ def draw_texts(self, node: Node):
+ """
+ Draw all labels.
+ """
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:
fill = text_struct["fill"] if "fill" in text_struct else "444444"
size = text_struct["size"] if "size" in text_struct else 10
text_y += size + 1
- self.wr(text_struct["text"], x, y, fill, text_y, size=size)
+ text = text_struct["text"]
+ text = text.replace(""", '"')
+ text = text.replace("&", '&')
+ 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:
- for k in tags:
- if not self.no_draw(k) and k not in processed:
- text = k + ": " + tags[k]
- self.draw_text(text, x, float(y) + text_y + 18, "734A08")
+ 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),
+ "734A08")
text_y += 10
- def draw_text(self, text: str, x, y, fill, size=10, out_fill="FFFFFF",
- out_opacity=1.0, out_fill_2=None, out_opacity_2=1.0):
+ def draw_text(
+ 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.
@@ -104,27 +130,24 @@ class Painter:
"""
if out_fill_2:
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}",
stroke_linejoin="round", stroke_width=5,
stroke=f"#{out_fill_2}", opacity=out_opacity_2))
if out_fill:
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}",
stroke_linejoin="round", stroke_width=3,
stroke=f"#{out_fill}", opacity=out_opacity))
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}"))
- 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):
- for key in tags:
- tags[key] = tags[key].replace(""", '"')
+ """
+ Construct labels for not processed tags.
+ """
texts = []
address: List[str] = []
name = None
@@ -207,8 +230,7 @@ class Painter:
texts.append({"text": tags[k], "fill": "444444"})
tags.pop(k)
for tag in tags:
- if self.to_write(tag) and not (tag in processed):
- # texts.append({"text": tag + ": " + tags[tag]})
+ if self.scheme.is_writable(tag) and not (tag in processed):
texts.append({"text": tags[tag]})
return texts
@@ -250,21 +272,27 @@ class Painter:
pass
def draw(self, nodes, ways, points):
-
+ """
+ Draw map.
+ """
ways = sorted(ways, key=lambda x: x.layer)
for way in ways:
if way.kind == "way":
if way.nodes:
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:
- 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 = Group(opacity=0.1)
- for way in ways:
+ for way in ways: # type: Way
if way.kind != "building" or not way.nodes:
continue
shift = [-5, 5]
@@ -290,7 +318,7 @@ class Painter:
# Building roof
- for way in ways:
+ for way in ways: # type: Way
if way.kind != "building":
continue
if way.nodes:
@@ -298,9 +326,13 @@ class Painter:
if way.levels:
shift = [0 * way.levels, min(-3, -1 * way.levels)]
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:
- 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
@@ -310,58 +342,59 @@ class Painter:
"diameter_crown" in node.tags):
continue
self.svg.add(Circle(
- (float(node.x), float(node.y)),
- float(node.tags["diameter_crown"]) * 1.2,
+ node.point, float(node.tags["diameter_crown"]) * 1.2,
fill="#688C44", stroke="#688C44", opacity=0.3))
# All other nodes
nodes = sorted(nodes, key=lambda x: x.layer)
- for node in nodes:
+ for node in nodes: # type: Node
if "natural" in node.tags and \
node.tags["natural"] == "tree" and \
"diameter_crown" in node.tags:
continue
- self.draw_shapes(
- node.shapes, points, node.x, node.y, node.color, node.tags,
- node.processed)
+ self.draw_shapes(node, points)
- for node in nodes:
- if self.mode not in ["time", "user-coloring"]:
- self.draw_texts(
- node.shapes, points, node.x, node.y, node.color,
- node.tags, node.processed)
+ if self.draw_captions == "no":
+ return
- def draw_point_shape(self, name, x, y, fill, tags=None):
- if not isinstance(name, list):
- name = [name]
- 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)
+ for node in nodes: # type: Node
+ if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
+ self.draw_texts(node)
- def draw_point(self, shape, x, y, fill, size=16, xx=0, yy=0, tags=None):
- x = int(float(x))
- y = int(float(y))
- path = self.svg.path(
- d=shape, fill=f"#{fill}", fill_opacity=1,
- transform=f"translate({x - size / 2.0 - xx * 16},"
- f"{y - size / 2.0 - yy * 16})")
- path.set_desc(title="\n".join(map(lambda x: x + ": " + tags[x], tags)))
+ def draw_point_shape(self, shape_ids: List[str], point, fill, tags=None):
+ """
+ Draw one icon.
+ """
+ if self.mode not in [CREATION_TIME_MODE, AUTHOR_MODE]:
+ for shape_id in shape_ids: # type: str
+ icon, _ = self.icon_extractor.get_path(shape_id)
+ 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)
def draw_point_outline(
- self, shape, x, y, fill, mode="default", size=16, xx=0, yy=0):
- x = int(float(x))
- y = int(float(y))
+ self, icon: Icon, point, fill, mode="default", size=16):
+
+ point = np.array(list(map(lambda x: int(x), point)))
+
opacity = 0.5
stroke_width = 2.2
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)
g = int(fill[2:4], 16)
b = int(fill[4:6], 16)
@@ -369,12 +402,13 @@ class Painter:
if Y > 200:
outline_fill = "000000"
opacity = 0.7
- self.svg.add(self.svg.path(
- d=shape, fill=f"#{outline_fill}", opacity=opacity,
- stroke=f"#{outline_fill}", stroke_width=stroke_width,
- stroke_linejoin="round",
- transform=f"translate({x - size / 2.0 - xx * 16},"
- f"{y - size / 2.0 - yy * 16})"))
+
+ path = icon.get_path(self.svg, point)
+ path.update({
+ "fill": f"#{outline_fill}", "opacity": opacity,
+ "stroke": f"#{outline_fill}", "stroke-width": stroke_width,
+ "stroke-linejoin": "round"})
+ self.svg.add(path)
def check_level_number(tags, level):
@@ -395,6 +429,14 @@ def check_level_overground(tags):
for level in levels:
if level <= 0:
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
@@ -410,7 +452,7 @@ def main():
sys.exit(1)
background_color = "#EEEEEE"
- if options.mode in ["user-coloring", "time"]:
+ if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
background_color = "#111111"
if options.input_file_name:
@@ -426,7 +468,7 @@ def main():
full = False # Full keys getting
- if options.mode in ["user-coloring", "time"]:
+ if options.mode in [AUTHOR_MODE, CREATION_TIME_MODE]:
full = True
osm_reader = OSMReader()
@@ -460,7 +502,7 @@ def main():
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):
""" Draw objects on all levels. """
@@ -489,7 +531,7 @@ def main():
show_missing_tags=options.show_missing_tags, overlap=options.overlap,
draw_nodes=options.draw_nodes, mode=options.mode,
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)
painter.draw(constructor.nodes, constructor.ways, points)
diff --git a/roentgen/osm_getter.py b/roentgen/osm_getter.py
index 2d61146..d80fe65 100644
--- a/roentgen/osm_getter.py
+++ b/roentgen/osm_getter.py
@@ -1,3 +1,8 @@
+"""
+Getting OpenStreetMap data from the web.
+
+Author: Sergey Vartanov (me@enzet.ru).
+"""
import os
import re
import time
diff --git a/roentgen/osm_reader.py b/roentgen/osm_reader.py
index 138a5de..89509c3 100644
--- a/roentgen/osm_reader.py
+++ b/roentgen/osm_reader.py
@@ -83,9 +83,15 @@ class OSMWay:
return self
def is_cycle(self) -> bool:
+ """
+ Is way a cycle way or an area boundary.
+ """
return self.nodes[0] == self.nodes[-1]
def try_to_glue(self, other: "OSMWay"):
+ """
+ Create new combined way if ways share endpoints.
+ """
if self.nodes[0] == other.nodes[0]:
return OSMWay(nodes=list(reversed(other.nodes[1:])) + self.nodes)
elif self.nodes[0] == other.nodes[-1]:
@@ -142,6 +148,9 @@ def get_value(key: str, text: str):
class Map:
+ """
+ The whole OpenStreetMap information about nodes, ways, and relations.
+ """
def __init__(self):
self.node_map: Dict[int, OSMNode] = {}
self.way_map: Dict[int, OSMWay] = {}
@@ -162,19 +171,18 @@ class OSMReader:
"""
Parse OSM XML representation.
"""
- print(f"Line number counting for {file_name}...")
- with open(file_name) as f:
- for lines_number, _ in enumerate(f):
- pass
- print("Done.")
+ lines_number: int = sum(1 for _ in open(file_name))
+
print(f"Parsing OSM file {file_name}...")
input_file = open(file_name)
line = input_file.readline()
line_number = 0
+ element = None
+
while line != "":
line_number += 1
- ui.write_line(line_number, lines_number)
+ ui.progress_bar(line_number, lines_number)
# Node parsing.
@@ -237,6 +245,6 @@ class OSMReader:
line = input_file.readline()
input_file.close()
- ui.write_line(-1, lines_number) # Complete progress bar.
+ ui.progress_bar(-1, lines_number) # Complete progress bar.
return self.map_
diff --git a/roentgen/scheme.py b/roentgen/scheme.py
index 9b212d6..b1dd81a 100644
--- a/roentgen/scheme.py
+++ b/roentgen/scheme.py
@@ -1,11 +1,37 @@
+"""
+Röntgen drawing scheme.
+
+Author: Sergey Vartanov (me@enzet.ru).
+"""
import copy
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"
+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:
"""
Map style.
@@ -22,6 +48,7 @@ class Scheme:
yaml.load(open(file_name).read(), Loader=yaml.FullLoader)
self.tags: List[Dict[str, Any]] = content["tags"]
+ self.ways: List[Dict[str, Any]] = content["ways"]
self.colors: Dict[str, str] = content["colors"]
w3c_colors: Dict[str, str] = \
@@ -34,7 +61,8 @@ class Scheme:
self.tags_to_skip: List[str] = content["tags_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:
"""
@@ -81,16 +109,24 @@ class Scheme:
return True
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:
return self.cache[tags_hash]
- main_icon = None
- extra_icons = []
+
+ main_icon: Optional[List[str]] = None
+ extra_icons: List[List[str]] = []
processed = set()
fill = DEFAULT_COLOR
+
for matcher in self.tags:
matched = True
for key in matcher["tags"]:
@@ -118,7 +154,7 @@ class Scheme:
for key in matcher["tags"].keys():
processed.add(key)
if "add_icon" in matcher:
- extra_icons += matcher["add_icon"]
+ extra_icons += [matcher["add_icon"]]
for key in matcher["tags"].keys():
processed.add(key)
if "color" in matcher:
@@ -126,17 +162,24 @@ class Scheme:
for key in matcher["tags"].keys():
processed.add(key)
- for color_name in ["color", "colour", "building:colour"]:
- if color_name in tags:
- fill = self.get_color(tags[color_name])
- processed.add(color_name)
+ for tag_key in tags: # type: str
+ if tag_key in ["color", "colour"] or tag_key.endswith(":color") or \
+ tag_key.endswith(":colour"):
+ fill = self.get_color(tags[tag_key])
+ processed.add(tag_key)
if main_icon:
- returned = [main_icon] + extra_icons, fill, processed
+ result_set: List[List[str]] = [main_icon] + extra_icons
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
return returned
-
diff --git a/roentgen/ui.py b/roentgen/ui.py
index daef08e..210000d 100644
--- a/roentgen/ui.py
+++ b/roentgen/ui.py
@@ -4,7 +4,10 @@ Author: Sergey Vartanov (me@enzet.ru).
import argparse
import sys
-from typing import Optional
+from typing import List, Optional
+
+BOXES: List[str] = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
+BOXES_LENGTH: int = len(BOXES)
def parse_options(args):
@@ -43,24 +46,44 @@ def parse_options(args):
action="store_false",
default=True)
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)
parser.add_argument(
- "--captions", "--no-draw-captions", dest="draw_captions",
+ "--captions", "--no-draw-captions",
+ dest="draw_captions",
default="main")
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(
- "--no-show-missing-tags", dest="show_missing_tags",
+ "--no-show-missing-tags",
+ dest="show_missing_tags",
action="store_false")
- parser.add_argument("--overlap", dest="overlap", default=12, type=int)
parser.add_argument(
- "--show-index", dest="show_index", action="store_true")
+ "--overlap",
+ dest="overlap",
+ default=12,
+ type=int)
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)
+ "--show-index",
+ dest="show_index",
+ action="store_true")
+ 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:])
@@ -70,20 +93,27 @@ def parse_options(args):
return arguments
-def write_line(number, total):
- length = 20
- parts = length * 8
- boxes = [" ", "▏", "▎", "▍", "▌", "▋", "▊", "▉"]
+def progress_bar(
+ number: int, total: int, length: int = 20, step: int = 1000) -> None:
+ """
+ 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:
- print("%3s" % "100" + " % █" + (length * "█") + "█")
- elif number % 1000 == 0:
- p = number / float(total)
- l = int(p * parts)
- fl = int(l / 8)
- pr = int(l - fl * 8)
- print(("%3s" % str(int(p * 1000) / 10)) + " % █" + (fl * "█") +
- boxes[pr] + ((length - fl - 1) * " ") + "█")
+ print(f"100 % {length * '█'}▏")
+ elif number % step == 0:
+ ratio: float = number / total
+ parts: int = int(ratio * length * BOXES_LENGTH)
+ fill_length: int = int(parts / BOXES_LENGTH)
+ box: str = BOXES[int(parts - fill_length * BOXES_LENGTH)]
+ print(
+ f"{str(int(int(ratio * 1000) / 10)):>3} % {fill_length * '█'}{box}"
+ f"{int(length - fill_length - 1) * ' '}▏")
sys.stdout.write("\033[F")
diff --git a/run.py b/run.py
index 98a3ffb..119633d 100644
--- a/run.py
+++ b/run.py
@@ -1,3 +1,8 @@
+"""
+Röntgen entry point.
+
+Author: Sergey Vartanov (me@enzet.ru).
+"""
from roentgen.mapper import main
if __name__ == "__main__":
diff --git a/test.py b/test.py
index 36582cf..4bc5542 100644
--- a/test.py
+++ b/test.py
@@ -6,8 +6,8 @@ import os
import random
import yaml
-from roentgen import extract_icon
from roentgen.flinger import map_
+from roentgen.grid import draw_grid
def test_flinger_map():
@@ -15,44 +15,4 @@ def test_flinger_map():
def test_icons():
- tags_file_name = 'data/tags.yml'
- 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)})
+ draw_grid()