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,
)
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.scheme import DEFAULT_COLOR, LineStyle, RoadMatcher, Scheme
from map_machine.text import Label
@ -478,8 +484,7 @@ class Constructor:
def check_level_number(tags: dict[str, Any], level: float) -> bool:
"""Check if element described by tags is no the specified level."""
if "level" in tags:
levels: map = map(float, tags["level"].replace(",", ".").split(";"))
if level not in levels:
if level not in parse_levels(tags["level"]):
return False
else:
return False

View file

@ -5,6 +5,7 @@ from typing import Any, Iterator, Optional
import numpy as np
from colour import Color
from shapely.geometry import LineString
from svgwrite import Drawing
from svgwrite.container import Group
from svgwrite.path import Path
@ -42,24 +43,38 @@ class Figure(Tagged):
)
def get_path(
self, flinger: Flinger, shift: np.ndarray = np.array((0, 0))
self, flinger: Flinger, offset: np.ndarray = np.array((0, 0))
) -> str:
"""
Get SVG path commands.
:param flinger: converter for geo coordinates
:param shift: shift vector
:param offset: offset vector
"""
path: str = ""
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:
path += f"{get_path(inner_nodes, shift, flinger)} "
path += f"{get_path(inner_nodes, offset, flinger)} "
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):
"""
@ -281,6 +296,25 @@ class Road(Figure):
path.update(style)
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):
"""

View file

@ -77,11 +77,17 @@ class Map:
layered_roads[road.layer].append(road)
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:
road.draw(self.svg, self.flinger, road.matcher.border_color, 2)
for road in roads:
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:
tree.draw(self.svg, self.flinger, self.scheme)

View file

@ -2,6 +2,7 @@
Parse OSM XML file.
"""
import json
import logging
import re
from dataclasses import dataclass, field
from datetime import datetime
@ -20,9 +21,9 @@ __email__ = "me@enzet.ru"
OSM_TIME_PATTERN: str = "%Y-%m-%dT%H:%M:%SZ"
METERS_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*m$")
KILOMETERS_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*km$")
MILES_PATTERN = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*mi$")
METERS_PATTERN: re.Pattern = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*m$")
KILOMETERS_PATTERN: re.Pattern = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*km$")
MILES_PATTERN: re.Pattern = re.compile("^(?P<value>\\d*\\.?\\d*)\\s*mi$")
# See https://wiki.openstreetmap.org/wiki/Lifecycle_prefix#Stages_of_decay
@ -46,6 +47,16 @@ def parse_float(string: str) -> Optional[float]:
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
class Tagged:
"""
@ -320,7 +331,7 @@ class OSMData:
if node.user:
self.authors.add(node.user)
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)
def add_way(self, way: OSMWay) -> None:
@ -333,7 +344,7 @@ class OSMData:
if way.user:
self.authors.add(way.user)
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)
def add_relation(self, relation: OSMRelation) -> None:

View file

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

View file

@ -9,6 +9,7 @@ from map_machine.osm_reader import (
OSMReader,
OSMRelation,
OSMWay,
parse_levels,
)
__author__ = "Sergey Vartanov"
@ -104,3 +105,19 @@ def test_relation() -> None:
assert len(relation.members) == 1
assert relation.members[0].type_ == "way"
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]