Issue #81: support lane drawing.

This commit is contained in:
Sergey Vartanov 2021-09-15 07:50:55 +03:00
parent cc9826ae57
commit 0a7bead1ca
6 changed files with 97 additions and 15 deletions

View file

@ -29,7 +29,13 @@ from map_machine.icon import (
ShapeSpecification, ShapeSpecification,
) )
from map_machine.map_configuration import DrawingMode, MapConfiguration from map_machine.map_configuration import DrawingMode, MapConfiguration
from map_machine.osm_reader import OSMData, OSMNode, OSMRelation, OSMWay from map_machine.osm_reader import (
OSMData,
OSMNode,
OSMRelation,
OSMWay,
parse_levels,
)
from map_machine.point import Point from map_machine.point import Point
from map_machine.scheme import DEFAULT_COLOR, LineStyle, RoadMatcher, Scheme from map_machine.scheme import DEFAULT_COLOR, LineStyle, RoadMatcher, Scheme
from map_machine.text import Label from map_machine.text import Label
@ -478,8 +484,7 @@ class Constructor:
def check_level_number(tags: dict[str, Any], level: float) -> bool: def check_level_number(tags: dict[str, Any], level: float) -> bool:
"""Check if element described by tags is no the specified level.""" """Check if element described by tags is no the specified level."""
if "level" in tags: if "level" in tags:
levels: map = map(float, tags["level"].replace(",", ".").split(";")) if level not in parse_levels(tags["level"]):
if level not in levels:
return False return False
else: else:
return False return False

View file

@ -5,6 +5,7 @@ from typing import Any, Iterator, Optional
import numpy as np import numpy as np
from colour import Color from colour import Color
from shapely.geometry import LineString
from svgwrite import Drawing from svgwrite import Drawing
from svgwrite.container import Group from svgwrite.container import Group
from svgwrite.path import Path from svgwrite.path import Path
@ -42,24 +43,38 @@ class Figure(Tagged):
) )
def get_path( def get_path(
self, flinger: Flinger, shift: np.ndarray = np.array((0, 0)) self, flinger: Flinger, offset: np.ndarray = np.array((0, 0))
) -> str: ) -> str:
""" """
Get SVG path commands. Get SVG path commands.
:param flinger: converter for geo coordinates :param flinger: converter for geo coordinates
:param shift: shift vector :param offset: offset vector
""" """
path: str = "" path: str = ""
for outer_nodes in self.outers: for outer_nodes in self.outers:
path += f"{get_path(outer_nodes, shift, flinger)} " path += f"{get_path(outer_nodes, offset, flinger)} "
for inner_nodes in self.inners: for inner_nodes in self.inners:
path += f"{get_path(inner_nodes, shift, flinger)} " path += f"{get_path(inner_nodes, offset, flinger)} "
return path return path
def get_outer_path(
self, flinger: Flinger, parallel_offset: float = 0
) -> str:
"""Get path of the first outer node list."""
points: list[tuple[float, float]] = [
tuple(flinger.fling(x.coordinates)) for x in self.outers[0]
]
offset = LineString(points).parallel_offset(parallel_offset)
path: str = ""
for index, point in enumerate(offset.coords):
path += ("L" if index else "M") + f" {point[0]},{point[1]} "
return path[:-1]
class Building(Figure): class Building(Figure):
""" """
@ -281,6 +296,25 @@ class Road(Figure):
path.update(style) path.update(style)
svg.add(path) svg.add(path)
def draw_lanes(self, svg: Drawing, flinger: Flinger, color: Color) -> None:
scale: float = flinger.get_scale(self.outers[0][0].coordinates)
if len(self.lanes) < 2:
return
for index in range(1, len(self.lanes)):
shift = scale * (
-self.width / 2 + index * self.width / len(self.lanes)
)
path: Path = Path(d=self.get_outer_path(flinger, shift))
style: dict[str, Any] = {
"fill": "none",
"stroke": color.hex,
"stroke-linejoin": "round",
"stroke-width": 1,
"opacity": 0.5,
}
path.update(style)
svg.add(path)
class Crater(Tagged): class Crater(Tagged):
""" """

View file

@ -77,11 +77,17 @@ class Map:
layered_roads[road.layer].append(road) layered_roads[road.layer].append(road)
for layer in sorted(layered_roads.keys()): for layer in sorted(layered_roads.keys()):
roads = layered_roads[layer] roads = sorted(
layered_roads[layer], key=lambda x: x.matcher.priority
)
for road in roads: for road in roads:
road.draw(self.svg, self.flinger, road.matcher.border_color, 2) road.draw(self.svg, self.flinger, road.matcher.border_color, 2)
for road in roads: for road in roads:
road.draw(self.svg, self.flinger, road.matcher.color) road.draw(self.svg, self.flinger, road.matcher.color)
for road in roads:
road.draw_lanes(
self.svg, self.flinger, road.matcher.border_color
)
for tree in constructor.trees: for tree in constructor.trees:
tree.draw(self.svg, self.flinger, self.scheme) tree.draw(self.svg, self.flinger, self.scheme)

View file

@ -2,6 +2,7 @@
Parse OSM XML file. Parse OSM XML file.
""" """
import json import json
import logging
import re import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime from datetime import datetime
@ -20,9 +21,9 @@ __email__ = "me@enzet.ru"
OSM_TIME_PATTERN: str = "%Y-%m-%dT%H:%M:%SZ" OSM_TIME_PATTERN: str = "%Y-%m-%dT%H:%M:%SZ"
METERS_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*m$") METERS_PATTERN: re.Pattern = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*m$")
KILOMETERS_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*km$") KILOMETERS_PATTERN: re.Pattern = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*km$")
MILES_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*mi$") MILES_PATTERN: re.Pattern = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*mi$")
# See https://wiki.openstreetmap.org/wiki/Lifecycle_prefix#Stages_of_decay # See https://wiki.openstreetmap.org/wiki/Lifecycle_prefix#Stages_of_decay
@ -46,6 +47,16 @@ def parse_float(string: str) -> Optional[float]:
return None return None
def parse_levels(string: str) -> list[float]:
"""Parse string representation of level sequence value."""
# TODO: add `-` parsing
try:
return list(map(float, string.replace(",", ".").split(";")))
except ValueError:
logging.warning(f"Cannot parse level description from `{string}`.")
return []
@dataclass @dataclass
class Tagged: class Tagged:
""" """
@ -320,7 +331,7 @@ class OSMData:
if node.user: if node.user:
self.authors.add(node.user) self.authors.add(node.user)
if node.tags.get("level"): if node.tags.get("level"):
self.levels.add(float(node.tags.get("level"))) self.levels.union(parse_levels(node.tags["level"]))
self.time.update(node.timestamp) self.time.update(node.timestamp)
def add_way(self, way: OSMWay) -> None: def add_way(self, way: OSMWay) -> None:
@ -333,7 +344,7 @@ class OSMData:
if way.user: if way.user:
self.authors.add(way.user) self.authors.add(way.user)
if way.tags.get("level"): if way.tags.get("level"):
self.levels.union(map(float, way.tags["level"].split(";"))) self.levels.union(parse_levels(way.tags["level"]))
self.time.update(way.timestamp) self.time.update(way.timestamp)
def add_relation(self, relation: OSMRelation) -> None: def add_relation(self, relation: OSMRelation) -> None:

View file

@ -19,8 +19,7 @@ colors:
barley_dark_color: "#908F62" barley_dark_color: "#908F62"
sunflower_dark_color: "#DEAC4A" sunflower_dark_color: "#DEAC4A"
# motorway_border_color: "#CC8800" motorway_border_color: "#CC8800"
motorway_border_color: "#FFFFFF"
motorway_color: "#FFAA33" motorway_color: "#FFAA33"
primary_border_color: "#AA8800" primary_border_color: "#AA8800"
primary_color: "#FFDD66" primary_color: "#FFDD66"
@ -1449,11 +1448,21 @@ roads:
border_color: secondary_border_color border_color: secondary_border_color
priority: 41.8 priority: 41.8
color: secondary_color color: secondary_color
- tags: {highway: secondary_link}
default_width: 9
border_color: secondary_border_color
priority: 41.8
color: secondary_color
- tags: {highway: tertiary} - tags: {highway: tertiary}
default_width: 7 default_width: 7
border_color: tertiary_border_color border_color: tertiary_border_color
priority: 41.7 priority: 41.7
color: tertiary_color color: tertiary_color
- tags: {highway: tertiary_link}
default_width: 7
border_color: tertiary_border_color
priority: 41.7
color: tertiary_color
- tags: {highway: unclassified} - tags: {highway: unclassified}
default_width: 5 default_width: 5
border_color: road_border_color border_color: road_border_color

View file

@ -9,6 +9,7 @@ from map_machine.osm_reader import (
OSMReader, OSMReader,
OSMRelation, OSMRelation,
OSMWay, OSMWay,
parse_levels,
) )
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -104,3 +105,19 @@ def test_relation() -> None:
assert len(relation.members) == 1 assert len(relation.members) == 1
assert relation.members[0].type_ == "way" assert relation.members[0].type_ == "way"
assert relation.members[0].ref == 2 assert relation.members[0].ref == 2
def test_parse_levels() -> None:
"""Test level parsing."""
assert parse_levels("1") == [1]
assert parse_levels("-1") == [-1]
assert parse_levels("1.5") == [1.5]
assert parse_levels("1,5") == [1.5]
def test_parse_levels_list() -> None:
"""Test list of levels parsing."""
assert parse_levels("0;1") == [0, 1]
assert parse_levels("0;2") == [0, 2]
assert parse_levels("0;2.5") == [0, 2.5]
assert parse_levels("0;2,5") == [0, 2.5]