Merge main.

This commit is contained in:
Sergey Vartanov 2022-09-07 02:47:46 +03:00
commit d5ef4aba4e
76 changed files with 2724 additions and 981 deletions

1
.gitignore vendored
View file

@ -16,6 +16,7 @@ cache/
# Work files # Work files
diffs
work work
precommit.py precommit.py

54
CHANGELOG.md Normal file
View file

@ -0,0 +1,54 @@
# Unreleased
- Change indoor colors, make columns visible ([#139](https://github.com/enzet/map-machine/issues/139)).
- Add 4 icons for `man_made=flagpole` + `country=*`.
# 0.1.7
_17 August 2022_
- Add icons for:
- `shop=car_parts`, `shop=variety_store` ([#48](https://github.com/enzet/map-machine/issues/48)),
- `natural=spring` ([#55](https://github.com/enzet/map-machine/issues/55)),
- `tomb=pyramid`.
- Reuse icon for `shop=department_store` ([#48](https://github.com/enzet/map-machine/issues/48)).
- Fix style for `indoor=room` ([#139](https://github.com/enzet/map-machine/issues/139)).
- Redraw diving tower and fountain icons.
- Add `--scheme` option ([#140](https://github.com/enzet/map-machine/issues/140)).
- Rename `element` command to `draw` and change format.
# 0.1.6
_4 July 2022_
- Support `attraction=water_slide` ([#137](https://github.com/enzet/map-machine/issues/137)).
- Fix diving tower priority ([#138](https://github.com/enzet/map-machine/issues/138)); test `test_icons/test_diving_tower`.
- Add icon for `amenity=dressing_room` ([#135](https://github.com/enzet/map-machine/issues/135)).
# 0.1.5
_6 June 2022_
- Support `/` as a delimiter for coordinates.
- Fix `placement` tag processing ([#128](https://github.com/enzet/map-machine/issues/128)).
- Split way priority ([#125](https://github.com/enzet/map-machine/issues/125)).
- Fix typo in `motorcar=yes` ([#133](https://github.com/enzet/map-machine/issues/133)).
# 0.1.4
- Fix vending machine priority ([#132](https://github.com/enzet/map-machine/issues/132)).
- Allow duplicate ids in OSM data (Andrew Klofas, [#131](https://github.com/enzet/map-machine/issues/131)).
# 0.1.3
_2022.4_
- Add style for
- `greenhouse_horticulture`,
- `recreation_ground`,
- `landuse=village_green` ([#129](https://github.com/enzet/map-machine/issues/129)).
- Add style for `railway=construction` ([#125](https://github.com/enzet/map-machine/issues/125)).
- Fix electricity shape.
- Support color for default icon.
- Fix waterways priority ([#126](https://github.com/enzet/map-machine/issues/126)).
- Show small dot for overlapped icons ([#121](https://github.com/enzet/map-machine/issues/121)).

View file

@ -91,6 +91,8 @@
"tags": {}, "tags": {},
"row_tags": [ "row_tags": [
{"amenity": "bench"}, {"amenity": "bench"},
{"amenity": "bench", "backrest": "yes"},
{"amenity": "bench", "backrest": "no"},
{"memorial": "bench"} {"memorial": "bench"}
] ]
}, },
@ -100,9 +102,13 @@
"id": "mast", "id": "mast",
"tags": {"man_made": "mast"}, "tags": {"man_made": "mast"},
"row_key": "tower:construction", "row_key": "tower:construction",
"row_values": ["freestanding", "lattice", "guyed_tube", "guyed_lattice"], "row_values": [
"freestanding", "lattice", "guyed_tube", "guyed_lattice"
],
"column_key": "tower:type", "column_key": "tower:type",
"column_values": ["", "communication", "lighting", "monitoring", "siren"] "column_values": [
"", "communication", "lighting", "monitoring", "siren"
]
}, },
{ {
"name": "Volcano", "name": "Volcano",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 439 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,6 +1,4 @@
""" """Map Machine: Python map renderer for OpenStreetMap with custom icon set."""
Map Machine project: simple Python map renderer for OpenStreetMap and icon set.
"""
__project__ = "Map Machine" __project__ = "Map Machine"
__description__ = ( __description__ = (
@ -11,7 +9,7 @@ __url__ = "https://github.com/enzet/map-machine"
__doc_url__ = f"{__url__}/blob/main/README.md" __doc_url__ = f"{__url__}/blob/main/README.md"
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
__version__ = "0.1.5" __version__ = "0.1.7"
REQUIREMENTS = [ REQUIREMENTS = [
"CairoSVG>=2.5.0", "CairoSVG>=2.5.0",

View file

@ -1,6 +1,4 @@
""" """Map Machine entry point."""
Map Machine entry point.
"""
from map_machine.main import main from map_machine.main import main
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"

View file

@ -1,6 +1,4 @@
""" """Color utility."""
Color utility.
"""
from typing import Any, List from typing import Any, List
from colour import Color from colour import Color

View file

@ -1,6 +1,4 @@
""" """Construct Map Machine nodes and ways."""
Construct Map Machine nodes and ways.
"""
import logging import logging
import sys import sys
from datetime import datetime from datetime import datetime
@ -57,7 +55,7 @@ TIME_COLOR_SCALE: List[Color] = [
def line_center( def line_center(
nodes: List[OSMNode], flinger: Flinger nodes: List[OSMNode], flinger: Flinger
) -> (np.ndarray, np.ndarray): ) -> Tuple[np.ndarray, np.ndarray]:
""" """
Get geometric center of nodes set. Get geometric center of nodes set.
@ -76,9 +74,7 @@ def line_center(
def get_user_color(text: str, seed: str) -> Color: def get_user_color(text: str, seed: str) -> Color:
""" """Generate random color based on text."""
Generate random color based on text.
"""
if text == "": if text == "":
return Color("black") return Color("black")
return Color("#" + sha256((seed + text).encode("utf-8")).hexdigest()[-6:]) return Color("#" + sha256((seed + text).encode("utf-8")).hexdigest()[-6:])
@ -160,13 +156,12 @@ class Constructor:
self, self,
osm_data: OSMData, osm_data: OSMData,
flinger: Flinger, flinger: Flinger,
scheme: Scheme,
extractor: ShapeExtractor, extractor: ShapeExtractor,
configuration: MapConfiguration, configuration: MapConfiguration,
) -> None: ) -> None:
self.osm_data: OSMData = osm_data self.osm_data: OSMData = osm_data
self.flinger: Flinger = flinger self.flinger: Flinger = flinger
self.scheme: Scheme = scheme self.scheme: Scheme = configuration.scheme
self.extractor: ShapeExtractor = extractor self.extractor: ShapeExtractor = extractor
self.configuration: MapConfiguration = configuration self.configuration: MapConfiguration = configuration
self.text_constructor: TextConstructor = TextConstructor(self.scheme) self.text_constructor: TextConstructor = TextConstructor(self.scheme)
@ -310,8 +305,8 @@ class Constructor:
priority: int priority: int
icon_set: IconSet icon_set: IconSet
icon_set, priority = self.scheme.get_icon( icon_set, priority = self.configuration.get_icon(
self.extractor, line.tags, processed, self.configuration self.extractor, line.tags, processed
) )
if icon_set is not None: if icon_set is not None:
labels: List[Label] = self.text_constructor.construct_text( labels: List[Label] = self.text_constructor.construct_text(
@ -331,9 +326,8 @@ class Constructor:
) )
self.points.append(point) self.points.append(point)
if line_styles: # TODO: probably we may want to skip the next part if `line_styles`
return # are not empty.
self.add_point_for_line(center_point, inners, line, outers) self.add_point_for_line(center_point, inners, line, outers)
def add_point_for_line(self, center_point, inners, line, outers) -> None: def add_point_for_line(self, center_point, inners, line, outers) -> None:
@ -352,8 +346,8 @@ class Constructor:
processed: Set[str] = set() processed: Set[str] = set()
priority: int priority: int
icon_set: IconSet icon_set: IconSet
icon_set, priority = self.scheme.get_icon( icon_set, priority = self.configuration.get_icon(
self.extractor, line.tags, processed, self.configuration self.extractor, line.tags, processed
) )
if icon_set is not None: if icon_set is not None:
labels: List[Label] = self.text_constructor.construct_text( labels: List[Label] = self.text_constructor.construct_text(
@ -418,6 +412,8 @@ class Constructor:
"""Draw nodes.""" """Draw nodes."""
logging.info("Constructing nodes...") logging.info("Constructing nodes...")
# Sort node vertically (using latitude values) to draw them from top to
# bottom.
sorted_node_ids: Iterator[int] = sorted( sorted_node_ids: Iterator[int] = sorted(
self.osm_data.nodes.keys(), self.osm_data.nodes.keys(),
key=lambda x: -self.osm_data.nodes[x].coordinates[0], key=lambda x: -self.osm_data.nodes[x].coordinates[0],
@ -428,6 +424,7 @@ class Constructor:
def construct_node(self, node: OSMNode) -> None: def construct_node(self, node: OSMNode) -> None:
"""Draw one node.""" """Draw one node."""
tags: Dict[str, str] = node.tags tags: Dict[str, str] = node.tags
if not tags: if not tags:
return return
if not self.check_level(tags): if not self.check_level(tags):
@ -477,8 +474,8 @@ class Constructor:
color = Color("#CCCCCC") color = Color("#CCCCCC")
if self.configuration.drawing_mode == DrawingMode.BLACK: if self.configuration.drawing_mode == DrawingMode.BLACK:
color = Color("#444444") color = Color("#444444")
icon_set, priority = self.scheme.get_icon( icon_set, priority = self.configuration.get_icon(
self.extractor, tags, processed, self.configuration self.extractor, tags, processed
) )
icon_set.main_icon.recolor(color) icon_set.main_icon.recolor(color)
point: Point = Point( point: Point = Point(
@ -492,8 +489,8 @@ class Constructor:
self.points.append(point) self.points.append(point)
return return
icon_set, priority = self.scheme.get_icon( icon_set, priority = self.configuration.get_icon(
self.extractor, tags, processed, self.configuration self.extractor, tags, processed
) )
if icon_set is None: if icon_set is None:
return return
@ -529,7 +526,7 @@ class Constructor:
def get_sorted_figures(self) -> List[StyledFigure]: def get_sorted_figures(self) -> List[StyledFigure]:
"""Get all figures sorted by priority.""" """Get all figures sorted by priority."""
return sorted(self.figures, key=lambda x: x.line_style.priority) return sorted(self.figures)
def check_level_number(tags: Tags, level: float) -> bool: def check_level_number(tags: Tags, level: float) -> bool:

View file

@ -1,3 +1 @@
""" """Documentation utilities."""
Documentation utilities.
"""

View file

@ -1,6 +1,4 @@
""" """Special icon collections for documentation."""
Special icon collections for documentation.
"""
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
@ -24,28 +22,40 @@ SCHEME: Scheme = Scheme.from_file(WORKSPACE.DEFAULT_SCHEME_PATH)
EXTRACTOR: ShapeExtractor = ShapeExtractor( EXTRACTOR: ShapeExtractor = ShapeExtractor(
WORKSPACE.ICONS_PATH, WORKSPACE.ICONS_CONFIG_PATH WORKSPACE.ICONS_PATH, WORKSPACE.ICONS_CONFIG_PATH
) )
MONOSPACE_FONTS: list[str] = [
"JetBrains Mono",
"Fira Code",
"Fira Mono",
"ui-monospace",
"SFMono-regular",
"SF Mono",
"Menlo",
"Consolas",
"Liberation Mono",
"monospace",
]
@dataclass @dataclass
class Collection: class Collection:
"""Icon collection.""" """Icon collection."""
# Core tags # Core tags.
tags: Tags tags: Tags
# Tag key to be used in rows # Tag key to be used in rows.
row_key: Optional[str] = None row_key: Optional[str] = None
# List of tag values to be used in rows # List of tag values to be used in rows.
row_values: List[str] = field(default_factory=list) row_values: List[str] = field(default_factory=list)
# Tag key to be used in columns # Tag key to be used in columns.
column_key: Optional[str] = None column_key: Optional[str] = None
# List of tag values to be used in columns # List of tag values to be used in columns.
column_values: List[str] = field(default_factory=list) column_values: List[str] = field(default_factory=list)
# List of tags to be used in rows # List of tags to be used in rows.
row_tags: List[Tags] = field(default_factory=list) row_tags: List[Tags] = field(default_factory=list)
@classmethod @classmethod
@ -91,20 +101,7 @@ class SVGTable:
self.half_step: np.ndarray = np.array( self.half_step: np.ndarray = np.array(
(self.step / 2.0, self.step / 2.0) (self.step / 2.0, self.step / 2.0)
) )
self.font: str = ",".join(MONOSPACE_FONTS)
fonts: List[str] = [
"JetBrains Mono",
"Fira Code",
"Fira Mono",
"ui-monospace",
"SFMono-regular",
"SF Mono",
"Menlo",
"Consolas",
"Liberation Mono",
"monospace",
]
self.font: str = ",".join(fonts)
self.font_width: float = self.font_size * 0.7 self.font_width: float = self.font_size * 0.7
self.size: List[float] = [ self.size: List[float] = [
@ -145,8 +142,8 @@ class SVGTable:
if column_value: if column_value:
current_tags |= {self.collection.column_key: column_value} current_tags |= {self.collection.column_key: column_value}
processed: Set[str] = set() processed: Set[str] = set()
icon, _ = SCHEME.get_icon( icon, _ = MapConfiguration(SCHEME).get_icon(
EXTRACTOR, current_tags, processed, MapConfiguration() EXTRACTOR, current_tags, processed
) )
processed = icon.processed processed = icon.processed
if not icon: if not icon:
@ -171,6 +168,9 @@ class SVGTable:
self.draw_cross(np.array((j, i))) self.draw_cross(np.array((j, i)))
width, height = self.get_size() width, height = self.get_size()
self.svg.elements.insert(
0, self.svg.rect((0, 0), (width, height), fill="white")
)
self.svg.update({"width": width, "height": height}) self.svg.update({"width": width, "height": height})
def draw_rows(self) -> None: def draw_rows(self) -> None:
@ -308,7 +308,7 @@ class SVGTable:
def draw_svg_tables(output_path: Path, html_file_path: Path) -> None: def draw_svg_tables(output_path: Path, html_file_path: Path) -> None:
"""Draw SVG tables of icon collections.""" """Draw SVG tables of icon collections."""
with Path("data/collections.json").open() as input_file: with (Path("data") / "collections.json").open() as input_file:
collections: List[Dict[str, Any]] = json.load(input_file) collections: List[Dict[str, Any]] = json.load(input_file)
with html_file_path.open("w+") as html_file: with html_file_path.open("w+") as html_file:
@ -332,4 +332,4 @@ def draw_svg_tables(output_path: Path, html_file_path: Path) -> None:
if __name__ == "__main__": if __name__ == "__main__":
draw_svg_tables(Path("doc"), Path("result.html")) draw_svg_tables(Path("doc"), Path("out") / "collections.html")

View file

@ -1,6 +1,4 @@
""" """Icon grids for documentation."""
Icon grids for documentation.
"""
from pathlib import Path from pathlib import Path
from typing import Iterable from typing import Iterable

View file

@ -1,6 +1,4 @@
""" """Moire markup extension for Map Machine."""
Moire markup extension for Map Machine.
"""
import argparse import argparse
from abc import ABC from abc import ABC
from pathlib import Path from pathlib import Path
@ -45,9 +43,7 @@ def parse_text(text: str, margins: str, tag_id: str) -> Code:
class ArgumentParser(argparse.ArgumentParser): class ArgumentParser(argparse.ArgumentParser):
""" """Parser that stores arguments and creates help in Moire markup."""
Argument parser that stores arguments and creates help in Moire markup.
"""
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
self.arguments: List[Dict[str, Any]] = [] self.arguments: List[Dict[str, Any]] = []
@ -160,7 +156,7 @@ class MapMachineMoire(Default, ABC):
elif command == "map": elif command == "map":
cli.add_map_arguments(parser) cli.add_map_arguments(parser)
elif command == "element": elif command == "element":
cli.add_element_arguments(parser) cli.add_draw_arguments(parser)
elif command == "mapcss": elif command == "mapcss":
cli.add_mapcss_arguments(parser) cli.add_mapcss_arguments(parser)
else: else:
@ -236,7 +232,7 @@ class MapMachineHTML(MapMachineMoire, DefaultHTML):
class MapMachineOSMWiki(MapMachineMoire, DefaultWiki): class MapMachineOSMWiki(MapMachineMoire, DefaultWiki):
""" """
OpenStreetMap wiki. Moire convertor to OpenStreetMap wiki markup.
See https://wiki.openstreetmap.org/wiki/Main_Page See https://wiki.openstreetmap.org/wiki/Main_Page
""" """
@ -249,7 +245,7 @@ class MapMachineOSMWiki(MapMachineMoire, DefaultWiki):
) )
def osm(self, arg: Arguments) -> str: def osm(self, arg: Arguments) -> str:
"""OSM tag key or keyvalue pair of tag.""" """Add special OSM tag key or keyvalue pair of tag."""
spec: str = self.clear(arg[0]) spec: str = self.clear(arg[0])
if "=" in spec: if "=" in spec:
key, tag = spec.split("=") key, tag = spec.split("=")
@ -258,11 +254,11 @@ class MapMachineOSMWiki(MapMachineMoire, DefaultWiki):
return f"{{{{Tag|{spec}}}}}" return f"{{{{Tag|{spec}}}}}"
def color(self, arg: Arguments) -> str: def color(self, arg: Arguments) -> str:
"""Simple color sample.""" """Add color box on the wiki page with specified color."""
return f"{{{{Color box|{self.clear(arg[0])}}}}}" return f"{{{{Color box|{self.clear(arg[0])}}}}}"
def icon(self, arg: Arguments) -> str: def icon(self, arg: Arguments) -> str:
"""Image with Röntgen icon.""" """Process image with Röntgen icon."""
size: str = self.clear(arg[1]) if len(arg) > 1 else "16" size: str = self.clear(arg[1]) if len(arg) > 1 else "16"
shape_id: str = self.clear(arg[0]) shape_id: str = self.clear(arg[0])
name: str = self.extractor.get_shape(shape_id).name name: str = self.extractor.get_shape(shape_id).name
@ -275,15 +271,15 @@ class MapMachineMarkdown(MapMachineMoire, DefaultMarkdown):
images = {} images = {}
def color(self, arg: Arguments) -> str: def color(self, arg: Arguments) -> str:
"""Simple color sample.""" """Ignore colors in Markdown."""
return self.clear(arg[0]) return self.clear(arg[0])
def icon(self, arg: Arguments) -> str: def icon(self, arg: Arguments) -> str:
"""Image with Röntgen icon.""" """Process image with Röntgen icon."""
return f"[{self.clear(arg[0])}]" return f"[{self.clear(arg[0])}]"
def kbd(self, arg: Arguments) -> str: def kbd(self, arg: Arguments) -> str:
"""Keyboard key.""" """Process keyboard key."""
return f"<kbd>{self.clear(arg[0])}</kbd>" return f"<kbd>{self.clear(arg[0])}</kbd>"
def no_wrap(self, arg: Arguments) -> str: def no_wrap(self, arg: Arguments) -> str:
@ -291,7 +287,7 @@ class MapMachineMarkdown(MapMachineMoire, DefaultMarkdown):
return f'<span style="white-space: nowrap;">{self.parse(arg[0])}</span>' return f'<span style="white-space: nowrap;">{self.parse(arg[0])}</span>'
def formal(self, arg: Arguments) -> str: def formal(self, arg: Arguments) -> str:
"""Formal variable.""" """Process formal variable."""
return f"<{self.parse(arg[0])}>" return f"<{self.parse(arg[0])}>"

View file

@ -1,6 +1,4 @@
""" """Actions to perform before commit: generate PNG images for documentation."""
Actions to perform before commit: generate PNG images for documentation.
"""
import logging import logging
import sys import sys
from pathlib import Path from pathlib import Path
@ -11,7 +9,7 @@ import svgwrite
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger from map_machine.geometry.flinger import MercatorFlinger
from map_machine.map_configuration import ( from map_machine.map_configuration import (
BuildingMode, BuildingMode,
DrawingMode, DrawingMode,
@ -23,16 +21,16 @@ from map_machine.osm.osm_getter import get_osm
from map_machine.osm.osm_reader import OSMData from map_machine.osm.osm_reader import OSMData
from map_machine.pictogram.icon import ShapeExtractor from map_machine.pictogram.icon import ShapeExtractor
from map_machine.scheme import Scheme from map_machine.scheme import Scheme
from map_machine.workspace import workspace
doc_path: Path = Path("doc") doc_path: Path = Path("doc")
cache: Path = Path("cache") cache: Path = Path("cache")
cache.mkdir(exist_ok=True) cache.mkdir(exist_ok=True)
SCHEME: Scheme = Scheme.from_file(Path("map_machine/scheme/default.yml")) SCHEME: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
EXTRACTOR: ShapeExtractor = ShapeExtractor( EXTRACTOR: ShapeExtractor = ShapeExtractor(
Path("map_machine/icons/icons.svg"), workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
Path("map_machine/icons/config.json"),
) )
@ -40,23 +38,26 @@ def draw(
input_file_name: Path, input_file_name: Path,
output_file_name: Path, output_file_name: Path,
boundary_box: BoundaryBox, boundary_box: BoundaryBox,
configuration: MapConfiguration = MapConfiguration(), configuration: Optional[MapConfiguration] = None,
) -> None: ) -> None:
"""Draw file.""" """Draw file."""
if configuration is None:
configuration = MapConfiguration(SCHEME)
osm_data: OSMData = OSMData() osm_data: OSMData = OSMData()
osm_data.parse_osm_file(input_file_name) osm_data.parse_osm_file(input_file_name)
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
boundary_box, configuration.zoom_level, osm_data.equator_length boundary_box, configuration.zoom_level, osm_data.equator_length
) )
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data, flinger, SCHEME, EXTRACTOR, configuration osm_data, flinger, EXTRACTOR, configuration
) )
constructor.construct() constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing( svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_file_name), size=flinger.size str(output_file_name), size=flinger.size
) )
map_: Map = Map(flinger, svg, SCHEME, configuration) map_: Map = Map(flinger, svg, configuration)
map_.draw(constructor) map_.draw(constructor)
svg.write(output_file_name.open("w")) svg.write(output_file_name.open("w"))
@ -65,11 +66,14 @@ def draw(
def draw_around_point( def draw_around_point(
point: np.ndarray, point: np.ndarray,
name: str, name: str,
configuration: MapConfiguration = MapConfiguration(), configuration: Optional[MapConfiguration] = None,
size: np.ndarray = np.array((600, 400)), size: np.ndarray = np.array((600, 400)),
get: Optional[BoundaryBox] = None, get: Optional[BoundaryBox] = None,
) -> None: ) -> None:
"""Draw around point.""" """Draw around point."""
if configuration is None:
configuration = MapConfiguration(SCHEME)
input_path: Path = doc_path / f"{name}.svg" input_path: Path = doc_path / f"{name}.svg"
boundary_box: BoundaryBox = BoundaryBox.from_coordinates( boundary_box: BoundaryBox = BoundaryBox.from_coordinates(
@ -92,7 +96,7 @@ def main(id_: str) -> None:
draw_around_point( draw_around_point(
np.array((55.75277, 37.40856)), np.array((55.75277, 37.40856)),
"fitness", "fitness",
MapConfiguration(zoom_level=20.2), MapConfiguration(SCHEME, zoom_level=20.2),
np.array((300, 200)), np.array((300, 200)),
) )
@ -100,14 +104,14 @@ def main(id_: str) -> None:
draw_around_point( draw_around_point(
np.array((52.5622, 12.94)), np.array((52.5622, 12.94)),
"power", "power",
configuration=MapConfiguration(zoom_level=15), configuration=MapConfiguration(SCHEME, zoom_level=15),
) )
if id_ is None or id_ == "playground": if id_ is None or id_ == "playground":
draw_around_point( draw_around_point(
np.array((52.47388, 13.43826)), np.array((52.47388, 13.43826)),
"playground", "playground",
configuration=MapConfiguration(zoom_level=19), configuration=MapConfiguration(SCHEME, zoom_level=19),
) )
# Playground: (59.91991/10.85535), (59.83627/10.83017), Oslo # Playground: (59.91991/10.85535), (59.83627/10.83017), Oslo
@ -118,6 +122,7 @@ def main(id_: str) -> None:
np.array((52.50892, 13.3244)), np.array((52.50892, 13.3244)),
"surveillance", "surveillance",
MapConfiguration( MapConfiguration(
SCHEME,
zoom_level=18.5, zoom_level=18.5,
ignore_level_matching=True, ignore_level_matching=True,
), ),
@ -128,6 +133,7 @@ def main(id_: str) -> None:
np.array((52.421, 13.101)), np.array((52.421, 13.101)),
"viewpoints", "viewpoints",
MapConfiguration( MapConfiguration(
SCHEME,
label_mode=LabelMode.NO, label_mode=LabelMode.NO,
zoom_level=15.7, zoom_level=15.7,
ignore_level_matching=True, ignore_level_matching=True,
@ -138,7 +144,7 @@ def main(id_: str) -> None:
draw_around_point( draw_around_point(
np.array((-26.19049, 28.05605)), np.array((-26.19049, 28.05605)),
"buildings", "buildings",
MapConfiguration(building_mode=BuildingMode.ISOMETRIC), MapConfiguration(SCHEME, building_mode=BuildingMode.ISOMETRIC),
) )
if id_ is None or id_ == "trees": if id_ is None or id_ == "trees":
@ -146,7 +152,7 @@ def main(id_: str) -> None:
np.array((55.751, 37.628)), np.array((55.751, 37.628)),
"trees", "trees",
MapConfiguration( MapConfiguration(
label_mode=LabelMode(LabelMode.ALL), zoom_level=18.1 SCHEME, label_mode=LabelMode(LabelMode.ALL), zoom_level=18.1
), ),
get=BoundaryBox(37.624, 55.749, 37.633, 55.753), get=BoundaryBox(37.624, 55.749, 37.633, 55.753),
) )
@ -160,6 +166,7 @@ def main(id_: str) -> None:
np.array((55.7655, 37.6055)), np.array((55.7655, 37.6055)),
"time", "time",
MapConfiguration( MapConfiguration(
SCHEME,
DrawingMode.TIME, DrawingMode.TIME,
zoom_level=16.5, zoom_level=16.5,
ignore_level_matching=True, ignore_level_matching=True,
@ -171,6 +178,7 @@ def main(id_: str) -> None:
np.array((55.7655, 37.6055)), np.array((55.7655, 37.6055)),
"author", "author",
MapConfiguration( MapConfiguration(
SCHEME,
DrawingMode.AUTHOR, DrawingMode.AUTHOR,
seed="a", seed="a",
zoom_level=16.5, zoom_level=16.5,
@ -183,6 +191,7 @@ def main(id_: str) -> None:
np.array((48.87422, 2.377)), np.array((48.87422, 2.377)),
"colors", "colors",
configuration=MapConfiguration( configuration=MapConfiguration(
SCHEME,
zoom_level=17.6, zoom_level=17.6,
building_mode=BuildingMode.ISOMETRIC, building_mode=BuildingMode.ISOMETRIC,
ignore_level_matching=True, ignore_level_matching=True,

View file

@ -1,11 +1,9 @@
""" """Automate OpenStreetMap wiki editing."""
Automate OpenStreetMap wiki editing.
"""
import re import re
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from map_machine.doc.collections import Collection from map_machine.doc.doc_collections import Collection
from map_machine.map_configuration import MapConfiguration from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import Tags from map_machine.osm.osm_reader import Tags
@ -38,9 +36,7 @@ class WikiTable:
self.page_name: str = page_name self.page_name: str = page_name
def generate_wiki_table(self) -> tuple[str, list[Icon]]: def generate_wiki_table(self) -> tuple[str, list[Icon]]:
""" """Generate Röntgen icon table for the OpenStreetMap wiki page."""
Generate Röntgen icon table for the OpenStreetMap wiki page.
"""
icons: list[Icon] = [] icons: list[Icon] = []
text: str = '{| class="wikitable"\n' text: str = '{| class="wikitable"\n'
@ -62,11 +58,10 @@ class WikiTable:
text += f"{{{{Tag|{key}|{value}}}}}<br />" text += f"{{{{Tag|{key}|{value}}}}}<br />"
text = text[:-6] text = text[:-6]
text += "\n" text += "\n"
icon, _ = SCHEME.get_icon( icon, _ = MapConfiguration(
EXTRACTOR, SCHEME, ignore_level_matching=True
current_tags | self.collection.tags, ).get_icon(
set(), EXTRACTOR, current_tags | self.collection.tags, set()
MapConfiguration(ignore_level_matching=True),
) )
icons.append(icon.main_icon) icons.append(icon.main_icon)
text += ( text += (
@ -107,7 +102,9 @@ class WikiTable:
} }
if column_value: if column_value:
current_tags |= {self.collection.column_key: column_value} current_tags |= {self.collection.column_key: column_value}
icon, _ = SCHEME.get_icon(EXTRACTOR, current_tags, set()) icon, _ = MapConfiguration(SCHEME).get_icon(
EXTRACTOR, current_tags, set()
)
if not icon: if not icon:
print("Icon was not constructed.") print("Icon was not constructed.")
text += ( text += (
@ -139,8 +136,8 @@ def generate_new_text(
wiki_text, icons = table.generate_wiki_table() wiki_text, icons = table.generate_wiki_table()
else: else:
processed = set() processed = set()
icon, _ = SCHEME.get_icon( icon, _ = MapConfiguration(SCHEME).get_icon(
EXTRACTOR, table.collection.tags, processed, MapConfiguration() EXTRACTOR, table.collection.tags, processed
) )
if not icon.main_icon.is_default(): if not icon.main_icon.is_default():
wiki_text = ( wiki_text = (

View file

@ -1,6 +1,4 @@
""" """Drawing utility."""
Drawing utility.
"""
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import List, Optional, Union from typing import List, Optional, Union

View file

@ -0,0 +1 @@
"""Drawing of separate map elements."""

View file

@ -0,0 +1,44 @@
"""Entry point for element drawing: nodes, ways, and relations."""
import argparse
from pathlib import Path
from map_machine.element.grid import Grid
from map_machine.osm.osm_reader import Tags, OSMNode
def draw_node(tags: Tags, path: Path):
"""Draw separate node."""
grid: Grid = Grid(show_credit=False, margin=3.5)
grid.add_node(tags, 0, 0)
grid.draw(path)
def draw_way():
"""Draw way."""
pass
def draw_area(tags: Tags, path: Path):
"""Draw closed way that should be interpreted as an area."""
grid: Grid = Grid(show_credit=False, margin=0.5)
node: OSMNode = grid.add_node({}, 0, 0)
nodes: list[OSMNode] = [
node,
grid.add_node({}, 0, 1),
grid.add_node({}, 1, 1),
grid.add_node({}, 1, 0),
node,
]
grid.add_way(tags, nodes)
grid.draw(path)
def draw_element(options: argparse.Namespace):
"""Entry point for element drawing."""
tags_description: Tags = {
x.split("=")[0]: x.split("=")[1] for x in options.tags.split(",")
}
if options.type == "area":
draw_area(tags_description, Path(options.output_file))
elif options.type == "node":
draw_node(tags_description, Path(options.output_file))

124
map_machine/element/grid.py Normal file
View file

@ -0,0 +1,124 @@
import logging
from pathlib import Path
from typing import List
import numpy as np
from svgwrite import Drawing
from svgwrite.text import Text
from map_machine.constructor import Constructor
from map_machine.geometry.flinger import Flinger, TranslateFlinger
from map_machine.map_configuration import MapConfiguration
from map_machine.mapper import Map
from map_machine.osm.osm_reader import (
OSMNode,
OSMData,
Tags,
OSMWay,
OSMMember,
OSMRelation,
)
from map_machine.pictogram.icon import ShapeExtractor
from map_machine.scheme import Scheme
from map_machine.workspace import Workspace
workspace: Workspace = Workspace(Path("temp"))
SCHEME: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
SHAPE_EXTRACTOR: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
)
DEFAULT_ZOOM: float = 18.0
class Grid:
"""Creating map with elements ordered in grid."""
def __init__(
self,
x_step: float = 20.0,
y_step: float = 20.0,
show_credit: bool = True,
margin: float = 1.5,
) -> None:
self.x_step: float = x_step
self.y_step: float = y_step
self.show_credit: bool = show_credit
self.margin: float = margin
self.index: int = 0
self.nodes: dict[OSMNode, tuple[int, int]] = {}
self.max_j: float = 0.0
self.max_i: float = 0.0
self.way_id: int = 0
self.relation_id: int = 0
self.osm_data: OSMData = OSMData()
self.texts: list[tuple[str, int, int]] = []
def add_node(self, tags: Tags, i: int, j: int) -> OSMNode:
"""Add OSM node to the grid."""
self.index += 1
node: OSMNode = OSMNode(tags, self.index, np.array((i, j)))
self.nodes[node] = (j, i)
self.osm_data.add_node(node)
self.max_j = max(self.max_j, j)
self.max_i = max(self.max_i, i)
return node
def add_way(self, tags: Tags, nodes: List[OSMNode]) -> OSMWay:
"""Add OSM way to the grid."""
osm_way: OSMWay = OSMWay(tags, self.way_id, nodes)
self.osm_data.add_way(osm_way)
self.way_id += 1
return osm_way
def add_relation(self, tags: Tags, members: List[OSMMember]) -> OSMRelation:
"""Connect objects on the gird with relations."""
osm_relation: OSMRelation = OSMRelation(tags, self.relation_id, members)
self.osm_data.add_relation(osm_relation)
self.relation_id += 1
return osm_relation
def add_text(self, text: str, i: int, j: int) -> None:
"""Add simple text label to the grid."""
self.texts.append((text, i, j))
def draw(self, output_path: Path) -> None:
"""Draw grid."""
configuration: MapConfiguration = MapConfiguration(
SCHEME, level="all", credit=None, show_credit=self.show_credit
)
size = (
(self.max_i + self.margin * 2.0) * self.x_step,
(self.max_j + self.margin * 2.0) * self.y_step,
)
flinger: Flinger = TranslateFlinger(
size,
np.array((self.x_step, self.y_step)),
np.array((self.margin, self.margin)),
)
svg: Drawing = Drawing(output_path.name, size)
constructor: Constructor = Constructor(
self.osm_data, flinger, SHAPE_EXTRACTOR, configuration
)
constructor.construct()
map_: Map = Map(flinger, svg, configuration)
map_.draw(constructor)
for text, i, j in self.texts:
svg.add(
Text(
text,
flinger.fling((i, j)) + (0, 3),
font_family="JetBrains Mono",
font_size=12,
)
)
with output_path.open("w") as output_file:
svg.write(output_file)
logging.info(f"Map is drawn to {output_path}.")

View file

@ -1,6 +1,4 @@
""" """Drawing separate map elements."""
Drawing separate map elements.
"""
import argparse import argparse
import logging import logging
from pathlib import Path from pathlib import Path
@ -10,7 +8,9 @@ import numpy as np
import svgwrite import svgwrite
from svgwrite.path import Path as SVGPath from svgwrite.path import Path as SVGPath
from map_machine.map_configuration import LabelMode from map_machine.element.grid import Grid
from map_machine.map_configuration import LabelMode, MapConfiguration
from map_machine.osm.osm_reader import Tags
from map_machine.pictogram.icon import ShapeExtractor from map_machine.pictogram.icon import ShapeExtractor
from map_machine.pictogram.point import Point from map_machine.pictogram.point import Point
from map_machine.scheme import LineStyle, Scheme from map_machine.scheme import LineStyle, Scheme
@ -21,6 +21,12 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
def draw_node(tags: Tags) -> None:
grid: Grid = Grid()
grid.add_node(tags, 0, 0)
grid.draw(Path("out.svg"))
def draw_element(options: argparse.Namespace) -> None: def draw_element(options: argparse.Namespace) -> None:
"""Draw single node, line, or area.""" """Draw single node, line, or area."""
target: str target: str
@ -44,7 +50,7 @@ def draw_element(options: argparse.Namespace) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
) )
processed: Set[str] = set() processed: Set[str] = set()
icon, _ = scheme.get_icon(extractor, tags, processed) icon, _ = MapConfiguration(scheme).get_icon(extractor, tags, processed)
is_for_node: bool = target == "node" is_for_node: bool = target == "node"
text_constructor: TextConstructor = TextConstructor(scheme) text_constructor: TextConstructor = TextConstructor(scheme)
labels: List[Label] = text_constructor.construct_text( labels: List[Label] = text_constructor.construct_text(

171
map_machine/element/way.py Normal file
View file

@ -0,0 +1,171 @@
"""Draw test nodes, ways, and relations."""
import logging
from pathlib import Path
from typing import Optional
from map_machine.element.grid import Grid
from map_machine.osm.osm_reader import OSMNode, OSMMember
from map_machine.osm.tags import (
HIGHWAY_VALUES,
AEROWAY_VALUES,
RAILWAY_VALUES,
ROAD_VALUES,
)
ROAD_WIDTHS_AND_FEATURES: list[dict[str, str]] = [
{"width": "4"},
{"width": "8"},
{"width": "12"},
{"width": "16"},
{"bridge": "yes", "width": "4"},
{"bridge": "yes", "width": "8"},
{"tunnel": "yes", "width": "4"},
{"tunnel": "yes", "width": "8"},
{"ford": "yes", "width": "4"},
{"ford": "yes", "width": "8"},
{"embankment": "yes", "width": "4"},
{"embankment": "yes", "width": "8"},
]
ROAD_LANES_AND_FEATURES: list[dict[str, str]] = [
{"lanes": "1"},
{"lanes": "2"},
{"lanes": "3"},
{"lanes": "4"},
{"bridge": "yes", "lanes": "1"},
{"bridge": "yes", "lanes": "2"},
{"tunnel": "yes", "lanes": "1"},
{"tunnel": "yes", "lanes": "2"},
{"ford": "yes", "lanes": "1"},
{"ford": "yes", "lanes": "2"},
{"embankment": "yes", "lanes": "1"},
{"embankment": "yes", "lanes": "2"},
]
# See https://wiki.openstreetmap.org/wiki/Proposed_features/placement
PLACEMENT_FEATURES_1: list[dict[str, str]] = [
{"lanes": "1"},
{"lanes": "2", "placement": "middle_of:1"},
{"lanes": "4", "placement": "middle_of:2"},
{"placement": "transition"},
{"lanes": "3", "placement": "right_of:1"}, # or placement=left_of:2
]
PLACEMENT_FEATURES_2: list[dict[str, str]] = [
{"lanes": "2"},
# or placement:backward=left_of:1
{"lanes": "3", "placement:forward": "left_of:1"},
{"lanes": "3", "placement": "transition"},
{"lanes": "4", "placement:backward": "middle_of:1"},
{"lanes": "3"},
]
def draw_overlapped_ways(types: list[dict[str, str]], path: Path) -> None:
"""
Draw two sets of ways intersecting each other.
The goal is to show check priority.
"""
grid: Grid = Grid()
for index, tags in enumerate(types):
node_1: OSMNode = grid.add_node({}, 8, index + 1)
node_2: OSMNode = grid.add_node({}, len(types) + 9, index + 1)
grid.add_way(tags, [node_1, node_2])
grid.add_text(", ".join(f"{k}={tags[k]}" for k in tags), 0, index + 1)
for index, tags in enumerate(types):
node_1: OSMNode = grid.add_node({}, index + 9, 0)
node_2: OSMNode = grid.add_node({}, index + 9, len(types) + 1)
grid.add_way(tags, [node_1, node_2])
grid.draw(path)
def draw_road_features(
types: list[dict[str, str]], features: list[dict[str, str]], path: Path
) -> None:
"""Draw test image with different road features."""
grid: Grid = Grid()
for i, type_ in enumerate(types):
previous: Optional[OSMNode] = None
for j in range(len(features) + 1):
node: OSMNode = grid.add_node({}, i, j)
if previous:
tags: dict[str, str] = dict(type_)
tags |= dict(features[j - 1])
grid.add_way(tags, [previous, node])
previous = node
grid.draw(path)
def draw_multipolygon(path: Path) -> None:
"""Draw simple multipolygon with one outer and one inner way."""
grid: Grid = Grid()
outer_node: OSMNode = grid.add_node({}, 0, 0)
outer_nodes: list[OSMNode] = [
outer_node,
grid.add_node({}, 0, 3),
grid.add_node({}, 3, 3),
grid.add_node({}, 3, 0),
outer_node,
]
inner_node: OSMNode = grid.add_node({}, 1, 1)
inner_nodes: list[OSMNode] = [
inner_node,
grid.add_node({}, 1, 2),
grid.add_node({}, 2, 2),
grid.add_node({}, 2, 1),
inner_node,
]
outer = grid.add_way({}, outer_nodes)
inner = grid.add_way({}, inner_nodes)
members: list[OSMMember] = [
OSMMember("way", outer.id_, "outer"),
OSMMember("way", inner.id_, "inner"),
]
grid.add_relation({"natural": "water", "type": "multipolygon"}, members)
grid.draw(path)
if __name__ == "__main__":
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO)
out_path: Path = Path("out")
road_tags: list[dict[str, str]] = [
{"highway": value} for value in ROAD_VALUES
]
highway_tags: list[dict[str, str]] = [
{"highway": value} for value in HIGHWAY_VALUES
]
aeroway_tags: list[dict[str, str]] = [
{"aeroway": value} for value in AEROWAY_VALUES
]
railway_tags: list[dict[str, str]] = [
{"railway": value} for value in RAILWAY_VALUES
]
draw_road_features(
highway_tags, ROAD_LANES_AND_FEATURES, out_path / "lanes.svg"
)
draw_road_features(
highway_tags + railway_tags + aeroway_tags,
ROAD_WIDTHS_AND_FEATURES,
out_path / "width.svg",
)
draw_road_features(
highway_tags,
PLACEMENT_FEATURES_1 + [{"highway": "none"}] + PLACEMENT_FEATURES_2,
out_path / "placement.svg",
)
draw_overlapped_ways(road_tags + railway_tags, out_path / "overlap.svg")
draw_multipolygon(out_path / "multipolygon.svg")

View file

@ -1,3 +1 @@
""" """Specific map features: roads, directions, etc."""
Specific map features: roads, directions, etc.
"""

View file

@ -1,7 +1,6 @@
""" """Buildings on the map."""
Buildings on the map.
"""
import numpy as np import numpy as np
import svgwrite
from colour import Color from colour import Color
from svgwrite import Drawing from svgwrite import Drawing
from svgwrite.container import Group from svgwrite.container import Group
@ -170,32 +169,44 @@ class Building(Figure):
svg.add(path) svg.add(path)
def draw_walls(svg, building: Building, segment, height, shift_1, shift_2): def draw_walls(
fill: str svg: svgwrite.Drawing,
building: Building,
segment: Segment,
height: float,
shift_1: np.ndarray,
shift_2: np.ndarray,
):
"""
Draw walls for buildings as a quadrangle.
Color of the wall is based on illumination.
"""
color: Color
if building.is_construction: if building.is_construction:
color_part: float = segment.angle * 0.2 color_part: float = segment.angle * 0.2
fill = Color( color = Color(
rgb=( rgb=(
building.wall_color.get_red() + color_part, building.wall_color.get_red() + color_part,
building.wall_color.get_green() + color_part, building.wall_color.get_green() + color_part,
building.wall_color.get_blue() + color_part, building.wall_color.get_blue() + color_part,
) )
).hex )
elif height <= 0.25 / BUILDING_SCALE: elif height <= 0.25 / BUILDING_SCALE:
fill = building.wall_bottom_color_1.hex color = building.wall_bottom_color_1
elif height <= 0.5 / BUILDING_SCALE: elif height <= 0.5 / BUILDING_SCALE:
fill = building.wall_bottom_color_2.hex color = building.wall_bottom_color_2
else: else:
color_part: float = segment.angle * 0.2 - 0.1 color_part: float = segment.angle * 0.2 - 0.1
fill = Color( color = Color(
rgb=( rgb=(
max(min(building.wall_color.get_red() + color_part, 1), 0), max(min(building.wall_color.get_red() + color_part, 1), 0),
max(min(building.wall_color.get_green() + color_part, 1), 0), max(min(building.wall_color.get_green() + color_part, 1), 0),
max(min(building.wall_color.get_blue() + color_part, 1), 0), max(min(building.wall_color.get_blue() + color_part, 1), 0),
) )
).hex )
command = ( command: PathCommands = [
"M", "M",
segment.point_1 + shift_1, segment.point_1 + shift_1,
"L", "L",
@ -204,11 +215,11 @@ def draw_walls(svg, building: Building, segment, height, shift_1, shift_2):
segment.point_1 + shift_2, segment.point_1 + shift_2,
segment.point_1 + shift_1, segment.point_1 + shift_1,
"Z", "Z",
) ]
path: Path = Path( path: Path = Path(
d=command, d=command,
fill=fill, fill=color.hex,
stroke=fill, stroke=color.hex,
stroke_width=1, stroke_width=1,
stroke_linejoin="round", stroke_linejoin="round",
) )

View file

@ -1,6 +1,4 @@
""" """Crater on the map."""
Crater on the map.
"""
import numpy as np import numpy as np
from colour import Color from colour import Color
from svgwrite import Drawing from svgwrite import Drawing

View file

@ -1,7 +1,5 @@
""" """Direction tag support."""
Direction tag support. from typing import Iterator, Optional, List, Dict
"""
from typing import Iterator, List, Optional, Dict
import numpy as np import numpy as np
from colour import Color from colour import Color
@ -23,8 +21,9 @@ DEFAULT_ANGLE: float = np.pi / 30.0
def parse_vector(text: str) -> Optional[np.ndarray]: def parse_vector(text: str) -> Optional[np.ndarray]:
""" """
Parse vector from text representation: compass points or 360-degree Parse vector from text representation.
notation. E.g. "NW", "270".
Compass points or 360-degree notation. E.g. "NW", "270".
:param text: vector text representation :param text: vector text representation
:return: parsed normalized vector :return: parsed normalized vector
@ -60,6 +59,8 @@ class Sector:
def __init__(self, text: str, angle: Optional[float] = None) -> None: def __init__(self, text: str, angle: Optional[float] = None) -> None:
""" """
Construct sector from text representation.
:param text: sector text representation (e.g. "70-210", "N-NW") :param text: sector text representation (e.g. "70-210", "N-NW")
:param angle: angle in degrees :param angle: angle in degrees
""" """
@ -127,6 +128,8 @@ class DirectionSet:
def __init__(self, text: str) -> None: def __init__(self, text: str) -> None:
""" """
Construct direction set from text representation.
:param text: direction tag value :param text: direction tag value
""" """
self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";")) self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";"))

View file

@ -1,6 +1,4 @@
""" """WIP: road shape drawing."""
WIP: road shape drawing.
"""
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
@ -67,6 +65,8 @@ class RoadPart:
scale: float, scale: float,
) -> None: ) -> None:
""" """
Initialize road part with two end points.
:param point_1: start point of the road part :param point_1: start point of the road part
:param point_2: end point of the road part :param point_2: end point of the road part
:param lanes: lane specification :param lanes: lane specification
@ -284,8 +284,9 @@ class RoadPart:
class Intersection: class Intersection:
""" """
An intersection of the roads, that is described by its parts. All first An intersection of the roads, that is described by its parts.
points of the road parts should be the same.
All first points of the road parts should be the same.
""" """
def __init__(self, parts: List[RoadPart]) -> None: def __init__(self, parts: List[RoadPart]) -> None:
@ -612,6 +613,8 @@ def get_curve_points(
is_end: bool, is_end: bool,
) -> List[np.ndarray]: ) -> List[np.ndarray]:
""" """
TODO: add description.
:param road: road segment :param road: road segment
:param center: road intersection point :param center: road intersection point
:param road_end: end point of the road segment :param road_end: end point of the road segment

View file

@ -1,3 +1,9 @@
"""
Drawing tree features on the map.
If radius of trunk or crown are specified they are displayed with simple
circles.
"""
import numpy as np import numpy as np
from colour import Color from colour import Color
from svgwrite import Drawing from svgwrite import Drawing

View file

@ -1,6 +1,4 @@
""" """Figures displayed on the map."""
Figures displayed on the map.
"""
from typing import Dict, List from typing import Dict, List
import numpy as np import numpy as np
@ -95,6 +93,26 @@ class StyledFigure(Figure):
return path return path
def get_layer(self) -> float:
"""
Get figure layer value or 0 if it is not specified.
TODO: support values separated by "," or ";".
"""
try:
if "layer" in self.tags:
return float(self.tags["layer"])
except ValueError:
return 0.0
return 0.0
def __lt__(self, other: "StyledFigure") -> bool:
"""Compare figures based on priority and layer."""
if self.get_layer() != other.get_layer():
return self.get_layer() < other.get_layer()
return self.line_style.priority < other.line_style.priority
def is_clockwise(polygon: List[OSMNode]) -> bool: def is_clockwise(polygon: List[OSMNode]) -> bool:
""" """

View file

@ -1,3 +1 @@
""" """Map geometry: dealing with coordinates, projections."""
Map geometry: dealing with coordinates, projections.
"""

View file

@ -1,6 +1,4 @@
""" """Rectangle that limit space on the map."""
Rectangle that limit space on the map.
"""
import logging import logging
import re import re
from dataclasses import dataclass from dataclasses import dataclass
@ -83,8 +81,7 @@ class BoundaryBox:
height: float, height: float,
) -> "BoundaryBox": ) -> "BoundaryBox":
""" """
Compute boundary box from central coordinates, zoom level and resulting Compute boundary box from center coordinates, zoom level and image size.
image size.
:param coordinates: boundary box central coordinates :param coordinates: boundary box central coordinates
:param zoom_level: resulting image zoom level :param zoom_level: resulting image zoom level
@ -146,7 +143,9 @@ class BoundaryBox:
def get_format(self) -> str: def get_format(self) -> str:
""" """
Get text representation of the boundary box: Get text representation of the boundary box.
Boundary box format is
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2>. Coordinates are <longitude 1>,<latitude 1>,<longitude 2>,<latitude 2>. Coordinates are
rounded to three digits after comma. rounded to three digits after comma.
""" """

View file

@ -1,6 +1,4 @@
""" """Geo projection."""
Geo projection.
"""
from typing import Optional from typing import Optional
import numpy as np import numpy as np
@ -13,26 +11,30 @@ __email__ = "me@enzet.ru"
def pseudo_mercator(coordinates: np.ndarray) -> np.ndarray: def pseudo_mercator(coordinates: np.ndarray) -> np.ndarray:
""" """
Use spherical pseudo-Mercator projection to convert geo coordinates into Use spherical pseudo-Mercator projection to convert geo coordinates.
plane.
The result is (x, y), where x is a longitude value, so x is in [-180, 180],
and y is a stretched latitude and may have any real value:
(-infinity, +infinity).
:param coordinates: geo positional in the form of (latitude, longitude) :param coordinates: geo positional in the form of (latitude, longitude)
:return: position on the plane in the form of (x, y) :return: position on the plane in the form of (x, y)
""" """
latitude, longitude = coordinates
y: float = ( y: float = (
180.0 180.0 / np.pi * np.log(np.tan(np.pi / 4.0 + latitude * np.pi / 360.0))
/ np.pi
* np.log(np.tan(np.pi / 4.0 + coordinates[0] * np.pi / 360.0))
) )
return np.array((coordinates[1], y)) return np.array((longitude, y))
def osm_zoom_level_to_pixels_per_meter( def osm_zoom_level_to_pixels_per_meter(
zoom_level: float, equator_length: float zoom_level: float, equator_length: float
) -> float: ) -> float:
""" """
Convert OSM zoom level to pixels per meter on Equator. See Convert OSM zoom level to pixels per meter on Equator.
https://wiki.openstreetmap.org/wiki/Zoom_levels
See https://wiki.openstreetmap.org/wiki/Zoom_levels
:param zoom_level: integer number usually not bigger than 20, but this :param zoom_level: integer number usually not bigger than 20, but this
function allows any non-negative float value function allows any non-negative float value
@ -42,7 +44,21 @@ def osm_zoom_level_to_pixels_per_meter(
class Flinger: class Flinger:
"""Convert geo coordinates into SVG position points.""" """Interface for flinger that converts coordinates."""
def __init__(self, size: np.ndarray) -> None:
self.size: np.ndarray = size
def fling(self, coordinates: np.ndarray) -> np.ndarray:
"""Do nothing but return coordinates unchanged."""
return coordinates
def get_scale(self, coordinates: Optional[np.ndarray] = None) -> float:
return 1.0
class MercatorFlinger(Flinger):
"""Convert geographical coordinates into (x, y) points on the plane."""
def __init__( def __init__(
self, self,
@ -51,30 +67,36 @@ class Flinger:
equator_length: float, equator_length: float,
) -> None: ) -> None:
""" """
Initialize flinger with geo boundary box and zoom level.
:param geo_boundaries: minimum and maximum latitude and longitude :param geo_boundaries: minimum and maximum latitude and longitude
:param zoom_level: zoom level in OpenStreetMap terminology :param zoom_level: zoom level in OpenStreetMap terminology
:param equator_length: celestial body equator length in meters :param equator_length: celestial body equator length in meters
""" """
self.geo_boundaries: BoundaryBox = geo_boundaries self.geo_boundaries: BoundaryBox = geo_boundaries
self.ratio: float = 2.0**zoom_level * 256.0 / 360.0 self.ratio: float = 2.0**zoom_level * 256.0 / 360.0
self.size: np.ndarray = self.ratio * ( size: np.ndarray = self.ratio * (
pseudo_mercator(self.geo_boundaries.max_()) pseudo_mercator(self.geo_boundaries.max_())
- pseudo_mercator(self.geo_boundaries.min_()) - pseudo_mercator(self.geo_boundaries.min_())
) )
self.pixels_per_meter: float = osm_zoom_level_to_pixels_per_meter( self.pixels_per_meter: float = osm_zoom_level_to_pixels_per_meter(
zoom_level, equator_length zoom_level, equator_length
) )
self.size: np.ndarray = self.size.astype(int).astype(float) size = size.astype(int).astype(float)
super().__init__(size)
self.min_ = self.ratio * pseudo_mercator(self.geo_boundaries.min_())
def fling(self, coordinates: np.ndarray) -> np.ndarray: def fling(self, coordinates: np.ndarray) -> np.ndarray:
""" """
Convert geo coordinates into SVG position points. Convert geo coordinates into (x, y) position points on the plane.
:param coordinates: vector to fling :param coordinates: geographical coordinates to fling in the form of
(latitude, longitude)
""" """
result: np.ndarray = self.ratio * ( result: np.ndarray = (
pseudo_mercator(coordinates) self.ratio * pseudo_mercator(coordinates) - self.min_
- pseudo_mercator(self.geo_boundaries.min_())
) )
# Invert y axis on coordinate plane. # Invert y axis on coordinate plane.
@ -86,7 +108,8 @@ class Flinger:
""" """
Return pixels per meter ratio for the given geo coordinates. Return pixels per meter ratio for the given geo coordinates.
:param coordinates: geo coordinates :param coordinates: geographical coordinates in the form of
(latitude, longitude)
""" """
if coordinates is None: if coordinates is None:
# Get pixels per meter ratio for the center of the boundary box. # Get pixels per meter ratio for the center of the boundary box.
@ -94,3 +117,15 @@ class Flinger:
scale_factor: float = abs(1.0 / np.cos(coordinates[0] / 180.0 * np.pi)) scale_factor: float = abs(1.0 / np.cos(coordinates[0] / 180.0 * np.pi))
return self.pixels_per_meter * scale_factor return self.pixels_per_meter * scale_factor
class TranslateFlinger(Flinger):
def __init__(
self, size: np.ndarray, scale: np.ndarray, offset: np.ndarray
) -> None:
super().__init__(size)
self.scale: np.ndarray = scale
self.offset: np.ndarray = offset
def fling(self, coordinates: np.ndarray) -> np.ndarray:
return self.scale * (coordinates + self.offset)

View file

@ -1,6 +1,6 @@
""" """Vector utility."""
Vector utility. from typing import Optional
"""
import numpy as np import numpy as np
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
@ -12,8 +12,9 @@ from shapely.geometry import LineString
def compute_angle(vector: np.ndarray) -> float: def compute_angle(vector: np.ndarray) -> float:
""" """
For the given vector compute an angle between it and (1, 0) vector. The For the given vector compute an angle between it and (1, 0) vector.
result is in [0, 2π].
The result is in [0, 2π].
""" """
if vector[0] == 0.0: if vector[0] == 0.0:
if vector[1] > 0.0: if vector[1] > 0.0:
@ -50,6 +51,7 @@ class Polyline:
def get_path(self, parallel_offset: float = 0.0) -> str: def get_path(self, parallel_offset: float = 0.0) -> str:
"""Construct SVG path commands.""" """Construct SVG path commands."""
points: List[np.ndarray] points: List[np.ndarray]
if np.allclose(parallel_offset, 0.0): if np.allclose(parallel_offset, 0.0):
points = self.points points = self.points
else: else:
@ -135,14 +137,22 @@ class Segment:
np.arccos(np.dot(vector, np.array((0.0, 1.0)))) / np.pi np.arccos(np.dot(vector, np.array((0.0, 1.0)))) / np.pi
) )
def __repr__(self): def __repr__(self) -> str:
"""Get simple string representation."""
return f"{self.point_1} -- {self.point_2}" return f"{self.point_1} -- {self.point_2}"
def __lt__(self, other: "Segment") -> bool: def __lt__(self, other: "Segment") -> bool:
"""Compare central y coordinates of segments."""
return self.y < other.y return self.y < other.y
def intersection(self, other: "Segment"): def intersection(self, other: "Segment") -> Optional[List[float]]:
divisor = (self.point_1[0] - self.point_2[0]) * ( """
Find and intersection point between two segments.
:return: `None` if segments don't intersect, [x, y] coordinates of
the resulting point otherwise.
"""
divisor: float = (self.point_1[0] - self.point_2[0]) * (
other.point_1[1] - other.point_2[1] other.point_1[1] - other.point_2[1]
) - (self.point_1[1] - self.point_2[1]) * ( ) - (self.point_1[1] - self.point_2[1]) * (
other.point_1[0] - other.point_2[0] other.point_1[0] - other.point_2[0]
@ -165,7 +175,6 @@ class Segment:
) / divisor ) / divisor
if 0 <= t <= 1 and 0 <= u <= 1: if 0 <= t <= 1 and 0 <= u <= 1:
print(t)
return [ return [
self.point_1[0] + t * (self.point_2[0] - self.point_1[0]), self.point_1[0] + t * (self.point_2[0] - self.point_1[0]),
self.point_1[1] + t * (self.point_2[1] - self.point_1[1]), self.point_1[1] + t * (self.point_2[1] - self.point_1[1]),

View file

@ -24,10 +24,15 @@
"mausoleum": {"name": "mausoleum", "categories": ["building"]}, "mausoleum": {"name": "mausoleum", "categories": ["building"]},
"minaret": {"name": "minaret", "categories": ["building"]}, "minaret": {"name": "minaret", "categories": ["building"]},
"pagoda": {"name": "pagoda", "categories": ["building"]}, "pagoda": {"name": "pagoda", "categories": ["building"]},
"townhall": {"name": "town hall", "categories": ["building"]} "pyramid": {"name": "pyramid", "categories": ["building"]},
"townhall": {"name": "town hall", "categories": ["building"]},
"food_court": {"name": "food court"}
}, },
"small": { "building_part": {
"main_entrance": {"name": "main entrance"},
"pillar": {"name": "pillar"}, "pillar": {"name": "pillar"},
"roof": {"is_part": true, "name": "roof"},
"roof_and_walls": {"is_part": true, "name": "roof and wall"},
"wayside_shrine": {"name": "wayside shrine"} "wayside_shrine": {"name": "wayside shrine"}
} }
}, },
@ -207,6 +212,7 @@
"shield_volcano": {"name": "shield volcano", "categories": ["natural"], "emoji": "🌋"}, "shield_volcano": {"name": "shield volcano", "categories": ["natural"], "emoji": "🌋"},
"smoke": {"name": "smoke"}, "smoke": {"name": "smoke"},
"smoke_2": {"name": "clouds"}, "smoke_2": {"name": "clouds"},
"spring": {"name": "spring", "categories": ["natural"]},
"stone": {"emoji": "🪨", "categories": ["natural"], "name": "stone"}, "stone": {"emoji": "🪨", "categories": ["natural"], "name": "stone"},
"stone_with_inscription": {"categories": ["natural"], "name": "stone with inscription"}, "stone_with_inscription": {"categories": ["natural"], "name": "stone with inscription"},
"stratovolcano": {"name": "stratovolcano", "categories": ["natural"], "emoji": "🌋"}, "stratovolcano": {"name": "stratovolcano", "categories": ["natural"], "emoji": "🌋"},
@ -300,6 +306,7 @@
}, },
"hand_items": { "hand_items": {
"bag": {"name": "bag"}, "bag": {"name": "bag"},
"bag_with_percent": {"name": "bag with percent"},
"book": {"emoji": "📕", "name": "book"}, "book": {"emoji": "📕", "name": "book"},
"books": {"emoji": "📚", "name": "books"} "books": {"emoji": "📚", "name": "books"}
}, },
@ -335,12 +342,11 @@
"fire_pit": {"name": "fire pit"}, "fire_pit": {"name": "fire pit"},
"fishing_angle": {"name": "fishing angle"}, "fishing_angle": {"name": "fishing angle"},
"flagpole": {"emoji": "🏴", "name": "flagpole"}, "flagpole": {"emoji": "🏴", "name": "flagpole"},
"food_court": {"name": "food court"},
"foot": {"emoji": "👣", "name": "footprint"},
"frame": {"name": "picture frame"}, "frame": {"name": "picture frame"},
"fuel_station": {"emoji": "⛽️", "name": "fuel station"}, "fuel_station": {"emoji": "⛽️", "name": "fuel station"},
"garages": {"name": "car under roof"}, "garages": {"name": "car under roof"},
"gavel": {"name": "gavel"}, "gavel": {"name": "gavel"},
"gazette": {"name": "gazette"},
"gift": {"emoji": "🎁", "name": "gift box"}, "gift": {"emoji": "🎁", "name": "gift box"},
"globe": {"name": "globe"}, "globe": {"name": "globe"},
"government": {"name": "building with dome and flag"}, "government": {"name": "building with dome and flag"},
@ -358,7 +364,6 @@
"lock_unlocked": {"name": "opened lock"}, "lock_unlocked": {"name": "opened lock"},
"lock_with_keyhole": {"emoji": "🔒", "name": "closed lock with keyhole"}, "lock_with_keyhole": {"emoji": "🔒", "name": "closed lock with keyhole"},
"lowered_kerb": {"name": "lowered kerb"}, "lowered_kerb": {"name": "lowered kerb"},
"main_entrance": {"name": "main entrance"},
"manhole_drain": {"name": "drain manhole cover"}, "manhole_drain": {"name": "drain manhole cover"},
"marketplace": {"name": "marketplace"}, "marketplace": {"name": "marketplace"},
"medicine_bottle": {"name": "medicine bottle"}, "medicine_bottle": {"name": "medicine bottle"},
@ -377,9 +382,7 @@
"pole": {"name": "pole"}, "pole": {"name": "pole"},
"power_generator": {"name": "power generator"}, "power_generator": {"name": "power generator"},
"prison": {"name": "bars"}, "prison": {"name": "bars"},
"restaurant": {"emoji": "🍴", "name": "fork and knife"}, "fork_and_knife": {"emoji": "🍴", "name": "fork and knife"},
"roof": {"is_part": true, "name": "roof"},
"roof_and_walls": {"is_part": true, "name": "roof and wall"},
"sheets": {"name": "two sheets"}, "sheets": {"name": "two sheets"},
"shelter": {"name": "shelter"}, "shelter": {"name": "shelter"},
"shop_convenience": {"name": "convenience store"}, "shop_convenience": {"name": "convenience store"},
@ -391,7 +394,7 @@
"stained_glass": {"name": "stained glass"}, "stained_glass": {"name": "stained glass"},
"staircase": {"name": "door with stairs"}, "staircase": {"name": "door with stairs"},
"statue_exhibit": {"name": "indoor statue"}, "statue_exhibit": {"name": "indoor statue"},
"supermarket_cart": {"name": "supermarket cart"}, "supermarket_cart": {"name": "supermarket cart", "emoji": "🛒"},
"support_column": {"is_part": true, "name": "support column"}, "support_column": {"is_part": true, "name": "support column"},
"support_pole": {"is_part": true, "name": "support pole"}, "support_pole": {"is_part": true, "name": "support pole"},
"support_wall": {"is_part": true, "name": "support wall"}, "support_wall": {"is_part": true, "name": "support wall"},
@ -412,21 +415,23 @@
"urban_tree_pot": {"is_part": true, "name": "tree pot"}, "urban_tree_pot": {"is_part": true, "name": "tree pot"},
"vanity_mirror": {"name": "vanity mirror"}, "vanity_mirror": {"name": "vanity mirror"},
"ventilation": {"name": "ventilation shaft"}, "ventilation": {"name": "ventilation shaft"},
"waving_flag": {"name": "waving flag"}, "waving_flag": {"name": "waving flag", "emoji": "🏳"},
"wretch_and_hammer": {"name": "wretch and hammer"} "wretch_and_hammer": {"name": "wretch and hammer"}
}, },
"body_part": { "body_part": {
"foot": {"emoji": "👣", "name": "footprint"},
"tooth": {"name": "tooth", "emoji": "🦷"} "tooth": {"name": "tooth", "emoji": "🦷"}
}, },
"clothes": { "clothes": {
"glasses": {"name": "glasses"}, "glasses": {"name": "glasses"},
"hanger": {"name": "hanger"},
"shoe": {"name": "shoe"}, "shoe": {"name": "shoe"},
"t_shirt": {"name": "T-shirt"}, "t_shirt": {"name": "T-shirt", "emoji": "👕"},
"t_shirt_and_scissors": {"name": "T-shirt and scissors"}, "t_shirt_and_scissors": {"name": "T-shirt and scissors"},
"watches": {"name": "hand watch"} "watches": {"name": "hand watch", "emoji": "⌚️"}
}, },
"sport": { "sport": {
"bowling_ball": {"categories": ["sport"], "name": "bowling ball"}, "bowling_ball": {"categories": ["sport"], "name": "bowling ball", "emoji": "🎳"},
"golf_club_and_ball": {"categories": ["sport"], "name": "golf club and ball"}, "golf_club_and_ball": {"categories": ["sport"], "name": "golf club and ball"},
"golf_pin": {"categories": ["sport"], "name": "golf pin"}, "golf_pin": {"categories": ["sport"], "name": "golf pin"},
"golf_tee": {"categories": ["sport"], "name": "golf tee"}, "golf_tee": {"categories": ["sport"], "name": "golf tee"},
@ -439,31 +444,38 @@
}, },
"recycling": { "recycling": {
"recycling_container": {"name": "recycling container with wheel"}, "recycling_container": {"name": "recycling container with wheel"},
"waste_basket": {"name": "waste basket"}, "waste_basket": {"name": "waste basket", "emoji": "🗑"},
"waste_disposal": {"name": "recycling container"} "waste_disposal": {"name": "recycling container", "emoji": "♻️"}
}, },
"electronic_device": { "electronic_device": {
"cctv": {"directed": "right", "name": "wall CCTV camera"}, "cctv": {"directed": "right", "name": "wall CCTV camera"},
"cctv_dome_wall": {"directed": "right", "name": "wall dome CCTV camera"}, "cctv_dome_wall": {"directed": "right", "name": "wall dome CCTV camera"},
"cctv_dome_ceiling": {"directed": "right", "name": "ceiling dome CCTV camera"}, "cctv_dome_ceiling": {"directed": "right", "name": "ceiling dome CCTV camera"},
"phone": {"name": "mobile phone"}, "phone": {"name": "mobile phone", "emoji": "📱"},
"photo_camera": {"emoji": "📷", "name": "photo camera"}, "photo_camera": {"emoji": "📷", "name": "photo camera"},
"telephone": {"emoji": "☎️", "name": "telephone"}, "telephone": {"emoji": "☎️", "name": "telephone"},
"tv": {"name": "monitor"} "tv": {"name": "monitor", "emoji": "🖥"}
}, },
"car_part": { "car_part": {
"engine": {"name": "engine"},
"tyre": {"name": "tyre"} "tyre": {"name": "tyre"}
}, },
"human": { "human": {
"massage": {"name": "massage"}, "massage": {"name": "massage"},
"pole_dancer": {"name": "pole dancer"}, "pole_dancer": {"name": "pole dancer"},
"sauna": {"name": "sauna"}, "sauna": {"name": "sauna", "emoji": "🧖"},
"two_people_together": {"emoji": "🧑‍🤝‍🧑", "name": "two people together"}, "two_people_together": {"emoji": "🧑‍🤝‍🧑", "name": "two people together"},
"woman_and_man": {"emoji": "🚻", "name": "woman and man"} "woman_and_man": {"emoji": "🚻", "name": "woman and man"}
}, },
"flag": {
"flag_usa": {"name": "flag of USA"},
"flag_bend_sinister": {"name": "flag with sinister bend"},
"flag_triangle_flanche": {"name": "flag with triangle on the left side"},
"flag_vertical_triband": {"name": "flag with three vertical bands"}
},
"tower": { "tower": {
"city_gate": {"name": "city gate"}, "city_gate": {"name": "city gate"},
"diving_platform": {"name": "diving platform"}, "diving_1_platforms": {"name": "diving tower with 1 platform"},
"diving_2_platforms": {"name": "diving tower with 2 platforms"}, "diving_2_platforms": {"name": "diving tower with 2 platforms"},
"diving_3_platforms": {"name": "diving tower with 3 platforms"}, "diving_3_platforms": {"name": "diving tower with 3 platforms"},
"diving_4_platforms": {"name": "diving tower with 4 platforms"}, "diving_4_platforms": {"name": "diving tower with 4 platforms"},

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -1,9 +1,18 @@
""" """Map Machine entry point."""
Map Machine entry point. import sys
"""
if sys.version_info.major < 3 or sys.version_info.minor < 8:
print(
"FATAL Python "
+ str(sys.version_info.major)
+ "."
+ str(sys.version_info.minor)
+ " is not supported. Please, use at least Python 3.8."
)
sys.exit(1)
import argparse import argparse
import logging import logging
import sys
from pathlib import Path from pathlib import Path
from map_machine.ui.cli import parse_arguments from map_machine.ui.cli import parse_arguments
@ -46,8 +55,8 @@ def main() -> None:
mapcss.generate_mapcss(arguments) mapcss.generate_mapcss(arguments)
elif arguments.command == "element": elif arguments.command == "draw":
from map_machine.element.single import draw_element from map_machine.element.element import draw_element
draw_element(arguments) draw_element(arguments)

View file

@ -1,13 +1,14 @@
""" """Map drawing configuration."""
Map drawing configuration.
"""
import argparse import argparse
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional from typing import Optional, Any, Dict, Set, Tuple
from colour import Color from colour import Color
from map_machine.pictogram.icon import ShapeExtractor, IconSet
from map_machine.scheme import Scheme
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
@ -44,6 +45,7 @@ class BuildingMode(Enum):
class MapConfiguration: class MapConfiguration:
"""Map drawing configuration.""" """Map drawing configuration."""
scheme: Scheme
drawing_mode: DrawingMode = DrawingMode.NORMAL drawing_mode: DrawingMode = DrawingMode.NORMAL
building_mode: BuildingMode = BuildingMode.FLAT building_mode: BuildingMode = BuildingMode.FLAT
label_mode: LabelMode = LabelMode.MAIN label_mode: LabelMode = LabelMode.MAIN
@ -58,13 +60,15 @@ class MapConfiguration:
use_building_colors: bool = False use_building_colors: bool = False
show_overlapped: bool = False show_overlapped: bool = False
credit: Optional[str] = "© OpenStreetMap contributors" credit: Optional[str] = "© OpenStreetMap contributors"
show_credit: bool = True
@classmethod @classmethod
def from_options( def from_options(
cls, options: argparse.Namespace, zoom_level: float cls, scheme: Scheme, options: argparse.Namespace, zoom_level: float
) -> "MapConfiguration": ) -> "MapConfiguration":
"""Initialize from command-line options.""" """Initialize from command-line options."""
return cls( return cls(
scheme,
DrawingMode(options.mode), DrawingMode(options.mode),
BuildingMode(options.buildings), BuildingMode(options.buildings),
LabelMode(options.label_mode), LabelMode(options.label_mode),
@ -89,3 +93,19 @@ class MapConfiguration:
if self.drawing_mode not in (DrawingMode.NORMAL, DrawingMode.BLACK): if self.drawing_mode not in (DrawingMode.NORMAL, DrawingMode.BLACK):
return Color("#111111") return Color("#111111")
return None return None
def get_icon(
self,
extractor: ShapeExtractor,
tags: Dict[str, Any],
processed: Set[str],
) -> Tuple[Optional[IconSet], int]:
return self.scheme.get_icon(
extractor,
tags,
processed,
self.country,
self.zoom_level,
self.ignore_level_matching,
self.show_overlapped,
)

View file

@ -1,6 +1,4 @@
""" """MapCSS scheme creation."""
MapCSS scheme creation.
"""
import argparse import argparse
import logging import logging
from pathlib import Path from pathlib import Path

View file

@ -1,6 +1,4 @@
""" """Simple OpenStreetMap renderer."""
Simple OpenStreetMap renderer.
"""
import argparse import argparse
import logging import logging
import sys import sys
@ -21,7 +19,7 @@ from map_machine.feature.building import Building, draw_walls, BUILDING_SCALE
from map_machine.feature.road import Intersection, Road, RoadPart from map_machine.feature.road import Intersection, Road, RoadPart
from map_machine.figure import StyledFigure from map_machine.figure import StyledFigure
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger from map_machine.geometry.flinger import Flinger, MercatorFlinger
from map_machine.geometry.vector import Segment from map_machine.geometry.vector import Segment
from map_machine.map_configuration import LabelMode, MapConfiguration from map_machine.map_configuration import LabelMode, MapConfiguration
from map_machine.osm.osm_getter import NetworkError, get_osm from map_machine.osm.osm_getter import NetworkError, get_osm
@ -36,6 +34,7 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
ROAD_PRIORITY: float = 40.0 ROAD_PRIORITY: float = 40.0
DEFAULT_SIZE = (800.0, 600.0)
class Map: class Map:
@ -45,12 +44,11 @@ class Map:
self, self,
flinger: Flinger, flinger: Flinger,
svg: svgwrite.Drawing, svg: svgwrite.Drawing,
scheme: Scheme,
configuration: MapConfiguration, configuration: MapConfiguration,
) -> None: ) -> None:
self.flinger: Flinger = flinger self.flinger: Flinger = flinger
self.svg: svgwrite.Drawing = svg self.svg: svgwrite.Drawing = svg
self.scheme: Scheme = scheme self.scheme: Scheme = configuration.scheme
self.configuration = configuration self.configuration = configuration
self.background_color: Color = self.scheme.get_color("background_color") self.background_color: Color = self.scheme.get_color("background_color")
@ -132,7 +130,8 @@ class Map:
self.svg, occupied, self.configuration.label_mode self.svg, occupied, self.configuration.label_mode
) )
self.draw_credits(constructor.flinger.size) if self.configuration.show_credit:
self.draw_credits(constructor.flinger.size)
def draw_buildings(self, constructor: Constructor) -> None: def draw_buildings(self, constructor: Constructor) -> None:
"""Draw buildings: shade, walls, and roof.""" """Draw buildings: shade, walls, and roof."""
@ -264,20 +263,28 @@ def render_map(arguments: argparse.Namespace) -> None:
:param arguments: command-line arguments :param arguments: command-line arguments
""" """
scheme_path: Optional[Path] = workspace.find_scheme_path(arguments.scheme)
if scheme_path is None:
fatal(f"Scheme `{arguments.scheme}` not found.")
scheme: Optional[Scheme] = Scheme.from_file(scheme_path)
if scheme is None:
fatal(f"Failed to load scheme from `{arguments.scheme}`.")
configuration: MapConfiguration = MapConfiguration.from_options( configuration: MapConfiguration = MapConfiguration.from_options(
arguments, float(arguments.zoom) scheme, arguments, float(arguments.zoom)
) )
cache_path: Path = Path(arguments.cache) cache_path: Path = Path(arguments.cache)
cache_path.mkdir(parents=True, exist_ok=True) cache_path.mkdir(parents=True, exist_ok=True)
# Compute boundary box # Compute boundary box.
boundary_box: Optional[BoundaryBox] = None boundary_box: Optional[BoundaryBox] = None
if arguments.boundary_box: if arguments.boundary_box:
boundary_box = BoundaryBox.from_text(arguments.boundary_box) boundary_box = BoundaryBox.from_text(arguments.boundary_box)
elif arguments.coordinates and arguments.size: elif arguments.coordinates:
coordinates: Optional[np.ndarray] = None coordinates: Optional[np.ndarray] = None
for delimiter in ",", "/": for delimiter in ",", "/":
@ -289,13 +296,20 @@ def render_map(arguments: argparse.Namespace) -> None:
if coordinates is None or len(coordinates) != 2: if coordinates is None or len(coordinates) != 2:
fatal("Wrong coordinates format.") fatal("Wrong coordinates format.")
width, height = np.array(list(map(float, arguments.size.split(",")))) if arguments.size:
width, height = np.array(
list(map(float, arguments.size.split(",")))
)
else:
width, height = DEFAULT_SIZE
boundary_box = BoundaryBox.from_coordinates( boundary_box = BoundaryBox.from_coordinates(
coordinates, configuration.zoom_level, width, height coordinates, configuration.zoom_level, width, height
) )
# Determine files # Determine files.
input_file_names: Optional[list[Path]] = None
if arguments.input_file_names: if arguments.input_file_names:
input_file_names = list(map(Path, arguments.input_file_names)) input_file_names = list(map(Path, arguments.input_file_names))
elif boundary_box: elif boundary_box:
@ -309,12 +323,9 @@ def render_map(arguments: argparse.Namespace) -> None:
logging.fatal(error.message) logging.fatal(error.message)
sys.exit(1) sys.exit(1)
else: else:
fatal( fatal("Specify either --input, or --boundary-box, or --coordinates.")
"Specify either --input, or --boundary-box, or --coordinates and "
"--size."
)
# Get OpenStreetMap data # Get OpenStreetMap data.
osm_data: OSMData = OSMData() osm_data: OSMData = OSMData()
for input_file_name in input_file_names: for input_file_name in input_file_names:
@ -332,9 +343,9 @@ def render_map(arguments: argparse.Namespace) -> None:
if not boundary_box: if not boundary_box:
boundary_box = osm_data.boundary_box boundary_box = osm_data.boundary_box
# Render # Render the map.
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
boundary_box, arguments.zoom, osm_data.equator_length boundary_box, arguments.zoom, osm_data.equator_length
) )
size: np.ndarray = flinger.size size: np.ndarray = flinger.size
@ -344,19 +355,15 @@ def render_map(arguments: argparse.Namespace) -> None:
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
) )
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data=osm_data, osm_data=osm_data,
flinger=flinger, flinger=flinger,
scheme=scheme,
extractor=icon_extractor, extractor=icon_extractor,
configuration=configuration, configuration=configuration,
) )
constructor.construct() constructor.construct()
map_: Map = Map( map_: Map = Map(flinger=flinger, svg=svg, configuration=configuration)
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
)
map_.draw(constructor) map_.draw(constructor)
logging.info(f"Writing output SVG to {arguments.output_file_name}...") logging.info(f"Writing output SVG to {arguments.output_file_name}...")

View file

@ -1,3 +1 @@
""" """OpenStreetMap-specific things."""
OpenStreetMap-specific things.
"""

View file

@ -1,6 +1,4 @@
""" """Getting OpenStreetMap data from the web."""
Getting OpenStreetMap data from the web.
"""
import logging import logging
import time import time
from dataclasses import dataclass from dataclasses import dataclass

View file

@ -1,6 +1,4 @@
""" """Parse OSM XML file."""
Parse OSM XML file.
"""
import json import json
import logging import logging
import re import re

View file

@ -1,3 +1 @@
""" """Icons and points."""
Icons and points.
"""

View file

@ -1,6 +1,4 @@
""" """Extract icons from SVG file."""
Extract icons from SVG file.
"""
import json import json
import logging import logging
import re import re
@ -34,6 +32,9 @@ PATH_MATCHER: re.Pattern = re.compile("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)")
GRID_STEP: int = 16 GRID_STEP: int = 16
USED_ICON_COLOR: str = "#000000"
UNUSED_ICON_COLORS: List[str] = ["#0000ff", "#ff0000"]
@dataclass @dataclass
class Shape: class Shape:
@ -168,9 +169,10 @@ def verify_sketch_element(element: Element, id_: str) -> bool:
""" """
Verify sketch SVG element from icon file. Verify sketch SVG element from icon file.
:param element: SVG element :param element: sketch SVG element (element with standard Inkscape
identifier)
:param id_: element `id` attribute :param id_: element `id` attribute
:return: True iff SVG element has right style :return: True iff SVG element has valid style
""" """
if "style" not in element.attrib or not element.attrib["style"]: if "style" not in element.attrib or not element.attrib["style"]:
return True return True
@ -180,7 +182,7 @@ def verify_sketch_element(element: Element, id_: str) -> bool:
for x in element.attrib["style"].split(";") for x in element.attrib["style"].split(";")
) )
# Sketch stroke element (black 0.1 px stroke, no fill). # Sketch element (black 0.1 px stroke, no fill).
if ( if (
style["fill"] == "none" style["fill"] == "none"
@ -190,20 +192,27 @@ def verify_sketch_element(element: Element, id_: str) -> bool:
): ):
return True return True
# Sketch fill element (black fill, no stroke, 20% opacity). # Sketch element (black 1 px stroke, no fill, 20% opacity).
if ( if (
style["fill"] == "none" style["fill"] == "none"
and style["stroke"] == "#000000" and style["stroke"] == "#000000"
and "opacity" in style and "opacity" in style
and np.allclose(float(style["opacity"]), 0.2) and np.allclose(float(style["opacity"]), 0.2)
and (
"stroke-width" not in style
or np.allclose(parse_length(style["stroke-width"]), 0.7)
or np.allclose(parse_length(style["stroke-width"]), 1)
or np.allclose(parse_length(style["stroke-width"]), 2)
or np.allclose(parse_length(style["stroke-width"]), 3)
)
): ):
return True return True
# Experimental shape (blue fill, no stroke). # Experimental shape (blue or red fill, no stroke).
if ( if (
style["fill"] == "#0000ff" style["fill"] in UNUSED_ICON_COLORS
and "stroke" in style and "stroke" in style
and style["stroke"] == "none" and style["stroke"] == "none"
): ):
@ -302,7 +311,12 @@ class ShapeExtractor:
id_: str = node.attrib["id"] id_: str = node.attrib["id"]
if STANDARD_INKSCAPE_ID_MATCHER.match(id_) is not None: if STANDARD_INKSCAPE_ID_MATCHER.match(id_) is not None:
if not verify_sketch_element(node, id_): if not verify_sketch_element(node, id_):
logging.warning(f"Not verified SVG element `{id_}`.") path_part = ""
try:
path_part = f", {node.attrib['d'].split(' ')[:3]}."
except (KeyError, ValueError):
pass
logging.warning(f"Not verified SVG element `{id_}`{path_part}")
return return
if "d" in node.attrib and node.attrib["d"]: if "d" in node.attrib and node.attrib["d"]:

View file

@ -1,6 +1,4 @@
""" """Icon grid drawing."""
Icon grid drawing.
"""
import logging import logging
import shutil import shutil
from dataclasses import dataclass from dataclasses import dataclass

View file

@ -1,6 +1,4 @@
""" """Point: node representation on the map."""
Point: node representation on the map.
"""
import logging import logging
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set

View file

@ -1,6 +1,4 @@
""" """Map Machine drawing scheme."""
Map Machine drawing scheme.
"""
import logging import logging
import re import re
from dataclasses import dataclass from dataclasses import dataclass
@ -13,7 +11,6 @@ import yaml
from colour import Color from colour import Color
from map_machine.feature.direction import DirectionSet from map_machine.feature.direction import DirectionSet
from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import Tagged, Tags from map_machine.osm.osm_reader import Tagged, Tags
from map_machine.pictogram.icon import ( from map_machine.pictogram.icon import (
DEFAULT_SHAPE_ID, DEFAULT_SHAPE_ID,
@ -30,6 +27,8 @@ __email__ = "me@enzet.ru"
IconDescription = List[Union[str, Dict[str, str]]] IconDescription = List[Union[str, Dict[str, str]]]
DEFAULT_COLOR: Color = Color("black")
@dataclass @dataclass
class LineStyle: class LineStyle:
@ -136,22 +135,21 @@ class Matcher(Tagged):
) )
def is_matched( def is_matched(
self, tags: Tags, configuration: Optional[MapConfiguration] = None self, tags: Tags, country: Optional[str] = None
) -> Tuple[bool, Dict[str, str]]: ) -> Tuple[bool, Dict[str, str]]:
""" """
Check whether element tags matches tag matcher. Check whether element tags matches tag matcher.
:param tags: element tags to be matched :param tags: element tags to be matched
:param configuration: current map configuration to be matched :param country: country of the element (to match location restrictions
if any)
""" """
groups: Dict[str, str] = {} groups: Dict[str, str] = {}
if ( if (
configuration is not None country is not None
and self.location_restrictions and self.location_restrictions
and not match_location( and not match_location(self.location_restrictions, country)
self.location_restrictions, configuration.country
)
): ):
return False, {} return False, {}
@ -365,15 +363,20 @@ class Scheme:
self.cache: Dict[str, Tuple[IconSet, int]] = {} self.cache: Dict[str, Tuple[IconSet, int]] = {}
@classmethod @classmethod
def from_file(cls, file_name: Path) -> "Scheme": def from_file(cls, file_name: Path) -> Optional["Scheme"]:
""" """
:param file_name: name of the scheme file with tags, colors, and tag key :param file_name: name of the scheme file with tags, colors, and tag key
specification specification
""" """
with file_name.open(encoding="utf-8") as input_file: with file_name.open(encoding="utf-8") as input_file:
content: Dict[str, Any] = yaml.load( try:
input_file.read(), Loader=yaml.FullLoader content: Dict[str, Any] = yaml.load(
) input_file.read(), Loader=yaml.FullLoader
)
except yaml.YAMLError:
return None
if not content:
return cls({})
return cls(content) return cls(content)
def get_color(self, color: str) -> Color: def get_color(self, color: str) -> Color:
@ -401,7 +404,9 @@ class Scheme:
return Color(color) return Color(color)
except (ValueError, AttributeError): except (ValueError, AttributeError):
logging.debug(f"Unknown color `{color}`.") logging.debug(f"Unknown color `{color}`.")
return Color(self.colors["default"]) if "default" in self.colors:
return Color(self.colors["default"])
return DEFAULT_COLOR
def get_default_color(self) -> Color: def get_default_color(self) -> Color:
"""Get default color for a main icon.""" """Get default color for a main icon."""
@ -413,6 +418,8 @@ class Scheme:
def get(self, variable_name: str): def get(self, variable_name: str):
""" """
Get value of variable.
FIXME: colors should be variables. FIXME: colors should be variables.
""" """
if variable_name in self.colors: if variable_name in self.colors:
@ -476,7 +483,10 @@ class Scheme:
extractor: ShapeExtractor, extractor: ShapeExtractor,
tags: Dict[str, Any], tags: Dict[str, Any],
processed: Set[str], processed: Set[str],
configuration: MapConfiguration = MapConfiguration(), country: Optional[str] = None,
zoom_level: float = 18,
ignore_level_matching: bool = False,
show_overlapped: bool = False,
) -> Tuple[Optional[IconSet], int]: ) -> Tuple[Optional[IconSet], int]:
""" """
Construct icon set. Construct icon set.
@ -484,7 +494,11 @@ class Scheme:
:param extractor: extractor with icon specifications :param extractor: extractor with icon specifications
:param tags: OpenStreetMap element tags dictionary :param tags: OpenStreetMap element tags dictionary
:param processed: set of already processed tag keys :param processed: set of already processed tag keys
:param configuration: current map configuration to be matched :param country: country to match location restrictions
:param zoom_level: current map zoom level
:param ignore_level_matching: do not check level for the icon
:param show_overlapped: get small dot instead of icon if point is
overlapped by some other points
:return (icon set, icon priority) :return (icon set, icon priority)
""" """
tags_hash: str = ( tags_hash: str = (
@ -501,12 +515,11 @@ class Scheme:
for index, matcher in enumerate(self.node_matchers): for index, matcher in enumerate(self.node_matchers):
if not matcher.replace_shapes and main_icon: if not matcher.replace_shapes and main_icon:
continue continue
matching, groups = matcher.is_matched(tags, configuration) matching, groups = matcher.is_matched(tags, country)
if not matching: if not matching:
continue continue
if ( if not ignore_level_matching and not matcher.check_zoom_level(
not configuration.ignore_level_matching zoom_level
and not matcher.check_zoom_level(configuration.zoom_level)
): ):
return None, 0 return None, 0
matcher_tags: Set[str] = set(matcher.tags.keys()) matcher_tags: Set[str] = set(matcher.tags.keys())
@ -567,7 +580,7 @@ class Scheme:
main_icon.recolor(color) main_icon.recolor(color)
default_icon: Optional[Icon] = None default_icon: Optional[Icon] = None
if configuration.show_overlapped: if show_overlapped:
small_dot_spec: ShapeSpecification = ShapeSpecification( small_dot_spec: ShapeSpecification = ShapeSpecification(
extractor.get_shape(DEFAULT_SMALL_SHAPE_ID), extractor.get_shape(DEFAULT_SMALL_SHAPE_ID),
color if color else self.get_color("default"), color if color else self.get_color("default"),

View file

@ -59,8 +59,9 @@ colors:
grass_border_color: "#BFD098" grass_border_color: "#BFD098"
grass_color: "#CFE0A8" grass_color: "#CFE0A8"
hidden_color: "#000000" hidden_color: "#000000"
indoor_border_color: "#C0B8B0" indoor_border_color: "#A0A890"
indoor_color: "#E8E4E0" indoor_color: "#E8E4E0"
indoor_column_color: {color: indoor_border_color, darken: 0.5}
meadow_border_color: "#BFD078" meadow_border_color: "#BFD078"
meadow_color: "#CFE088" meadow_color: "#CFE088"
orchard_color: "#B8DCA4" orchard_color: "#B8DCA4"
@ -435,6 +436,8 @@ node_icons:
shapes: [fort] shapes: [fort]
- tags: {shop: mall} - tags: {shop: mall}
shapes: [bag] shapes: [bag]
- tags: {shop: department_store}
shapes: [bag]
- tags: {shop: mall, building: "yes"} - tags: {shop: mall, building: "yes"}
shapes: [bag] shapes: [bag]
- tags: {leisure: water_park} - tags: {leisure: water_park}
@ -546,6 +549,8 @@ node_icons:
shapes: [star_of_david] shapes: [star_of_david]
- tags: {historic: tomb, tomb: mausoleum} - tags: {historic: tomb, tomb: mausoleum}
shapes: [mausoleum] shapes: [mausoleum]
- tags: {historic: tomb, tomb: pyramid}
shapes: [pyramid]
- tags: {historic: "*"} - tags: {historic: "*"}
shapes: [japan_historic] shapes: [japan_historic]
replace_shapes: no replace_shapes: no
@ -556,6 +561,8 @@ node_icons:
tags: tags:
- tags: {shop: supermarket} - tags: {shop: supermarket}
shapes: [supermarket_cart] shapes: [supermarket_cart]
- tags: {shop: variety_store}
shapes: [bag_with_percent]
- tags: {shop: general} - tags: {shop: general}
shapes: [bag] shapes: [bag]
- tags: {amenity: arts_centre} - tags: {amenity: arts_centre}
@ -670,6 +677,8 @@ node_icons:
shapes: [knives] shapes: [knives]
- tags: {shop: car} - tags: {shop: car}
shapes: [{shape: car, color: sell_color}] shapes: [{shape: car, color: sell_color}]
- tags: {shop: car_parts}
shapes: [shape: engine]
- tags: {shop: chocolate} - tags: {shop: chocolate}
shapes: [cupcake] shapes: [cupcake]
- tags: {shop: coffee} - tags: {shop: coffee}
@ -717,7 +726,7 @@ node_icons:
- tags: {shop: mobile_phone} - tags: {shop: mobile_phone}
shapes: [{shape: phone, color: sell_color}] shapes: [{shape: phone, color: sell_color}]
- tags: {shop: newsagent} - tags: {shop: newsagent}
shapes: [sheets] shapes: [gazette]
- tags: {shop: optician} - tags: {shop: optician}
shapes: [glasses] shapes: [glasses]
- tags: {shop: pastry} - tags: {shop: pastry}
@ -771,9 +780,9 @@ node_icons:
- tags: {amenity: nightclub} - tags: {amenity: nightclub}
shapes: [cocktail_glass_with_straw] shapes: [cocktail_glass_with_straw]
- tags: {amenity: restaurant} - tags: {amenity: restaurant}
shapes: [restaurant] shapes: [fork_and_knife]
- tags: {amenity: restaurant;bar} - tags: {amenity: restaurant;bar}
shapes: [restaurant] shapes: [fork_and_knife]
add_shapes: [cocktail_glass] add_shapes: [cocktail_glass]
- tags: {shop: ice_cream} - tags: {shop: ice_cream}
shapes: [ice_cream] shapes: [ice_cream]
@ -1140,6 +1149,10 @@ node_icons:
shapes: [power_pole_triangle_armless] shapes: [power_pole_triangle_armless]
- tags: {power: pole, design: delta} - tags: {power: pole, design: delta}
shapes: [power_pole_delta] shapes: [power_pole_delta]
- tags: {power: pole, design: delta_two-level}
shapes: [power_pole_delta] # power_pole_delta_2_level
- tags: {power: pole, design: delta_three-level}
shapes: [power_pole_delta] # power_pole_delta_3_level
- tags: {man_made: chimney} - tags: {man_made: chimney}
shapes: [chimney] shapes: [chimney]
@ -1147,16 +1160,6 @@ node_icons:
shapes: [tower_cooling] shapes: [tower_cooling]
- tags: {man_made: tower, tower:type: defensive} - tags: {man_made: tower, tower:type: defensive}
shapes: [tower_defensive] shapes: [tower_defensive]
- tags: {man_made: tower, tower:type: diving}
shapes: [diving_platform]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "1"}
shapes: [diving_platform]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "2"}
shapes: [diving_2_platforms]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "3"}
shapes: [diving_3_platforms]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "4"}
shapes: [diving_4_platforms]
- tags: {man_made: tower, tower:type: pagoda} - tags: {man_made: tower, tower:type: pagoda}
shapes: [pagoda] shapes: [pagoda]
- tags: {man_made: tower, tower:type: observation} - tags: {man_made: tower, tower:type: observation}
@ -1410,6 +1413,28 @@ node_icons:
- {shape: wave_left, offset: [-4, -3]} - {shape: wave_left, offset: [-4, -3]}
- {shape: wave_right, offset: [3, -3]} - {shape: wave_right, offset: [3, -3]}
- tags: {man_made: flagpole, country: US}
shapes: [flag_usa]
- tags: {man_made: flagpole, country: Tanzania}
shapes: [flag_bend_sinister]
- tags: {man_made: flagpole, country: PH}
shapes: [flag_triangle_flanche]
- tags: {man_made: flagpole, country: FR}
shapes: [flag_vertical_triband]
# Diving towers.
- tags: {man_made: tower, tower:type: diving}
shapes: [diving_1_platforms]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "1"}
shapes: [diving_1_platforms]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "2"}
shapes: [diving_2_platforms]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "3"}
shapes: [diving_3_platforms]
- tags: {man_made: tower, tower:type: diving, tower:platforms: "4"}
shapes: [diving_4_platforms]
- tags: {communication:mobile_phone: "yes"} - tags: {communication:mobile_phone: "yes"}
add_shapes: [phone] add_shapes: [phone]
@ -1470,6 +1495,8 @@ node_icons:
- group: "Important small objects" - group: "Important small objects"
start_zoom_level: 17.0 start_zoom_level: 17.0
tags: tags:
- tags: {natural: spring}
shapes: [{shape: spring, color: water_border_color}]
- tags: {highway: elevator} - tags: {highway: elevator}
shapes: [elevator] shapes: [elevator]
- tags: {historic: cannon} - tags: {historic: cannon}
@ -1495,7 +1522,7 @@ node_icons:
- tags: {historic: tomb} - tags: {historic: tomb}
shapes: [tomb] shapes: [tomb]
- tags: {tomb: "*"} - tags: {tomb: "*"}
exception: {tomb: mausoleum} exception: {tomb: mausoleum} # TODO: add exception "tomb: pyramid"
shapes: [tomb] shapes: [tomb]
- tags: {barrier: toll_booth} - tags: {barrier: toll_booth}
shapes: [toll_booth] shapes: [toll_booth]
@ -1653,6 +1680,8 @@ node_icons:
shapes: [hopscotch] shapes: [hopscotch]
- tags: {playground: slide} - tags: {playground: slide}
shapes: [slide] shapes: [slide]
- tags: {attraction: water_slide}
shapes: [slide_and_water]
- tags: {playground: roundabout} - tags: {playground: roundabout}
shapes: [roundabout] shapes: [roundabout]
- tags: {playground: sandpit} - tags: {playground: sandpit}
@ -1669,6 +1698,8 @@ node_icons:
shapes: [golf_pin] shapes: [golf_pin]
- tags: {highway: traffic_mirror} - tags: {highway: traffic_mirror}
shapes: [side_mirror] shapes: [side_mirror]
- tags: {amenity: dressing_room}
shapes: [hanger]
- group: "Entrances" - group: "Entrances"
start_zoom_level: 18.0 start_zoom_level: 18.0
@ -1961,7 +1992,7 @@ node_icons:
- tags: {recycling:glass_bottles: "yes"} - tags: {recycling:glass_bottles: "yes"}
add_shapes: [bottle] add_shapes: [bottle]
- tags: {recycling:paper: "yes"} - tags: {recycling:paper: "yes"}
add_shapes: [sheets] add_shapes: [gazette]
- tags: {recycling:glass: "yes"} - tags: {recycling:glass: "yes"}
add_shapes: [bottle_and_wine_glass] add_shapes: [bottle_and_wine_glass]
- tags: {recycling:clothes: "yes"} - tags: {recycling:clothes: "yes"}
@ -1971,11 +2002,11 @@ node_icons:
- tags: {recycling:green_waste: "yes"} - tags: {recycling:green_waste: "yes"}
add_shapes: [apple] add_shapes: [apple]
- tags: {recycling:paper_packaging: "yes"} - tags: {recycling:paper_packaging: "yes"}
add_shapes: [sheets] add_shapes: [gazette]
- tags: {recycling:newspaper: "yes"} - tags: {recycling:newspaper: "yes"}
add_shapes: [sheets] add_shapes: [gazette]
- tags: {recycling:magazines: "yes"} - tags: {recycling:magazines: "yes"}
add_shapes: [sheets] add_shapes: [gazette]
- tags: {recycling:books: "yes"} - tags: {recycling:books: "yes"}
add_shapes: [book] add_shapes: [book]
- tags: {recycling:wood: "yes"} - tags: {recycling:wood: "yes"}
@ -2095,7 +2126,7 @@ ways:
priority: 10.0 priority: 10.0
- tags: {indoor: corridor} - tags: {indoor: corridor}
style: style:
stroke: indoor_color stroke: indoor_border_color
stroke-width: 1.0 stroke-width: 1.0
fill: indoor_color fill: indoor_color
priority: 11.0 priority: 11.0
@ -2110,11 +2141,10 @@ ways:
stroke-width: 1.0 stroke-width: 1.0
fill: indoor_color fill: indoor_color
priority: 12.0 priority: 12.0
- tags: {indoor: room, area: "yes"} - tags: {indoor: room}
style: style:
stroke: indoor_color stroke: indoor_border_color
stroke-width: 1.0 stroke-width: 1.0
fill: indoor_color
priority: 12.0 priority: 12.0
- tags: {indoor: elevator, area: "yes"} - tags: {indoor: elevator, area: "yes"}
style: style:
@ -2124,9 +2154,9 @@ ways:
priority: 12.0 priority: 12.0
- tags: {indoor: column} - tags: {indoor: column}
style: style:
stroke: indoor_color stroke: indoor_column_color
stroke-width: 1.0 stroke-width: 1.0
fill: indoor_color fill: indoor_column_color
priority: 13.0 priority: 13.0
- tags: {power: line} - tags: {power: line}
@ -2160,6 +2190,17 @@ ways:
stroke-dasharray: "1.0,10.0,1.0,1.5" stroke-dasharray: "1.0,10.0,1.0,1.5"
priority: 80.0 priority: 80.0
- tags: {attraction: water_slide}
style:
stroke: "#FFFFFF"
stroke-width: 2.0
priority: 81
- tags: {attraction: water_slide}
style:
stroke: "#888888"
stroke-width: 4.0
priority: 80.0
- tags: {highway: track} - tags: {highway: track}
style: style:
stroke-width: 1.5 stroke-width: 1.5

View file

@ -1,3 +1 @@
""" """Tiles generation for slippy maps."""
Tiles generation for slippy maps.
"""

View file

@ -1,6 +1,4 @@
""" """Map Machine tile server for slippy maps."""
Map Machine tile server for slippy maps.
"""
import argparse import argparse
import logging import logging
from http.server import HTTPServer, SimpleHTTPRequestHandler from http.server import HTTPServer, SimpleHTTPRequestHandler
@ -77,7 +75,7 @@ def run_server(options: argparse.Namespace) -> None:
handler = TileServerHandler handler = TileServerHandler
handler.cache = Path(options.cache) handler.cache = Path(options.cache)
handler.options = options handler.options = options
server: HTTPServer = HTTPServer(("", options.port), handler) server = HTTPServer(("", options.port), handler)
logging.info(f"Server started on port {options.port}.") logging.info(f"Server started on port {options.port}.")
server.serve_forever() server.serve_forever()
finally: finally:

View file

@ -17,7 +17,7 @@ from PIL import Image
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger from map_machine.geometry.flinger import MercatorFlinger
from map_machine.map_configuration import MapConfiguration from map_machine.map_configuration import MapConfiguration
from map_machine.mapper import Map from map_machine.mapper import Map
from map_machine.osm.osm_getter import NetworkError, get_osm from map_machine.osm.osm_getter import NetworkError, get_osm
@ -158,7 +158,7 @@ class Tile:
self.x + 1, self.y + 1, self.zoom_level self.x + 1, self.y + 1, self.zoom_level
).get_coordinates() ).get_coordinates()
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
BoundaryBox(left, bottom, right, top), BoundaryBox(left, bottom, right, top),
self.zoom_level, self.zoom_level,
osm_data.equator_length, osm_data.equator_length,
@ -173,14 +173,13 @@ class Tile:
icon_extractor: ShapeExtractor = ShapeExtractor( icon_extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
) )
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data, flinger, scheme, icon_extractor, configuration osm_data, flinger, icon_extractor, configuration
) )
constructor.construct() constructor.construct()
painter: Map = Map( painter: Map = Map(
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration flinger=flinger, svg=svg, configuration=configuration
) )
painter.draw(constructor) painter.draw(constructor)
@ -382,7 +381,7 @@ class Tiles:
self.zoom_level, self.zoom_level,
).get_coordinates() ).get_coordinates()
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
BoundaryBox(left, bottom, right, top), BoundaryBox(left, bottom, right, top),
self.zoom_level, self.zoom_level,
osm_data.equator_length, osm_data.equator_length,
@ -390,16 +389,15 @@ class Tiles:
extractor: ShapeExtractor = ShapeExtractor( extractor: ShapeExtractor = ShapeExtractor(
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
) )
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data, flinger, scheme, extractor, configuration osm_data, flinger, extractor, configuration
) )
constructor.construct() constructor.construct()
svg: svgwrite.Drawing = svgwrite.Drawing( svg: svgwrite.Drawing = svgwrite.Drawing(
str(output_path), size=flinger.size str(output_path), size=flinger.size
) )
map_: Map = Map(flinger, svg, scheme, configuration) map_: Map = Map(flinger, svg, configuration)
map_.draw(constructor) map_.draw(constructor)
logging.info(f"Writing output SVG {output_path}...") logging.info(f"Writing output SVG {output_path}...")
@ -472,17 +470,30 @@ def generate_tiles(options: argparse.Namespace) -> None:
zoom_levels: List[int] = parse_zoom_level(options.zoom) zoom_levels: List[int] = parse_zoom_level(options.zoom)
min_zoom_level: int = min(zoom_levels) min_zoom_level: int = min(zoom_levels)
scheme: Scheme = Scheme.from_file(
workspace.find_scheme_path(options.scheme)
)
if options.input_file_name: if options.input_file_name:
osm_data: OSMData = OSMData() osm_data: OSMData = OSMData()
osm_data.parse_osm_file(Path(options.input_file_name)) osm_data.parse_osm_file(Path(options.input_file_name))
if osm_data.view_box is None:
logging.fatal(
"Failed to parse boundary box input file "
f"{options.input_file_name}."
)
sys.exit(1)
boundary_box: BoundaryBox = osm_data.view_box
for zoom_level in zoom_levels: for zoom_level in zoom_levels:
configuration: MapConfiguration = MapConfiguration.from_options( configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level scheme, options, zoom_level
)
tiles: Tiles = Tiles.from_boundary_box(
osm_data.view_box, zoom_level
) )
tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level)
tiles.draw(directory, Path(options.cache), configuration, osm_data) tiles.draw(directory, Path(options.cache), configuration, osm_data)
elif options.coordinates: elif options.coordinates:
coordinates: List[float] = list( coordinates: List[float] = list(
map(float, options.coordinates.strip().split(",")) map(float, options.coordinates.strip().split(","))
@ -501,24 +512,28 @@ def generate_tiles(options: argparse.Namespace) -> None:
) )
try: try:
configuration: MapConfiguration = MapConfiguration.from_options( configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level scheme, options, zoom_level
) )
tile.draw_with_osm_data(osm_data, directory, configuration) tile.draw_with_osm_data(osm_data, directory, configuration)
except NetworkError as error: except NetworkError as error:
logging.fatal(error.message) logging.fatal(error.message)
elif options.tile: elif options.tile:
zoom_level, x, y = map(int, options.tile.split("/")) zoom_level, x, y = map(int, options.tile.split("/"))
tile: Tile = Tile(x, y, zoom_level) tile: Tile = Tile(x, y, zoom_level)
configuration: MapConfiguration = MapConfiguration.from_options( configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level scheme, options, zoom_level
) )
tile.draw(directory, Path(options.cache), configuration) tile.draw(directory, Path(options.cache), configuration)
elif options.boundary_box: elif options.boundary_box:
boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text( boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text(
options.boundary_box options.boundary_box
) )
if boundary_box is None: if boundary_box is None:
logging.fatal("Failed to parse boundary box.")
sys.exit(1) sys.exit(1)
min_tiles: Tiles = Tiles.from_boundary_box(boundary_box, min_zoom_level) min_tiles: Tiles = Tiles.from_boundary_box(boundary_box, min_zoom_level)
try: try:
osm_data: OSMData = min_tiles.load_osm_data(Path(options.cache)) osm_data: OSMData = min_tiles.load_osm_data(Path(options.cache))
@ -531,9 +546,10 @@ def generate_tiles(options: argparse.Namespace) -> None:
else: else:
tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level) tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level)
configuration: MapConfiguration = MapConfiguration.from_options( configuration: MapConfiguration = MapConfiguration.from_options(
options, zoom_level scheme, options, zoom_level
) )
tiles.draw(directory, Path(options.cache), configuration, osm_data) tiles.draw(directory, Path(options.cache), configuration, osm_data)
else: else:
logging.fatal( logging.fatal(
"Specify either --coordinates, --boundary-box, --tile, or --input." "Specify either --coordinates, --boundary-box, --tile, or --input."

View file

@ -1,6 +1,4 @@
""" """OSM address tag processing."""
OSM address tag processing.
"""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Set from typing import Any, Dict, List, Optional, Set

View file

@ -1,3 +1 @@
""" """User interface."""
User interface.
"""

View file

@ -1,6 +1,4 @@
""" """Command-line user interface."""
Command-line user interface.
"""
import argparse import argparse
from typing import Dict, List from typing import Dict, List
@ -21,7 +19,7 @@ COMMAND_LINES: Dict[str, List[str]] = {
], ],
"icons": ["icons"], "icons": ["icons"],
"mapcss": ["mapcss"], "mapcss": ["mapcss"],
"element": ["element", "--node", "amenity=bench,material=wood"], "draw": ["draw", "node", "amenity=bench,material=wood"],
"tile": ["tile", "--coordinates", "50.000,40.000"], "tile": ["tile", "--coordinates", "50.000,40.000"],
} }
COMMANDS: List[str] = [ COMMANDS: List[str] = [
@ -88,9 +86,9 @@ def parse_arguments(args: List[str]) -> argparse.Namespace:
help="run tile server", help="run tile server",
) )
) )
add_element_arguments( add_draw_arguments(
subparser.add_parser( subparser.add_parser(
"element", "draw",
description="Draw map element separately.", description="Draw map element separately.",
help="draw OSM element: node, way, relation", help="draw OSM element: node, way, relation",
) )
@ -123,6 +121,13 @@ def parse_arguments(args: List[str]) -> argparse.Namespace:
def add_map_arguments(parser: argparse.ArgumentParser) -> None: def add_map_arguments(parser: argparse.ArgumentParser) -> None:
"""Add map-specific arguments.""" """Add map-specific arguments."""
parser.add_argument(
"--scheme",
metavar="<id> or <path>",
default="default",
help="scheme identifier (look for `<id>.yml` file) or path to a YAML "
"scheme file",
)
parser.add_argument( parser.add_argument(
"--buildings", "--buildings",
metavar="<mode>", metavar="<mode>",
@ -293,11 +298,11 @@ def add_server_arguments(parser: argparse.ArgumentParser) -> None:
) )
def add_element_arguments(parser: argparse.ArgumentParser) -> None: def add_draw_arguments(parser: argparse.ArgumentParser) -> None:
"""Add arguments for element command.""" """Add arguments for element command."""
parser.add_argument("-n", "--node") parser.add_argument("type")
parser.add_argument("-w", "--way") parser.add_argument("tags")
parser.add_argument("-r", "--relation") parser.add_argument("-o", "--output-file", default="out/element.svg")
def add_render_arguments(parser: argparse.ArgumentParser) -> None: def add_render_arguments(parser: argparse.ArgumentParser) -> None:

View file

@ -70,7 +70,7 @@ def completion_commands() -> str:
cli.add_tile_arguments(parser) cli.add_tile_arguments(parser)
cli.add_map_arguments(parser) cli.add_map_arguments(parser)
elif command == "element": elif command == "element":
cli.add_element_arguments(parser) cli.add_draw_arguments(parser)
elif command == "mapcss": elif command == "mapcss":
cli.add_mapcss_arguments(parser) cli.add_mapcss_arguments(parser)
else: else:

View file

@ -1,6 +1,4 @@
""" """Utility file."""
Utility file.
"""
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any

View file

@ -1,11 +1,11 @@
""" """File and directory path in the project."""
File and directory path in the project.
"""
from pathlib import Path from pathlib import Path
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
from typing import Optional
HERE: Path = Path(__file__).parent HERE: Path = Path(__file__).parent
@ -42,6 +42,33 @@ class Workspace:
self._mapcss_path: Path = output_path / "map_machine_mapcss" self._mapcss_path: Path = output_path / "map_machine_mapcss"
self._tile_path: Path = output_path / "tiles" self._tile_path: Path = output_path / "tiles"
def find_scheme_path(self, identifier: str) -> Optional[Path]:
"""
Find map scheme file by its identifier.
:param identifier: scheme identifier or file path.
:returns:
- default scheme file `default.yml` if identifier is not specified,
- `<identifier>.yml` from the default scheme directory (`scheme`) if
exists,
- path if identifier is a relative or absolute path to a scheme file.
- `None` otherwise.
See `Scheme`.
"""
if not identifier:
return self.DEFAULT_SCHEME_PATH
path: Path = self.SCHEME_PATH / (identifier + ".yml")
if path.is_file():
return path
path = Path(identifier)
if path.is_file():
return path
return None
def get_icons_by_id_path(self) -> Path: def get_icons_by_id_path(self) -> Path:
"""Directory for the icon files named by identifiers.""" """Directory for the icon files named by identifiers."""
return check_and_create(self._icons_by_id_path) return check_and_create(self._icons_by_id_path)

View file

@ -1,6 +1,4 @@
""" """Test boundary box."""
Test boundary box.
"""
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.boundary_box import BoundaryBox
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"

View file

@ -1,6 +1,4 @@
""" """Test color functions."""
Test color functions.
"""
from colour import Color from colour import Color
from map_machine.color import get_gradient_color, is_bright from map_machine.color import get_gradient_color, is_bright

View file

@ -1,6 +1,4 @@
""" """Test command line commands."""
Test command line commands.
"""
import argparse import argparse
from pathlib import Path from pathlib import Path
from subprocess import PIPE, Popen from subprocess import PIPE, Popen
@ -46,8 +44,8 @@ def test_wrong_render_arguments() -> None:
"""Test `render` command with wrong arguments.""" """Test `render` command with wrong arguments."""
error_run( error_run(
["render", "-z", "17"], ["render", "-z", "17"],
b"CRITICAL Specify either --input, or --boundary-box, or --coordinates " b"CRITICAL Specify either --input, or --boundary-box, or "
b"and --size.\n", b"--coordinates.\n",
) )
@ -115,20 +113,21 @@ def test_mapcss() -> None:
assert (out_path / "icons" / "LICENSE").is_file() assert (out_path / "icons" / "LICENSE").is_file()
def test_element() -> None: def test_draw() -> None:
"""Test `element` command.""" """Test `draw` command."""
run( run(
COMMAND_LINES["element"], COMMAND_LINES["draw"],
b"INFO Element is written to out/element.svg.\n", LOG + b"INFO Map is drawn to out/element.svg.\n",
) )
assert (OUTPUT_PATH / "element.svg").is_file() assert (OUTPUT_PATH / "element.svg").is_file()
def test_unwrapped_element() -> None: def test_unwrapped_draw() -> None:
"""Test `element` command from inside the project."""
arguments: argparse.Namespace = parse_arguments( arguments: argparse.Namespace = parse_arguments(
["map_machine"] + COMMAND_LINES["element"] ["map_machine"] + COMMAND_LINES["draw"]
) )
from map_machine.element.single import draw_element from map_machine.element.element import draw_element
draw_element(arguments) draw_element(arguments)

View file

@ -1,10 +1,7 @@
""" """Test Fish shell completion."""
Test Fish shell completion.
"""
from map_machine.ui.completion import completion_commands from map_machine.ui.completion import completion_commands
def test_completion() -> None: def test_completion() -> None:
"""Test Fish shell completion generation.""" """Test Fish shell completion generation."""
commands: str = completion_commands() assert completion_commands().startswith("set -l")
assert commands.startswith("set -l")

View file

@ -1,6 +1,4 @@
""" """Test direction processing."""
Test direction processing.
"""
import numpy as np import numpy as np
from map_machine.feature.direction import DirectionSet, parse_vector, Sector from map_machine.feature.direction import DirectionSet, parse_vector, Sector

View file

@ -1,6 +1,4 @@
""" """Test coordinates computation."""
Test coordinates computation.
"""
import numpy as np import numpy as np
from map_machine.geometry.flinger import ( from map_machine.geometry.flinger import (

View file

@ -11,6 +11,7 @@ from typing import Optional
from colour import Color from colour import Color
from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import Tags from map_machine.osm.osm_reader import Tags
from map_machine.pictogram.icon import IconSet, ShapeSpecification, Icon from map_machine.pictogram.icon import IconSet, ShapeSpecification, Icon
from map_machine.pictogram.icon_collection import IconCollection from map_machine.pictogram.icon_collection import IconCollection
@ -20,6 +21,7 @@ __author__ = "Sergey Vartanov"
__email__ = "me@enzet.ru" __email__ = "me@enzet.ru"
CONFIGURATION: MapConfiguration = MapConfiguration(SCHEME)
COLLECTION: IconCollection = IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR) COLLECTION: IconCollection = IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR)
DEFAULT_COLOR: Color = SCHEME.get_default_color() DEFAULT_COLOR: Color = SCHEME.get_default_color()
EXTRA_COLOR: Color = SCHEME.get_extra_color() EXTRA_COLOR: Color = SCHEME.get_extra_color()
@ -50,14 +52,16 @@ def test_icons_by_name() -> None:
def get_icon(tags: Tags) -> IconSet: def get_icon(tags: Tags) -> IconSet:
"""Construct icon from tags.""" """Construct icon from tags."""
processed: Set[str] = set() processed: Set[str] = set()
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed) icon, _ = CONFIGURATION.get_icon(SHAPE_EXTRACTOR, tags, processed)
return icon return icon
def test_no_icons() -> None: def test_no_icons() -> None:
""" """
Tags that has no description in scheme and should be visualized with default Test icon creation for tags not described in the scheme.
shape.
Tags that has no description in the scheme and should be visualized with
default shape.
""" """
icon: IconSet = get_icon({"aaa": "bbb"}) icon: IconSet = get_icon({"aaa": "bbb"})
assert icon.main_icon.is_default() assert icon.main_icon.is_default()
@ -66,6 +70,8 @@ def test_no_icons() -> None:
def test_no_icons_but_color() -> None: def test_no_icons_but_color() -> None:
""" """
Test icon creation for tags not described in the scheme and `colour` tag.
Tags that has no description in scheme, but have `colour` tag and should be Tags that has no description in scheme, but have `colour` tag and should be
visualized with default shape with the given color. visualized with default shape with the given color.
""" """
@ -77,11 +83,14 @@ def test_no_icons_but_color() -> None:
def check_icon_set( def check_icon_set(
tags: Tags, tags: Tags,
main_specification: List[Tuple[str, Optional[Color]]], main_specification: List[Tuple[str, Optional[Color]]],
extra_specifications: List[List[Tuple[str, Optional[Color]]]], extra_specifications: List[List[Tuple[str, Optional[Color]]]] = None,
) -> None: ) -> None:
"""Check icon set using simple specification.""" """Check icon set using simple specification."""
icon: IconSet = get_icon(tags) icon: IconSet = get_icon(tags)
if extra_specifications is None:
extra_specifications = []
if not main_specification: if not main_specification:
assert icon.main_icon.is_default() assert icon.main_icon.is_default()
else: else:
@ -110,7 +119,7 @@ def test_icon() -> None:
Tags that should be visualized with single main icon and without extra Tags that should be visualized with single main icon and without extra
icons. icons.
""" """
check_icon_set({"natural": "tree"}, [("tree", Color("#98AC64"))], []) check_icon_set({"natural": "tree"}, [("tree", Color("#98AC64"))])
def test_icon_1_extra() -> None: def test_icon_1_extra() -> None:
@ -165,7 +174,6 @@ def test_icon_regex() -> None:
check_icon_set( check_icon_set(
{"traffic_sign": "maxspeed", "maxspeed": "42"}, {"traffic_sign": "maxspeed", "maxspeed": "42"},
[("circle_11", DEFAULT_COLOR), ("digit_4", WHITE), ("digit_2", WHITE)], [("circle_11", DEFAULT_COLOR), ("digit_4", WHITE), ("digit_2", WHITE)],
[],
) )
@ -178,15 +186,30 @@ def test_vending_machine() -> None:
check_icon_set( check_icon_set(
{"amenity": "vending_machine"}, {"amenity": "vending_machine"},
[("vending_machine", DEFAULT_COLOR)], [("vending_machine", DEFAULT_COLOR)],
[],
) )
check_icon_set( check_icon_set(
{"amenity": "vending_machine", "vending": "drinks"}, {"amenity": "vending_machine", "vending": "drinks"},
[("vending_bottle", DEFAULT_COLOR)], [("vending_bottle", DEFAULT_COLOR)],
[],
) )
check_icon_set( check_icon_set(
{"vending": "drinks"}, {"vending": "drinks"},
[("vending_bottle", DEFAULT_COLOR)], [("vending_bottle", DEFAULT_COLOR)],
[], )
def test_diving_tower() -> None:
"""
Check that diving towers are rendered as diving towers, not just
freestanding towers.
See https://github.com/enzet/map-machine/issues/138
"""
check_icon_set(
{
"man_made": "tower",
"tower:type": "diving",
"tower:construction": "freestanding",
"tower:platforms": "4",
},
[("diving_4_platforms", DEFAULT_COLOR)],
) )

View file

@ -1,6 +1,4 @@
""" """Test label generation for nodes."""
Test label generation for nodes.
"""
from typing import Dict, List, Set from typing import Dict, List, Set
from map_machine.map_configuration import LabelMode from map_machine.map_configuration import LabelMode

View file

@ -1,6 +1,4 @@
""" """Test MapCSS generation."""
Test MapCSS generation.
"""
from map_machine.mapcss import MapCSSWriter from map_machine.mapcss import MapCSSWriter
from map_machine.scheme import NodeMatcher from map_machine.scheme import NodeMatcher
from tests import SCHEME from tests import SCHEME

View file

@ -1,6 +1,4 @@
""" """Test OSM XML parsing."""
Test OSM XML parsing.
"""
import numpy as np import numpy as np
from map_machine.osm.osm_reader import ( from map_machine.osm.osm_reader import (

View file

@ -1,18 +1,7 @@
"""
Check whether `requirements.txt` contains all requirements from `setup.py`.
"""
from pathlib import Path
from typing import List
from map_machine import REQUIREMENTS from map_machine import REQUIREMENTS
from pathlib import Path
def test_requirements() -> None: def test_requirements() -> None:
"""Test whether `requirements.txt` has the same packages as `setup.py`.""" with Path("requirements.txt").open() as requirements_file:
requirements: List[str] assert [x[:-1] for x in requirements_file.readlines()] == REQUIREMENTS
with Path("requirements.txt").open(encoding="utf-8") as requirements_file:
requirements = list(
map(lambda x: x[:-1], requirements_file.readlines())
)
assert requirements == REQUIREMENTS

View file

@ -1,12 +1,10 @@
""" """Test scheme parsing."""
Test scheme parsing.
"""
from typing import Any from typing import Any
from map_machine.scheme import Scheme from map_machine.scheme import Scheme
def test_verification() -> None: def test_verification_right() -> None:
"""Test verification process of tags in scheme.""" """Test verification process of tags in scheme."""
tags: dict[str, Any] = { tags: dict[str, Any] = {
@ -15,7 +13,9 @@ def test_verification() -> None:
} }
assert Scheme(tags).node_matchers[0].verify() is True assert Scheme(tags).node_matchers[0].verify() is True
# Tag value should be string, not integer.
def test_verification_wrong() -> None:
"""Tag value should be string, not integer."""
tags: dict[str, Any] = { tags: dict[str, Any] = {
"colors": {"default": "#444444"}, "colors": {"default": "#444444"},

View file

@ -1,6 +1,4 @@
""" """Test style constructing for ways and areas."""
Test style constructing for ways and areas.
"""
from tests import SCHEME from tests import SCHEME
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"

View file

@ -1,6 +1,4 @@
""" """Tests for length tag parsing."""
Tests for length tag parsing.
"""
from typing import Optional from typing import Optional
from map_machine.osm.osm_reader import Tagged from map_machine.osm.osm_reader import Tagged

View file

@ -1,6 +1,4 @@
""" """Test Taginfo project generation."""
Test Taginfo project generation.
"""
from pathlib import Path from pathlib import Path
from map_machine.doc.taginfo import TaginfoProjectFile from map_machine.doc.taginfo import TaginfoProjectFile

View file

@ -1,6 +1,4 @@
""" """Test text generation."""
Test text generation.
"""
from map_machine.text import format_voltage from map_machine.text import format_voltage
__author__ = "Sergey Vartanov" __author__ = "Sergey Vartanov"

View file

@ -1,6 +1,4 @@
""" """Test vector operations."""
Test vector operations.
"""
import numpy as np import numpy as np
from map_machine.geometry.vector import compute_angle, turn_by_angle from map_machine.geometry.vector import compute_angle, turn_by_angle

View file

@ -9,22 +9,24 @@ import numpy as np
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
from map_machine.figure import Figure from map_machine.figure import Figure
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger from map_machine.geometry.flinger import MercatorFlinger
from map_machine.map_configuration import MapConfiguration from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags
from tests import SCHEME, SHAPE_EXTRACTOR from tests import SCHEME, SHAPE_EXTRACTOR
CONFIGURATION: MapConfiguration = MapConfiguration(SCHEME)
def get_constructor(osm_data: OSMData) -> Constructor: def get_constructor(osm_data: OSMData) -> Constructor:
""" """
Get custom constructor for bounds (-0.01, -0.01, 0.01, 0.01) and zoom level Get custom constructor for bounds (-0.01, -0.01, 0.01, 0.01) and zoom level
18. 18.
""" """
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length
) )
constructor: Constructor = Constructor( constructor: Constructor = Constructor(
osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, MapConfiguration() osm_data, flinger, SHAPE_EXTRACTOR, CONFIGURATION
) )
constructor.construct_ways() constructor.construct_ways()
return constructor return constructor

View file

@ -1,6 +1,4 @@
""" """Test zoom level specification parsing."""
Test zoom level specification parsing.
"""
from map_machine.slippy.tile import ( from map_machine.slippy.tile import (
ScaleConfigurationException, ScaleConfigurationException,
parse_zoom_level, parse_zoom_level,