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()