mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-22 13:36:26 +02:00
Issue #81: support lane drawing.
This commit is contained in:
parent
cc9826ae57
commit
0a7bead1ca
6 changed files with 97 additions and 15 deletions
|
@ -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
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue