Merge main.
1
.gitignore
vendored
|
@ -16,6 +16,7 @@ cache/
|
|||
|
||||
# Work files
|
||||
|
||||
diffs
|
||||
work
|
||||
precommit.py
|
||||
|
||||
|
|
54
CHANGELOG.md
Normal 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)).
|
|
@ -91,6 +91,8 @@
|
|||
"tags": {},
|
||||
"row_tags": [
|
||||
{"amenity": "bench"},
|
||||
{"amenity": "bench", "backrest": "yes"},
|
||||
{"amenity": "bench", "backrest": "no"},
|
||||
{"memorial": "bench"}
|
||||
]
|
||||
},
|
||||
|
@ -100,9 +102,13 @@
|
|||
"id": "mast",
|
||||
"tags": {"man_made": "mast"},
|
||||
"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_values": ["", "communication", "lighting", "monitoring", "siren"]
|
||||
"column_values": [
|
||||
"", "communication", "lighting", "monitoring", "siren"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Volcano",
|
||||
|
|
Before Width: | Height: | Size: 424 KiB After Width: | Height: | Size: 439 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 36 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Map Machine project: simple Python map renderer for OpenStreetMap and icon set.
|
||||
"""
|
||||
"""Map Machine: Python map renderer for OpenStreetMap with custom icon set."""
|
||||
|
||||
__project__ = "Map Machine"
|
||||
__description__ = (
|
||||
|
@ -11,7 +9,7 @@ __url__ = "https://github.com/enzet/map-machine"
|
|||
__doc_url__ = f"{__url__}/blob/main/README.md"
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
__version__ = "0.1.5"
|
||||
__version__ = "0.1.7"
|
||||
|
||||
REQUIREMENTS = [
|
||||
"CairoSVG>=2.5.0",
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Map Machine entry point.
|
||||
"""
|
||||
"""Map Machine entry point."""
|
||||
from map_machine.main import main
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Color utility.
|
||||
"""
|
||||
"""Color utility."""
|
||||
from typing import Any, List
|
||||
|
||||
from colour import Color
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Construct Map Machine nodes and ways.
|
||||
"""
|
||||
"""Construct Map Machine nodes and ways."""
|
||||
import logging
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
@ -57,7 +55,7 @@ TIME_COLOR_SCALE: List[Color] = [
|
|||
|
||||
def line_center(
|
||||
nodes: List[OSMNode], flinger: Flinger
|
||||
) -> (np.ndarray, np.ndarray):
|
||||
) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Get geometric center of nodes set.
|
||||
|
||||
|
@ -76,9 +74,7 @@ def line_center(
|
|||
|
||||
|
||||
def get_user_color(text: str, seed: str) -> Color:
|
||||
"""
|
||||
Generate random color based on text.
|
||||
"""
|
||||
"""Generate random color based on text."""
|
||||
if text == "":
|
||||
return Color("black")
|
||||
return Color("#" + sha256((seed + text).encode("utf-8")).hexdigest()[-6:])
|
||||
|
@ -160,13 +156,12 @@ class Constructor:
|
|||
self,
|
||||
osm_data: OSMData,
|
||||
flinger: Flinger,
|
||||
scheme: Scheme,
|
||||
extractor: ShapeExtractor,
|
||||
configuration: MapConfiguration,
|
||||
) -> None:
|
||||
self.osm_data: OSMData = osm_data
|
||||
self.flinger: Flinger = flinger
|
||||
self.scheme: Scheme = scheme
|
||||
self.scheme: Scheme = configuration.scheme
|
||||
self.extractor: ShapeExtractor = extractor
|
||||
self.configuration: MapConfiguration = configuration
|
||||
self.text_constructor: TextConstructor = TextConstructor(self.scheme)
|
||||
|
@ -310,8 +305,8 @@ class Constructor:
|
|||
|
||||
priority: int
|
||||
icon_set: IconSet
|
||||
icon_set, priority = self.scheme.get_icon(
|
||||
self.extractor, line.tags, processed, self.configuration
|
||||
icon_set, priority = self.configuration.get_icon(
|
||||
self.extractor, line.tags, processed
|
||||
)
|
||||
if icon_set is not None:
|
||||
labels: List[Label] = self.text_constructor.construct_text(
|
||||
|
@ -331,9 +326,8 @@ class Constructor:
|
|||
)
|
||||
self.points.append(point)
|
||||
|
||||
if line_styles:
|
||||
return
|
||||
|
||||
# TODO: probably we may want to skip the next part if `line_styles`
|
||||
# are not empty.
|
||||
self.add_point_for_line(center_point, inners, line, outers)
|
||||
|
||||
def add_point_for_line(self, center_point, inners, line, outers) -> None:
|
||||
|
@ -352,8 +346,8 @@ class Constructor:
|
|||
processed: Set[str] = set()
|
||||
priority: int
|
||||
icon_set: IconSet
|
||||
icon_set, priority = self.scheme.get_icon(
|
||||
self.extractor, line.tags, processed, self.configuration
|
||||
icon_set, priority = self.configuration.get_icon(
|
||||
self.extractor, line.tags, processed
|
||||
)
|
||||
if icon_set is not None:
|
||||
labels: List[Label] = self.text_constructor.construct_text(
|
||||
|
@ -418,6 +412,8 @@ class Constructor:
|
|||
"""Draw nodes."""
|
||||
logging.info("Constructing nodes...")
|
||||
|
||||
# Sort node vertically (using latitude values) to draw them from top to
|
||||
# bottom.
|
||||
sorted_node_ids: Iterator[int] = sorted(
|
||||
self.osm_data.nodes.keys(),
|
||||
key=lambda x: -self.osm_data.nodes[x].coordinates[0],
|
||||
|
@ -428,6 +424,7 @@ class Constructor:
|
|||
def construct_node(self, node: OSMNode) -> None:
|
||||
"""Draw one node."""
|
||||
tags: Dict[str, str] = node.tags
|
||||
|
||||
if not tags:
|
||||
return
|
||||
if not self.check_level(tags):
|
||||
|
@ -477,8 +474,8 @@ class Constructor:
|
|||
color = Color("#CCCCCC")
|
||||
if self.configuration.drawing_mode == DrawingMode.BLACK:
|
||||
color = Color("#444444")
|
||||
icon_set, priority = self.scheme.get_icon(
|
||||
self.extractor, tags, processed, self.configuration
|
||||
icon_set, priority = self.configuration.get_icon(
|
||||
self.extractor, tags, processed
|
||||
)
|
||||
icon_set.main_icon.recolor(color)
|
||||
point: Point = Point(
|
||||
|
@ -492,8 +489,8 @@ class Constructor:
|
|||
self.points.append(point)
|
||||
return
|
||||
|
||||
icon_set, priority = self.scheme.get_icon(
|
||||
self.extractor, tags, processed, self.configuration
|
||||
icon_set, priority = self.configuration.get_icon(
|
||||
self.extractor, tags, processed
|
||||
)
|
||||
if icon_set is None:
|
||||
return
|
||||
|
@ -529,7 +526,7 @@ class Constructor:
|
|||
|
||||
def get_sorted_figures(self) -> List[StyledFigure]:
|
||||
"""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:
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
Documentation utilities.
|
||||
"""
|
||||
"""Documentation utilities."""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Special icon collections for documentation.
|
||||
"""
|
||||
"""Special icon collections for documentation."""
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
@ -24,28 +22,40 @@ SCHEME: Scheme = Scheme.from_file(WORKSPACE.DEFAULT_SCHEME_PATH)
|
|||
EXTRACTOR: ShapeExtractor = ShapeExtractor(
|
||||
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
|
||||
class Collection:
|
||||
"""Icon collection."""
|
||||
|
||||
# Core tags
|
||||
# Core tags.
|
||||
tags: Tags
|
||||
|
||||
# Tag key to be used in rows
|
||||
# Tag key to be used in rows.
|
||||
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)
|
||||
|
||||
# Tag key to be used in columns
|
||||
# Tag key to be used in columns.
|
||||
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)
|
||||
|
||||
# List of tags to be used in rows
|
||||
# List of tags to be used in rows.
|
||||
row_tags: List[Tags] = field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
|
@ -91,20 +101,7 @@ class SVGTable:
|
|||
self.half_step: np.ndarray = np.array(
|
||||
(self.step / 2.0, self.step / 2.0)
|
||||
)
|
||||
|
||||
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: str = ",".join(MONOSPACE_FONTS)
|
||||
self.font_width: float = self.font_size * 0.7
|
||||
|
||||
self.size: List[float] = [
|
||||
|
@ -145,8 +142,8 @@ class SVGTable:
|
|||
if column_value:
|
||||
current_tags |= {self.collection.column_key: column_value}
|
||||
processed: Set[str] = set()
|
||||
icon, _ = SCHEME.get_icon(
|
||||
EXTRACTOR, current_tags, processed, MapConfiguration()
|
||||
icon, _ = MapConfiguration(SCHEME).get_icon(
|
||||
EXTRACTOR, current_tags, processed
|
||||
)
|
||||
processed = icon.processed
|
||||
if not icon:
|
||||
|
@ -171,6 +168,9 @@ class SVGTable:
|
|||
self.draw_cross(np.array((j, i)))
|
||||
|
||||
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})
|
||||
|
||||
def draw_rows(self) -> None:
|
||||
|
@ -308,7 +308,7 @@ class SVGTable:
|
|||
def draw_svg_tables(output_path: Path, html_file_path: Path) -> None:
|
||||
"""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)
|
||||
|
||||
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__":
|
||||
draw_svg_tables(Path("doc"), Path("result.html"))
|
||||
draw_svg_tables(Path("doc"), Path("out") / "collections.html")
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Icon grids for documentation.
|
||||
"""
|
||||
"""Icon grids for documentation."""
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Moire markup extension for Map Machine.
|
||||
"""
|
||||
"""Moire markup extension for Map Machine."""
|
||||
import argparse
|
||||
from abc import ABC
|
||||
from pathlib import Path
|
||||
|
@ -45,9 +43,7 @@ def parse_text(text: str, margins: str, tag_id: str) -> Code:
|
|||
|
||||
|
||||
class ArgumentParser(argparse.ArgumentParser):
|
||||
"""
|
||||
Argument parser that stores arguments and creates help in Moire markup.
|
||||
"""
|
||||
"""Parser that stores arguments and creates help in Moire markup."""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
self.arguments: List[Dict[str, Any]] = []
|
||||
|
@ -160,7 +156,7 @@ class MapMachineMoire(Default, ABC):
|
|||
elif command == "map":
|
||||
cli.add_map_arguments(parser)
|
||||
elif command == "element":
|
||||
cli.add_element_arguments(parser)
|
||||
cli.add_draw_arguments(parser)
|
||||
elif command == "mapcss":
|
||||
cli.add_mapcss_arguments(parser)
|
||||
else:
|
||||
|
@ -236,7 +232,7 @@ class MapMachineHTML(MapMachineMoire, DefaultHTML):
|
|||
|
||||
class MapMachineOSMWiki(MapMachineMoire, DefaultWiki):
|
||||
"""
|
||||
OpenStreetMap wiki.
|
||||
Moire convertor to OpenStreetMap wiki markup.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/Main_Page
|
||||
"""
|
||||
|
@ -249,7 +245,7 @@ class MapMachineOSMWiki(MapMachineMoire, DefaultWiki):
|
|||
)
|
||||
|
||||
def osm(self, arg: Arguments) -> str:
|
||||
"""OSM tag key or key–value pair of tag."""
|
||||
"""Add special OSM tag key or key–value pair of tag."""
|
||||
spec: str = self.clear(arg[0])
|
||||
if "=" in spec:
|
||||
key, tag = spec.split("=")
|
||||
|
@ -258,11 +254,11 @@ class MapMachineOSMWiki(MapMachineMoire, DefaultWiki):
|
|||
return f"{{{{Tag|{spec}}}}}"
|
||||
|
||||
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])}}}}}"
|
||||
|
||||
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"
|
||||
shape_id: str = self.clear(arg[0])
|
||||
name: str = self.extractor.get_shape(shape_id).name
|
||||
|
@ -275,15 +271,15 @@ class MapMachineMarkdown(MapMachineMoire, DefaultMarkdown):
|
|||
images = {}
|
||||
|
||||
def color(self, arg: Arguments) -> str:
|
||||
"""Simple color sample."""
|
||||
"""Ignore colors in Markdown."""
|
||||
return self.clear(arg[0])
|
||||
|
||||
def icon(self, arg: Arguments) -> str:
|
||||
"""Image with Röntgen icon."""
|
||||
"""Process image with Röntgen icon."""
|
||||
return f"[{self.clear(arg[0])}]"
|
||||
|
||||
def kbd(self, arg: Arguments) -> str:
|
||||
"""Keyboard key."""
|
||||
"""Process keyboard key."""
|
||||
return f"<kbd>{self.clear(arg[0])}</kbd>"
|
||||
|
||||
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>'
|
||||
|
||||
def formal(self, arg: Arguments) -> str:
|
||||
"""Formal variable."""
|
||||
"""Process formal variable."""
|
||||
return f"<{self.parse(arg[0])}>"
|
||||
|
||||
|
||||
|
|
|
@ -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 sys
|
||||
from pathlib import Path
|
||||
|
@ -11,7 +9,7 @@ import svgwrite
|
|||
|
||||
from map_machine.constructor import Constructor
|
||||
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 (
|
||||
BuildingMode,
|
||||
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.pictogram.icon import ShapeExtractor
|
||||
from map_machine.scheme import Scheme
|
||||
from map_machine.workspace import workspace
|
||||
|
||||
doc_path: Path = Path("doc")
|
||||
|
||||
cache: Path = Path("cache")
|
||||
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(
|
||||
Path("map_machine/icons/icons.svg"),
|
||||
Path("map_machine/icons/config.json"),
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
|
||||
|
||||
|
@ -40,23 +38,26 @@ def draw(
|
|||
input_file_name: Path,
|
||||
output_file_name: Path,
|
||||
boundary_box: BoundaryBox,
|
||||
configuration: MapConfiguration = MapConfiguration(),
|
||||
configuration: Optional[MapConfiguration] = None,
|
||||
) -> None:
|
||||
"""Draw file."""
|
||||
if configuration is None:
|
||||
configuration = MapConfiguration(SCHEME)
|
||||
|
||||
osm_data: OSMData = OSMData()
|
||||
osm_data.parse_osm_file(input_file_name)
|
||||
flinger: Flinger = Flinger(
|
||||
flinger: MercatorFlinger = MercatorFlinger(
|
||||
boundary_box, configuration.zoom_level, osm_data.equator_length
|
||||
)
|
||||
constructor: Constructor = Constructor(
|
||||
osm_data, flinger, SCHEME, EXTRACTOR, configuration
|
||||
osm_data, flinger, EXTRACTOR, configuration
|
||||
)
|
||||
constructor.construct()
|
||||
|
||||
svg: svgwrite.Drawing = svgwrite.Drawing(
|
||||
str(output_file_name), size=flinger.size
|
||||
)
|
||||
map_: Map = Map(flinger, svg, SCHEME, configuration)
|
||||
map_: Map = Map(flinger, svg, configuration)
|
||||
map_.draw(constructor)
|
||||
|
||||
svg.write(output_file_name.open("w"))
|
||||
|
@ -65,11 +66,14 @@ def draw(
|
|||
def draw_around_point(
|
||||
point: np.ndarray,
|
||||
name: str,
|
||||
configuration: MapConfiguration = MapConfiguration(),
|
||||
configuration: Optional[MapConfiguration] = None,
|
||||
size: np.ndarray = np.array((600, 400)),
|
||||
get: Optional[BoundaryBox] = None,
|
||||
) -> None:
|
||||
"""Draw around point."""
|
||||
if configuration is None:
|
||||
configuration = MapConfiguration(SCHEME)
|
||||
|
||||
input_path: Path = doc_path / f"{name}.svg"
|
||||
|
||||
boundary_box: BoundaryBox = BoundaryBox.from_coordinates(
|
||||
|
@ -92,7 +96,7 @@ def main(id_: str) -> None:
|
|||
draw_around_point(
|
||||
np.array((55.75277, 37.40856)),
|
||||
"fitness",
|
||||
MapConfiguration(zoom_level=20.2),
|
||||
MapConfiguration(SCHEME, zoom_level=20.2),
|
||||
np.array((300, 200)),
|
||||
)
|
||||
|
||||
|
@ -100,14 +104,14 @@ def main(id_: str) -> None:
|
|||
draw_around_point(
|
||||
np.array((52.5622, 12.94)),
|
||||
"power",
|
||||
configuration=MapConfiguration(zoom_level=15),
|
||||
configuration=MapConfiguration(SCHEME, zoom_level=15),
|
||||
)
|
||||
|
||||
if id_ is None or id_ == "playground":
|
||||
draw_around_point(
|
||||
np.array((52.47388, 13.43826)),
|
||||
"playground",
|
||||
configuration=MapConfiguration(zoom_level=19),
|
||||
configuration=MapConfiguration(SCHEME, zoom_level=19),
|
||||
)
|
||||
|
||||
# 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)),
|
||||
"surveillance",
|
||||
MapConfiguration(
|
||||
SCHEME,
|
||||
zoom_level=18.5,
|
||||
ignore_level_matching=True,
|
||||
),
|
||||
|
@ -128,6 +133,7 @@ def main(id_: str) -> None:
|
|||
np.array((52.421, 13.101)),
|
||||
"viewpoints",
|
||||
MapConfiguration(
|
||||
SCHEME,
|
||||
label_mode=LabelMode.NO,
|
||||
zoom_level=15.7,
|
||||
ignore_level_matching=True,
|
||||
|
@ -138,7 +144,7 @@ def main(id_: str) -> None:
|
|||
draw_around_point(
|
||||
np.array((-26.19049, 28.05605)),
|
||||
"buildings",
|
||||
MapConfiguration(building_mode=BuildingMode.ISOMETRIC),
|
||||
MapConfiguration(SCHEME, building_mode=BuildingMode.ISOMETRIC),
|
||||
)
|
||||
|
||||
if id_ is None or id_ == "trees":
|
||||
|
@ -146,7 +152,7 @@ def main(id_: str) -> None:
|
|||
np.array((55.751, 37.628)),
|
||||
"trees",
|
||||
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),
|
||||
)
|
||||
|
@ -160,6 +166,7 @@ def main(id_: str) -> None:
|
|||
np.array((55.7655, 37.6055)),
|
||||
"time",
|
||||
MapConfiguration(
|
||||
SCHEME,
|
||||
DrawingMode.TIME,
|
||||
zoom_level=16.5,
|
||||
ignore_level_matching=True,
|
||||
|
@ -171,6 +178,7 @@ def main(id_: str) -> None:
|
|||
np.array((55.7655, 37.6055)),
|
||||
"author",
|
||||
MapConfiguration(
|
||||
SCHEME,
|
||||
DrawingMode.AUTHOR,
|
||||
seed="a",
|
||||
zoom_level=16.5,
|
||||
|
@ -183,6 +191,7 @@ def main(id_: str) -> None:
|
|||
np.array((48.87422, 2.377)),
|
||||
"colors",
|
||||
configuration=MapConfiguration(
|
||||
SCHEME,
|
||||
zoom_level=17.6,
|
||||
building_mode=BuildingMode.ISOMETRIC,
|
||||
ignore_level_matching=True,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
"""
|
||||
Automate OpenStreetMap wiki editing.
|
||||
"""
|
||||
"""Automate OpenStreetMap wiki editing."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
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.osm.osm_reader import Tags
|
||||
|
@ -38,9 +36,7 @@ class WikiTable:
|
|||
self.page_name: str = page_name
|
||||
|
||||
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] = []
|
||||
text: str = '{| class="wikitable"\n'
|
||||
|
||||
|
@ -62,11 +58,10 @@ class WikiTable:
|
|||
text += f"{{{{Tag|{key}|{value}}}}}<br />"
|
||||
text = text[:-6]
|
||||
text += "\n"
|
||||
icon, _ = SCHEME.get_icon(
|
||||
EXTRACTOR,
|
||||
current_tags | self.collection.tags,
|
||||
set(),
|
||||
MapConfiguration(ignore_level_matching=True),
|
||||
icon, _ = MapConfiguration(
|
||||
SCHEME, ignore_level_matching=True
|
||||
).get_icon(
|
||||
EXTRACTOR, current_tags | self.collection.tags, set()
|
||||
)
|
||||
icons.append(icon.main_icon)
|
||||
text += (
|
||||
|
@ -107,7 +102,9 @@ class WikiTable:
|
|||
}
|
||||
if 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:
|
||||
print("Icon was not constructed.")
|
||||
text += (
|
||||
|
@ -139,8 +136,8 @@ def generate_new_text(
|
|||
wiki_text, icons = table.generate_wiki_table()
|
||||
else:
|
||||
processed = set()
|
||||
icon, _ = SCHEME.get_icon(
|
||||
EXTRACTOR, table.collection.tags, processed, MapConfiguration()
|
||||
icon, _ = MapConfiguration(SCHEME).get_icon(
|
||||
EXTRACTOR, table.collection.tags, processed
|
||||
)
|
||||
if not icon.main_icon.is_default():
|
||||
wiki_text = (
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Drawing utility.
|
||||
"""
|
||||
"""Drawing utility."""
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"""Drawing of separate map elements."""
|
44
map_machine/element/element.py
Normal 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
|
@ -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}.")
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Drawing separate map elements.
|
||||
"""
|
||||
"""Drawing separate map elements."""
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
@ -10,7 +8,9 @@ import numpy as np
|
|||
import svgwrite
|
||||
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.point import Point
|
||||
from map_machine.scheme import LineStyle, Scheme
|
||||
|
@ -21,6 +21,12 @@ __author__ = "Sergey Vartanov"
|
|||
__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:
|
||||
"""Draw single node, line, or area."""
|
||||
target: str
|
||||
|
@ -44,7 +50,7 @@ def draw_element(options: argparse.Namespace) -> None:
|
|||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
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"
|
||||
text_constructor: TextConstructor = TextConstructor(scheme)
|
||||
labels: List[Label] = text_constructor.construct_text(
|
171
map_machine/element/way.py
Normal 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")
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
Specific map features: roads, directions, etc.
|
||||
"""
|
||||
"""Specific map features: roads, directions, etc."""
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
"""
|
||||
Buildings on the map.
|
||||
"""
|
||||
"""Buildings on the map."""
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
from colour import Color
|
||||
from svgwrite import Drawing
|
||||
from svgwrite.container import Group
|
||||
|
@ -170,32 +169,44 @@ class Building(Figure):
|
|||
svg.add(path)
|
||||
|
||||
|
||||
def draw_walls(svg, building: Building, segment, height, shift_1, shift_2):
|
||||
fill: str
|
||||
def draw_walls(
|
||||
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:
|
||||
color_part: float = segment.angle * 0.2
|
||||
fill = Color(
|
||||
color = Color(
|
||||
rgb=(
|
||||
building.wall_color.get_red() + color_part,
|
||||
building.wall_color.get_green() + color_part,
|
||||
building.wall_color.get_blue() + color_part,
|
||||
)
|
||||
).hex
|
||||
)
|
||||
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:
|
||||
fill = building.wall_bottom_color_2.hex
|
||||
color = building.wall_bottom_color_2
|
||||
else:
|
||||
color_part: float = segment.angle * 0.2 - 0.1
|
||||
fill = Color(
|
||||
color = Color(
|
||||
rgb=(
|
||||
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_blue() + color_part, 1), 0),
|
||||
)
|
||||
).hex
|
||||
)
|
||||
|
||||
command = (
|
||||
command: PathCommands = [
|
||||
"M",
|
||||
segment.point_1 + shift_1,
|
||||
"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_1,
|
||||
"Z",
|
||||
)
|
||||
]
|
||||
path: Path = Path(
|
||||
d=command,
|
||||
fill=fill,
|
||||
stroke=fill,
|
||||
fill=color.hex,
|
||||
stroke=color.hex,
|
||||
stroke_width=1,
|
||||
stroke_linejoin="round",
|
||||
)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Crater on the map.
|
||||
"""
|
||||
"""Crater on the map."""
|
||||
import numpy as np
|
||||
from colour import Color
|
||||
from svgwrite import Drawing
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
"""
|
||||
Direction tag support.
|
||||
"""
|
||||
from typing import Iterator, List, Optional, Dict
|
||||
"""Direction tag support."""
|
||||
from typing import Iterator, Optional, List, Dict
|
||||
|
||||
import numpy as np
|
||||
from colour import Color
|
||||
|
@ -23,8 +21,9 @@ DEFAULT_ANGLE: float = np.pi / 30.0
|
|||
|
||||
def parse_vector(text: str) -> Optional[np.ndarray]:
|
||||
"""
|
||||
Parse vector from text representation: compass points or 360-degree
|
||||
notation. E.g. "NW", "270".
|
||||
Parse vector from text representation.
|
||||
|
||||
Compass points or 360-degree notation. E.g. "NW", "270".
|
||||
|
||||
:param text: vector text representation
|
||||
:return: parsed normalized vector
|
||||
|
@ -60,6 +59,8 @@ class Sector:
|
|||
|
||||
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 angle: angle in degrees
|
||||
"""
|
||||
|
@ -127,6 +128,8 @@ class DirectionSet:
|
|||
|
||||
def __init__(self, text: str) -> None:
|
||||
"""
|
||||
Construct direction set from text representation.
|
||||
|
||||
:param text: direction tag value
|
||||
"""
|
||||
self.sectors: Iterator[Optional[Sector]] = map(Sector, text.split(";"))
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
WIP: road shape drawing.
|
||||
"""
|
||||
"""WIP: road shape drawing."""
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
|
@ -67,6 +65,8 @@ class RoadPart:
|
|||
scale: float,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize road part with two end points.
|
||||
|
||||
:param point_1: start point of the road part
|
||||
:param point_2: end point of the road part
|
||||
:param lanes: lane specification
|
||||
|
@ -284,8 +284,9 @@ class RoadPart:
|
|||
|
||||
class Intersection:
|
||||
"""
|
||||
An intersection of the roads, that is described by its parts. All first
|
||||
points of the road parts should be the same.
|
||||
An intersection of the roads, that is described by its parts.
|
||||
|
||||
All first points of the road parts should be the same.
|
||||
"""
|
||||
|
||||
def __init__(self, parts: List[RoadPart]) -> None:
|
||||
|
@ -612,6 +613,8 @@ def get_curve_points(
|
|||
is_end: bool,
|
||||
) -> List[np.ndarray]:
|
||||
"""
|
||||
TODO: add description.
|
||||
|
||||
:param road: road segment
|
||||
:param center: road intersection point
|
||||
:param road_end: end point of the road segment
|
||||
|
|
|
@ -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
|
||||
from colour import Color
|
||||
from svgwrite import Drawing
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Figures displayed on the map.
|
||||
"""
|
||||
"""Figures displayed on the map."""
|
||||
from typing import Dict, List
|
||||
|
||||
import numpy as np
|
||||
|
@ -95,6 +93,26 @@ class StyledFigure(Figure):
|
|||
|
||||
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:
|
||||
"""
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
Map geometry: dealing with coordinates, projections.
|
||||
"""
|
||||
"""Map geometry: dealing with coordinates, projections."""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Rectangle that limit space on the map.
|
||||
"""
|
||||
"""Rectangle that limit space on the map."""
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
@ -83,8 +81,7 @@ class BoundaryBox:
|
|||
height: float,
|
||||
) -> "BoundaryBox":
|
||||
"""
|
||||
Compute boundary box from central coordinates, zoom level and resulting
|
||||
image size.
|
||||
Compute boundary box from center coordinates, zoom level and image size.
|
||||
|
||||
:param coordinates: boundary box central coordinates
|
||||
:param zoom_level: resulting image zoom level
|
||||
|
@ -146,7 +143,9 @@ class BoundaryBox:
|
|||
|
||||
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
|
||||
rounded to three digits after comma.
|
||||
"""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Geo projection.
|
||||
"""
|
||||
"""Geo projection."""
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
@ -13,26 +11,30 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
def pseudo_mercator(coordinates: np.ndarray) -> np.ndarray:
|
||||
"""
|
||||
Use spherical pseudo-Mercator projection to convert geo coordinates into
|
||||
plane.
|
||||
Use spherical pseudo-Mercator projection to convert geo coordinates.
|
||||
|
||||
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)
|
||||
:return: position on the plane in the form of (x, y)
|
||||
"""
|
||||
latitude, longitude = coordinates
|
||||
|
||||
y: float = (
|
||||
180.0
|
||||
/ np.pi
|
||||
* np.log(np.tan(np.pi / 4.0 + coordinates[0] * np.pi / 360.0))
|
||||
180.0 / np.pi * np.log(np.tan(np.pi / 4.0 + latitude * np.pi / 360.0))
|
||||
)
|
||||
return np.array((coordinates[1], y))
|
||||
return np.array((longitude, y))
|
||||
|
||||
|
||||
def osm_zoom_level_to_pixels_per_meter(
|
||||
zoom_level: float, equator_length: float
|
||||
) -> float:
|
||||
"""
|
||||
Convert OSM zoom level to pixels per meter on Equator. See
|
||||
https://wiki.openstreetmap.org/wiki/Zoom_levels
|
||||
Convert OSM zoom level to pixels per meter on Equator.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/Zoom_levels
|
||||
|
||||
:param zoom_level: integer number usually not bigger than 20, but this
|
||||
function allows any non-negative float value
|
||||
|
@ -42,7 +44,21 @@ def osm_zoom_level_to_pixels_per_meter(
|
|||
|
||||
|
||||
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__(
|
||||
self,
|
||||
|
@ -51,30 +67,36 @@ class Flinger:
|
|||
equator_length: float,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize flinger with geo boundary box and zoom level.
|
||||
|
||||
:param geo_boundaries: minimum and maximum latitude and longitude
|
||||
:param zoom_level: zoom level in OpenStreetMap terminology
|
||||
:param equator_length: celestial body equator length in meters
|
||||
"""
|
||||
self.geo_boundaries: BoundaryBox = geo_boundaries
|
||||
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.min_())
|
||||
)
|
||||
self.pixels_per_meter: float = osm_zoom_level_to_pixels_per_meter(
|
||||
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:
|
||||
"""
|
||||
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 * (
|
||||
pseudo_mercator(coordinates)
|
||||
- pseudo_mercator(self.geo_boundaries.min_())
|
||||
result: np.ndarray = (
|
||||
self.ratio * pseudo_mercator(coordinates) - self.min_
|
||||
)
|
||||
|
||||
# Invert y axis on coordinate plane.
|
||||
|
@ -86,7 +108,8 @@ class Flinger:
|
|||
"""
|
||||
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:
|
||||
# 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))
|
||||
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)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"""
|
||||
Vector utility.
|
||||
"""
|
||||
"""Vector utility."""
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
|
@ -12,8 +12,9 @@ from shapely.geometry import LineString
|
|||
|
||||
def compute_angle(vector: np.ndarray) -> float:
|
||||
"""
|
||||
For the given vector compute an angle between it and (1, 0) vector. The
|
||||
result is in [0, 2π].
|
||||
For the given vector compute an angle between it and (1, 0) vector.
|
||||
|
||||
The result is in [0, 2π].
|
||||
"""
|
||||
if vector[0] == 0.0:
|
||||
if vector[1] > 0.0:
|
||||
|
@ -50,6 +51,7 @@ class Polyline:
|
|||
def get_path(self, parallel_offset: float = 0.0) -> str:
|
||||
"""Construct SVG path commands."""
|
||||
points: List[np.ndarray]
|
||||
|
||||
if np.allclose(parallel_offset, 0.0):
|
||||
points = self.points
|
||||
else:
|
||||
|
@ -135,14 +137,22 @@ class Segment:
|
|||
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}"
|
||||
|
||||
def __lt__(self, other: "Segment") -> bool:
|
||||
"""Compare central y coordinates of segments."""
|
||||
return self.y < other.y
|
||||
|
||||
def intersection(self, other: "Segment"):
|
||||
divisor = (self.point_1[0] - self.point_2[0]) * (
|
||||
def intersection(self, other: "Segment") -> Optional[List[float]]:
|
||||
"""
|
||||
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]
|
||||
) - (self.point_1[1] - self.point_2[1]) * (
|
||||
other.point_1[0] - other.point_2[0]
|
||||
|
@ -165,7 +175,6 @@ class Segment:
|
|||
) / divisor
|
||||
|
||||
if 0 <= t <= 1 and 0 <= u <= 1:
|
||||
print(t)
|
||||
return [
|
||||
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]),
|
||||
|
|
|
@ -24,10 +24,15 @@
|
|||
"mausoleum": {"name": "mausoleum", "categories": ["building"]},
|
||||
"minaret": {"name": "minaret", "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"},
|
||||
"roof": {"is_part": true, "name": "roof"},
|
||||
"roof_and_walls": {"is_part": true, "name": "roof and wall"},
|
||||
"wayside_shrine": {"name": "wayside shrine"}
|
||||
}
|
||||
},
|
||||
|
@ -207,6 +212,7 @@
|
|||
"shield_volcano": {"name": "shield volcano", "categories": ["natural"], "emoji": "🌋"},
|
||||
"smoke": {"name": "smoke"},
|
||||
"smoke_2": {"name": "clouds"},
|
||||
"spring": {"name": "spring", "categories": ["natural"]},
|
||||
"stone": {"emoji": "🪨", "categories": ["natural"], "name": "stone"},
|
||||
"stone_with_inscription": {"categories": ["natural"], "name": "stone with inscription"},
|
||||
"stratovolcano": {"name": "stratovolcano", "categories": ["natural"], "emoji": "🌋"},
|
||||
|
@ -300,6 +306,7 @@
|
|||
},
|
||||
"hand_items": {
|
||||
"bag": {"name": "bag"},
|
||||
"bag_with_percent": {"name": "bag with percent"},
|
||||
"book": {"emoji": "📕", "name": "book"},
|
||||
"books": {"emoji": "📚", "name": "books"}
|
||||
},
|
||||
|
@ -335,12 +342,11 @@
|
|||
"fire_pit": {"name": "fire pit"},
|
||||
"fishing_angle": {"name": "fishing angle"},
|
||||
"flagpole": {"emoji": "🏴", "name": "flagpole"},
|
||||
"food_court": {"name": "food court"},
|
||||
"foot": {"emoji": "👣", "name": "footprint"},
|
||||
"frame": {"name": "picture frame"},
|
||||
"fuel_station": {"emoji": "⛽️", "name": "fuel station"},
|
||||
"garages": {"name": "car under roof"},
|
||||
"gavel": {"name": "gavel"},
|
||||
"gazette": {"name": "gazette"},
|
||||
"gift": {"emoji": "🎁", "name": "gift box"},
|
||||
"globe": {"name": "globe"},
|
||||
"government": {"name": "building with dome and flag"},
|
||||
|
@ -358,7 +364,6 @@
|
|||
"lock_unlocked": {"name": "opened lock"},
|
||||
"lock_with_keyhole": {"emoji": "🔒", "name": "closed lock with keyhole"},
|
||||
"lowered_kerb": {"name": "lowered kerb"},
|
||||
"main_entrance": {"name": "main entrance"},
|
||||
"manhole_drain": {"name": "drain manhole cover"},
|
||||
"marketplace": {"name": "marketplace"},
|
||||
"medicine_bottle": {"name": "medicine bottle"},
|
||||
|
@ -377,9 +382,7 @@
|
|||
"pole": {"name": "pole"},
|
||||
"power_generator": {"name": "power generator"},
|
||||
"prison": {"name": "bars"},
|
||||
"restaurant": {"emoji": "🍴", "name": "fork and knife"},
|
||||
"roof": {"is_part": true, "name": "roof"},
|
||||
"roof_and_walls": {"is_part": true, "name": "roof and wall"},
|
||||
"fork_and_knife": {"emoji": "🍴", "name": "fork and knife"},
|
||||
"sheets": {"name": "two sheets"},
|
||||
"shelter": {"name": "shelter"},
|
||||
"shop_convenience": {"name": "convenience store"},
|
||||
|
@ -391,7 +394,7 @@
|
|||
"stained_glass": {"name": "stained glass"},
|
||||
"staircase": {"name": "door with stairs"},
|
||||
"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_pole": {"is_part": true, "name": "support pole"},
|
||||
"support_wall": {"is_part": true, "name": "support wall"},
|
||||
|
@ -412,21 +415,23 @@
|
|||
"urban_tree_pot": {"is_part": true, "name": "tree pot"},
|
||||
"vanity_mirror": {"name": "vanity mirror"},
|
||||
"ventilation": {"name": "ventilation shaft"},
|
||||
"waving_flag": {"name": "waving flag"},
|
||||
"waving_flag": {"name": "waving flag", "emoji": "🏳"},
|
||||
"wretch_and_hammer": {"name": "wretch and hammer"}
|
||||
},
|
||||
"body_part": {
|
||||
"foot": {"emoji": "👣", "name": "footprint"},
|
||||
"tooth": {"name": "tooth", "emoji": "🦷"}
|
||||
},
|
||||
"clothes": {
|
||||
"glasses": {"name": "glasses"},
|
||||
"hanger": {"name": "hanger"},
|
||||
"shoe": {"name": "shoe"},
|
||||
"t_shirt": {"name": "T-shirt"},
|
||||
"t_shirt": {"name": "T-shirt", "emoji": "👕"},
|
||||
"t_shirt_and_scissors": {"name": "T-shirt and scissors"},
|
||||
"watches": {"name": "hand watch"}
|
||||
"watches": {"name": "hand watch", "emoji": "⌚️"}
|
||||
},
|
||||
"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_pin": {"categories": ["sport"], "name": "golf pin"},
|
||||
"golf_tee": {"categories": ["sport"], "name": "golf tee"},
|
||||
|
@ -439,31 +444,38 @@
|
|||
},
|
||||
"recycling": {
|
||||
"recycling_container": {"name": "recycling container with wheel"},
|
||||
"waste_basket": {"name": "waste basket"},
|
||||
"waste_disposal": {"name": "recycling container"}
|
||||
"waste_basket": {"name": "waste basket", "emoji": "🗑"},
|
||||
"waste_disposal": {"name": "recycling container", "emoji": "♻️"}
|
||||
},
|
||||
"electronic_device": {
|
||||
"cctv": {"directed": "right", "name": "wall CCTV camera"},
|
||||
"cctv_dome_wall": {"directed": "right", "name": "wall 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"},
|
||||
"telephone": {"emoji": "☎️", "name": "telephone"},
|
||||
"tv": {"name": "monitor"}
|
||||
"tv": {"name": "monitor", "emoji": "🖥"}
|
||||
},
|
||||
"car_part": {
|
||||
"engine": {"name": "engine"},
|
||||
"tyre": {"name": "tyre"}
|
||||
},
|
||||
"human": {
|
||||
"massage": {"name": "massage"},
|
||||
"pole_dancer": {"name": "pole dancer"},
|
||||
"sauna": {"name": "sauna"},
|
||||
"sauna": {"name": "sauna", "emoji": "🧖"},
|
||||
"two_people_together": {"emoji": "🧑🤝🧑", "name": "two people together"},
|
||||
"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": {
|
||||
"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_3_platforms": {"name": "diving tower with 3 platforms"},
|
||||
"diving_4_platforms": {"name": "diving tower with 4 platforms"},
|
||||
|
|
Before Width: | Height: | Size: 1.9 MiB After Width: | Height: | Size: 2 MiB |
|
@ -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 logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from map_machine.ui.cli import parse_arguments
|
||||
|
@ -46,8 +55,8 @@ def main() -> None:
|
|||
|
||||
mapcss.generate_mapcss(arguments)
|
||||
|
||||
elif arguments.command == "element":
|
||||
from map_machine.element.single import draw_element
|
||||
elif arguments.command == "draw":
|
||||
from map_machine.element.element import draw_element
|
||||
|
||||
draw_element(arguments)
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
"""
|
||||
Map drawing configuration.
|
||||
"""
|
||||
"""Map drawing configuration."""
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
from typing import Optional, Any, Dict, Set, Tuple
|
||||
|
||||
from colour import Color
|
||||
|
||||
from map_machine.pictogram.icon import ShapeExtractor, IconSet
|
||||
from map_machine.scheme import Scheme
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
|
@ -44,6 +45,7 @@ class BuildingMode(Enum):
|
|||
class MapConfiguration:
|
||||
"""Map drawing configuration."""
|
||||
|
||||
scheme: Scheme
|
||||
drawing_mode: DrawingMode = DrawingMode.NORMAL
|
||||
building_mode: BuildingMode = BuildingMode.FLAT
|
||||
label_mode: LabelMode = LabelMode.MAIN
|
||||
|
@ -58,13 +60,15 @@ class MapConfiguration:
|
|||
use_building_colors: bool = False
|
||||
show_overlapped: bool = False
|
||||
credit: Optional[str] = "© OpenStreetMap contributors"
|
||||
show_credit: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_options(
|
||||
cls, options: argparse.Namespace, zoom_level: float
|
||||
cls, scheme: Scheme, options: argparse.Namespace, zoom_level: float
|
||||
) -> "MapConfiguration":
|
||||
"""Initialize from command-line options."""
|
||||
return cls(
|
||||
scheme,
|
||||
DrawingMode(options.mode),
|
||||
BuildingMode(options.buildings),
|
||||
LabelMode(options.label_mode),
|
||||
|
@ -89,3 +93,19 @@ class MapConfiguration:
|
|||
if self.drawing_mode not in (DrawingMode.NORMAL, DrawingMode.BLACK):
|
||||
return Color("#111111")
|
||||
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,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
MapCSS scheme creation.
|
||||
"""
|
||||
"""MapCSS scheme creation."""
|
||||
import argparse
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Simple OpenStreetMap renderer.
|
||||
"""
|
||||
"""Simple OpenStreetMap renderer."""
|
||||
import argparse
|
||||
import logging
|
||||
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.figure import StyledFigure
|
||||
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.map_configuration import LabelMode, MapConfiguration
|
||||
from map_machine.osm.osm_getter import NetworkError, get_osm
|
||||
|
@ -36,6 +34,7 @@ __author__ = "Sergey Vartanov"
|
|||
__email__ = "me@enzet.ru"
|
||||
|
||||
ROAD_PRIORITY: float = 40.0
|
||||
DEFAULT_SIZE = (800.0, 600.0)
|
||||
|
||||
|
||||
class Map:
|
||||
|
@ -45,12 +44,11 @@ class Map:
|
|||
self,
|
||||
flinger: Flinger,
|
||||
svg: svgwrite.Drawing,
|
||||
scheme: Scheme,
|
||||
configuration: MapConfiguration,
|
||||
) -> None:
|
||||
self.flinger: Flinger = flinger
|
||||
self.svg: svgwrite.Drawing = svg
|
||||
self.scheme: Scheme = scheme
|
||||
self.scheme: Scheme = configuration.scheme
|
||||
self.configuration = configuration
|
||||
|
||||
self.background_color: Color = self.scheme.get_color("background_color")
|
||||
|
@ -132,7 +130,8 @@ class Map:
|
|||
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:
|
||||
"""Draw buildings: shade, walls, and roof."""
|
||||
|
@ -264,20 +263,28 @@ def render_map(arguments: argparse.Namespace) -> None:
|
|||
|
||||
: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(
|
||||
arguments, float(arguments.zoom)
|
||||
scheme, arguments, float(arguments.zoom)
|
||||
)
|
||||
cache_path: Path = Path(arguments.cache)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Compute boundary box
|
||||
# Compute boundary box.
|
||||
|
||||
boundary_box: Optional[BoundaryBox] = None
|
||||
|
||||
if 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
|
||||
|
||||
for delimiter in ",", "/":
|
||||
|
@ -289,13 +296,20 @@ def render_map(arguments: argparse.Namespace) -> None:
|
|||
if coordinates is None or len(coordinates) != 2:
|
||||
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(
|
||||
coordinates, configuration.zoom_level, width, height
|
||||
)
|
||||
|
||||
# Determine files
|
||||
# Determine files.
|
||||
|
||||
input_file_names: Optional[list[Path]] = None
|
||||
if arguments.input_file_names:
|
||||
input_file_names = list(map(Path, arguments.input_file_names))
|
||||
elif boundary_box:
|
||||
|
@ -309,12 +323,9 @@ def render_map(arguments: argparse.Namespace) -> None:
|
|||
logging.fatal(error.message)
|
||||
sys.exit(1)
|
||||
else:
|
||||
fatal(
|
||||
"Specify either --input, or --boundary-box, or --coordinates and "
|
||||
"--size."
|
||||
)
|
||||
fatal("Specify either --input, or --boundary-box, or --coordinates.")
|
||||
|
||||
# Get OpenStreetMap data
|
||||
# Get OpenStreetMap data.
|
||||
|
||||
osm_data: OSMData = OSMData()
|
||||
for input_file_name in input_file_names:
|
||||
|
@ -332,9 +343,9 @@ def render_map(arguments: argparse.Namespace) -> None:
|
|||
if not 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
|
||||
)
|
||||
size: np.ndarray = flinger.size
|
||||
|
@ -344,19 +355,15 @@ def render_map(arguments: argparse.Namespace) -> None:
|
|||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
|
||||
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
|
||||
constructor: Constructor = Constructor(
|
||||
osm_data=osm_data,
|
||||
flinger=flinger,
|
||||
scheme=scheme,
|
||||
extractor=icon_extractor,
|
||||
configuration=configuration,
|
||||
)
|
||||
constructor.construct()
|
||||
|
||||
map_: Map = Map(
|
||||
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
|
||||
)
|
||||
map_: Map = Map(flinger=flinger, svg=svg, configuration=configuration)
|
||||
map_.draw(constructor)
|
||||
|
||||
logging.info(f"Writing output SVG to {arguments.output_file_name}...")
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
OpenStreetMap-specific things.
|
||||
"""
|
||||
"""OpenStreetMap-specific things."""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Getting OpenStreetMap data from the web.
|
||||
"""
|
||||
"""Getting OpenStreetMap data from the web."""
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Parse OSM XML file.
|
||||
"""
|
||||
"""Parse OSM XML file."""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
Icons and points.
|
||||
"""
|
||||
"""Icons and points."""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Extract icons from SVG file.
|
||||
"""
|
||||
"""Extract icons from SVG file."""
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
@ -34,6 +32,9 @@ PATH_MATCHER: re.Pattern = re.compile("[Mm] ([0-9.e-]*)[, ]([0-9.e-]*)")
|
|||
|
||||
GRID_STEP: int = 16
|
||||
|
||||
USED_ICON_COLOR: str = "#000000"
|
||||
UNUSED_ICON_COLORS: List[str] = ["#0000ff", "#ff0000"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Shape:
|
||||
|
@ -168,9 +169,10 @@ def verify_sketch_element(element: Element, id_: str) -> bool:
|
|||
"""
|
||||
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
|
||||
: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"]:
|
||||
return True
|
||||
|
@ -180,7 +182,7 @@ def verify_sketch_element(element: Element, id_: str) -> bool:
|
|||
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 (
|
||||
style["fill"] == "none"
|
||||
|
@ -190,20 +192,27 @@ def verify_sketch_element(element: Element, id_: str) -> bool:
|
|||
):
|
||||
return True
|
||||
|
||||
# Sketch fill element (black fill, no stroke, 20% opacity).
|
||||
# Sketch element (black 1 px stroke, no fill, 20% opacity).
|
||||
|
||||
if (
|
||||
style["fill"] == "none"
|
||||
and style["stroke"] == "#000000"
|
||||
and "opacity" in style
|
||||
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
|
||||
|
||||
# Experimental shape (blue fill, no stroke).
|
||||
# Experimental shape (blue or red fill, no stroke).
|
||||
|
||||
if (
|
||||
style["fill"] == "#0000ff"
|
||||
style["fill"] in UNUSED_ICON_COLORS
|
||||
and "stroke" in style
|
||||
and style["stroke"] == "none"
|
||||
):
|
||||
|
@ -302,7 +311,12 @@ class ShapeExtractor:
|
|||
id_: str = node.attrib["id"]
|
||||
if STANDARD_INKSCAPE_ID_MATCHER.match(id_) is not None:
|
||||
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
|
||||
|
||||
if "d" in node.attrib and node.attrib["d"]:
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Icon grid drawing.
|
||||
"""
|
||||
"""Icon grid drawing."""
|
||||
import logging
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Point: node representation on the map.
|
||||
"""
|
||||
"""Point: node representation on the map."""
|
||||
import logging
|
||||
from typing import Dict, List, Optional, Set
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Map Machine drawing scheme.
|
||||
"""
|
||||
"""Map Machine drawing scheme."""
|
||||
import logging
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
|
@ -13,7 +11,6 @@ import yaml
|
|||
from colour import Color
|
||||
|
||||
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.pictogram.icon import (
|
||||
DEFAULT_SHAPE_ID,
|
||||
|
@ -30,6 +27,8 @@ __email__ = "me@enzet.ru"
|
|||
|
||||
IconDescription = List[Union[str, Dict[str, str]]]
|
||||
|
||||
DEFAULT_COLOR: Color = Color("black")
|
||||
|
||||
|
||||
@dataclass
|
||||
class LineStyle:
|
||||
|
@ -136,22 +135,21 @@ class Matcher(Tagged):
|
|||
)
|
||||
|
||||
def is_matched(
|
||||
self, tags: Tags, configuration: Optional[MapConfiguration] = None
|
||||
self, tags: Tags, country: Optional[str] = None
|
||||
) -> Tuple[bool, Dict[str, str]]:
|
||||
"""
|
||||
Check whether element tags matches tag matcher.
|
||||
|
||||
: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] = {}
|
||||
|
||||
if (
|
||||
configuration is not None
|
||||
country is not None
|
||||
and self.location_restrictions
|
||||
and not match_location(
|
||||
self.location_restrictions, configuration.country
|
||||
)
|
||||
and not match_location(self.location_restrictions, country)
|
||||
):
|
||||
return False, {}
|
||||
|
||||
|
@ -365,15 +363,20 @@ class Scheme:
|
|||
self.cache: Dict[str, Tuple[IconSet, int]] = {}
|
||||
|
||||
@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
|
||||
specification
|
||||
"""
|
||||
with file_name.open(encoding="utf-8") as input_file:
|
||||
content: Dict[str, Any] = yaml.load(
|
||||
input_file.read(), Loader=yaml.FullLoader
|
||||
)
|
||||
try:
|
||||
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)
|
||||
|
||||
def get_color(self, color: str) -> Color:
|
||||
|
@ -401,7 +404,9 @@ class Scheme:
|
|||
return Color(color)
|
||||
except (ValueError, AttributeError):
|
||||
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:
|
||||
"""Get default color for a main icon."""
|
||||
|
@ -413,6 +418,8 @@ class Scheme:
|
|||
|
||||
def get(self, variable_name: str):
|
||||
"""
|
||||
Get value of variable.
|
||||
|
||||
FIXME: colors should be variables.
|
||||
"""
|
||||
if variable_name in self.colors:
|
||||
|
@ -476,7 +483,10 @@ class Scheme:
|
|||
extractor: ShapeExtractor,
|
||||
tags: Dict[str, Any],
|
||||
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]:
|
||||
"""
|
||||
Construct icon set.
|
||||
|
@ -484,7 +494,11 @@ class Scheme:
|
|||
:param extractor: extractor with icon specifications
|
||||
:param tags: OpenStreetMap element tags dictionary
|
||||
: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)
|
||||
"""
|
||||
tags_hash: str = (
|
||||
|
@ -501,12 +515,11 @@ class Scheme:
|
|||
for index, matcher in enumerate(self.node_matchers):
|
||||
if not matcher.replace_shapes and main_icon:
|
||||
continue
|
||||
matching, groups = matcher.is_matched(tags, configuration)
|
||||
matching, groups = matcher.is_matched(tags, country)
|
||||
if not matching:
|
||||
continue
|
||||
if (
|
||||
not configuration.ignore_level_matching
|
||||
and not matcher.check_zoom_level(configuration.zoom_level)
|
||||
if not ignore_level_matching and not matcher.check_zoom_level(
|
||||
zoom_level
|
||||
):
|
||||
return None, 0
|
||||
matcher_tags: Set[str] = set(matcher.tags.keys())
|
||||
|
@ -567,7 +580,7 @@ class Scheme:
|
|||
main_icon.recolor(color)
|
||||
|
||||
default_icon: Optional[Icon] = None
|
||||
if configuration.show_overlapped:
|
||||
if show_overlapped:
|
||||
small_dot_spec: ShapeSpecification = ShapeSpecification(
|
||||
extractor.get_shape(DEFAULT_SMALL_SHAPE_ID),
|
||||
color if color else self.get_color("default"),
|
||||
|
|
|
@ -59,8 +59,9 @@ colors:
|
|||
grass_border_color: "#BFD098"
|
||||
grass_color: "#CFE0A8"
|
||||
hidden_color: "#000000"
|
||||
indoor_border_color: "#C0B8B0"
|
||||
indoor_border_color: "#A0A890"
|
||||
indoor_color: "#E8E4E0"
|
||||
indoor_column_color: {color: indoor_border_color, darken: 0.5}
|
||||
meadow_border_color: "#BFD078"
|
||||
meadow_color: "#CFE088"
|
||||
orchard_color: "#B8DCA4"
|
||||
|
@ -435,6 +436,8 @@ node_icons:
|
|||
shapes: [fort]
|
||||
- tags: {shop: mall}
|
||||
shapes: [bag]
|
||||
- tags: {shop: department_store}
|
||||
shapes: [bag]
|
||||
- tags: {shop: mall, building: "yes"}
|
||||
shapes: [bag]
|
||||
- tags: {leisure: water_park}
|
||||
|
@ -546,6 +549,8 @@ node_icons:
|
|||
shapes: [star_of_david]
|
||||
- tags: {historic: tomb, tomb: mausoleum}
|
||||
shapes: [mausoleum]
|
||||
- tags: {historic: tomb, tomb: pyramid}
|
||||
shapes: [pyramid]
|
||||
- tags: {historic: "*"}
|
||||
shapes: [japan_historic]
|
||||
replace_shapes: no
|
||||
|
@ -556,6 +561,8 @@ node_icons:
|
|||
tags:
|
||||
- tags: {shop: supermarket}
|
||||
shapes: [supermarket_cart]
|
||||
- tags: {shop: variety_store}
|
||||
shapes: [bag_with_percent]
|
||||
- tags: {shop: general}
|
||||
shapes: [bag]
|
||||
- tags: {amenity: arts_centre}
|
||||
|
@ -670,6 +677,8 @@ node_icons:
|
|||
shapes: [knives]
|
||||
- tags: {shop: car}
|
||||
shapes: [{shape: car, color: sell_color}]
|
||||
- tags: {shop: car_parts}
|
||||
shapes: [shape: engine]
|
||||
- tags: {shop: chocolate}
|
||||
shapes: [cupcake]
|
||||
- tags: {shop: coffee}
|
||||
|
@ -717,7 +726,7 @@ node_icons:
|
|||
- tags: {shop: mobile_phone}
|
||||
shapes: [{shape: phone, color: sell_color}]
|
||||
- tags: {shop: newsagent}
|
||||
shapes: [sheets]
|
||||
shapes: [gazette]
|
||||
- tags: {shop: optician}
|
||||
shapes: [glasses]
|
||||
- tags: {shop: pastry}
|
||||
|
@ -771,9 +780,9 @@ node_icons:
|
|||
- tags: {amenity: nightclub}
|
||||
shapes: [cocktail_glass_with_straw]
|
||||
- tags: {amenity: restaurant}
|
||||
shapes: [restaurant]
|
||||
shapes: [fork_and_knife]
|
||||
- tags: {amenity: restaurant;bar}
|
||||
shapes: [restaurant]
|
||||
shapes: [fork_and_knife]
|
||||
add_shapes: [cocktail_glass]
|
||||
- tags: {shop: ice_cream}
|
||||
shapes: [ice_cream]
|
||||
|
@ -1140,6 +1149,10 @@ node_icons:
|
|||
shapes: [power_pole_triangle_armless]
|
||||
- tags: {power: pole, design: 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}
|
||||
shapes: [chimney]
|
||||
|
@ -1147,16 +1160,6 @@ node_icons:
|
|||
shapes: [tower_cooling]
|
||||
- tags: {man_made: tower, tower:type: 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}
|
||||
shapes: [pagoda]
|
||||
- tags: {man_made: tower, tower:type: observation}
|
||||
|
@ -1410,6 +1413,28 @@ node_icons:
|
|||
- {shape: wave_left, offset: [-4, -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"}
|
||||
add_shapes: [phone]
|
||||
|
||||
|
@ -1470,6 +1495,8 @@ node_icons:
|
|||
- group: "Important small objects"
|
||||
start_zoom_level: 17.0
|
||||
tags:
|
||||
- tags: {natural: spring}
|
||||
shapes: [{shape: spring, color: water_border_color}]
|
||||
- tags: {highway: elevator}
|
||||
shapes: [elevator]
|
||||
- tags: {historic: cannon}
|
||||
|
@ -1495,7 +1522,7 @@ node_icons:
|
|||
- tags: {historic: tomb}
|
||||
shapes: [tomb]
|
||||
- tags: {tomb: "*"}
|
||||
exception: {tomb: mausoleum}
|
||||
exception: {tomb: mausoleum} # TODO: add exception "tomb: pyramid"
|
||||
shapes: [tomb]
|
||||
- tags: {barrier: toll_booth}
|
||||
shapes: [toll_booth]
|
||||
|
@ -1653,6 +1680,8 @@ node_icons:
|
|||
shapes: [hopscotch]
|
||||
- tags: {playground: slide}
|
||||
shapes: [slide]
|
||||
- tags: {attraction: water_slide}
|
||||
shapes: [slide_and_water]
|
||||
- tags: {playground: roundabout}
|
||||
shapes: [roundabout]
|
||||
- tags: {playground: sandpit}
|
||||
|
@ -1669,6 +1698,8 @@ node_icons:
|
|||
shapes: [golf_pin]
|
||||
- tags: {highway: traffic_mirror}
|
||||
shapes: [side_mirror]
|
||||
- tags: {amenity: dressing_room}
|
||||
shapes: [hanger]
|
||||
|
||||
- group: "Entrances"
|
||||
start_zoom_level: 18.0
|
||||
|
@ -1961,7 +1992,7 @@ node_icons:
|
|||
- tags: {recycling:glass_bottles: "yes"}
|
||||
add_shapes: [bottle]
|
||||
- tags: {recycling:paper: "yes"}
|
||||
add_shapes: [sheets]
|
||||
add_shapes: [gazette]
|
||||
- tags: {recycling:glass: "yes"}
|
||||
add_shapes: [bottle_and_wine_glass]
|
||||
- tags: {recycling:clothes: "yes"}
|
||||
|
@ -1971,11 +2002,11 @@ node_icons:
|
|||
- tags: {recycling:green_waste: "yes"}
|
||||
add_shapes: [apple]
|
||||
- tags: {recycling:paper_packaging: "yes"}
|
||||
add_shapes: [sheets]
|
||||
add_shapes: [gazette]
|
||||
- tags: {recycling:newspaper: "yes"}
|
||||
add_shapes: [sheets]
|
||||
add_shapes: [gazette]
|
||||
- tags: {recycling:magazines: "yes"}
|
||||
add_shapes: [sheets]
|
||||
add_shapes: [gazette]
|
||||
- tags: {recycling:books: "yes"}
|
||||
add_shapes: [book]
|
||||
- tags: {recycling:wood: "yes"}
|
||||
|
@ -2095,7 +2126,7 @@ ways:
|
|||
priority: 10.0
|
||||
- tags: {indoor: corridor}
|
||||
style:
|
||||
stroke: indoor_color
|
||||
stroke: indoor_border_color
|
||||
stroke-width: 1.0
|
||||
fill: indoor_color
|
||||
priority: 11.0
|
||||
|
@ -2110,11 +2141,10 @@ ways:
|
|||
stroke-width: 1.0
|
||||
fill: indoor_color
|
||||
priority: 12.0
|
||||
- tags: {indoor: room, area: "yes"}
|
||||
- tags: {indoor: room}
|
||||
style:
|
||||
stroke: indoor_color
|
||||
stroke: indoor_border_color
|
||||
stroke-width: 1.0
|
||||
fill: indoor_color
|
||||
priority: 12.0
|
||||
- tags: {indoor: elevator, area: "yes"}
|
||||
style:
|
||||
|
@ -2124,9 +2154,9 @@ ways:
|
|||
priority: 12.0
|
||||
- tags: {indoor: column}
|
||||
style:
|
||||
stroke: indoor_color
|
||||
stroke: indoor_column_color
|
||||
stroke-width: 1.0
|
||||
fill: indoor_color
|
||||
fill: indoor_column_color
|
||||
priority: 13.0
|
||||
|
||||
- tags: {power: line}
|
||||
|
@ -2160,6 +2190,17 @@ ways:
|
|||
stroke-dasharray: "1.0,10.0,1.0,1.5"
|
||||
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}
|
||||
style:
|
||||
stroke-width: 1.5
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
Tiles generation for slippy maps.
|
||||
"""
|
||||
"""Tiles generation for slippy maps."""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Map Machine tile server for slippy maps.
|
||||
"""
|
||||
"""Map Machine tile server for slippy maps."""
|
||||
import argparse
|
||||
import logging
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
|
@ -77,7 +75,7 @@ def run_server(options: argparse.Namespace) -> None:
|
|||
handler = TileServerHandler
|
||||
handler.cache = Path(options.cache)
|
||||
handler.options = options
|
||||
server: HTTPServer = HTTPServer(("", options.port), handler)
|
||||
server = HTTPServer(("", options.port), handler)
|
||||
logging.info(f"Server started on port {options.port}.")
|
||||
server.serve_forever()
|
||||
finally:
|
||||
|
|
|
@ -17,7 +17,7 @@ from PIL import Image
|
|||
|
||||
from map_machine.constructor import Constructor
|
||||
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.mapper import Map
|
||||
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
|
||||
).get_coordinates()
|
||||
|
||||
flinger: Flinger = Flinger(
|
||||
flinger: MercatorFlinger = MercatorFlinger(
|
||||
BoundaryBox(left, bottom, right, top),
|
||||
self.zoom_level,
|
||||
osm_data.equator_length,
|
||||
|
@ -173,14 +173,13 @@ class Tile:
|
|||
icon_extractor: ShapeExtractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
|
||||
constructor: Constructor = Constructor(
|
||||
osm_data, flinger, scheme, icon_extractor, configuration
|
||||
osm_data, flinger, icon_extractor, configuration
|
||||
)
|
||||
constructor.construct()
|
||||
|
||||
painter: Map = Map(
|
||||
flinger=flinger, svg=svg, scheme=scheme, configuration=configuration
|
||||
flinger=flinger, svg=svg, configuration=configuration
|
||||
)
|
||||
painter.draw(constructor)
|
||||
|
||||
|
@ -382,7 +381,7 @@ class Tiles:
|
|||
self.zoom_level,
|
||||
).get_coordinates()
|
||||
|
||||
flinger: Flinger = Flinger(
|
||||
flinger: MercatorFlinger = MercatorFlinger(
|
||||
BoundaryBox(left, bottom, right, top),
|
||||
self.zoom_level,
|
||||
osm_data.equator_length,
|
||||
|
@ -390,16 +389,15 @@ class Tiles:
|
|||
extractor: ShapeExtractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
)
|
||||
scheme: Scheme = Scheme.from_file(workspace.DEFAULT_SCHEME_PATH)
|
||||
constructor: Constructor = Constructor(
|
||||
osm_data, flinger, scheme, extractor, configuration
|
||||
osm_data, flinger, extractor, configuration
|
||||
)
|
||||
constructor.construct()
|
||||
|
||||
svg: svgwrite.Drawing = svgwrite.Drawing(
|
||||
str(output_path), size=flinger.size
|
||||
)
|
||||
map_: Map = Map(flinger, svg, scheme, configuration)
|
||||
map_: Map = Map(flinger, svg, configuration)
|
||||
map_.draw(constructor)
|
||||
|
||||
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)
|
||||
min_zoom_level: int = min(zoom_levels)
|
||||
|
||||
scheme: Scheme = Scheme.from_file(
|
||||
workspace.find_scheme_path(options.scheme)
|
||||
)
|
||||
|
||||
if options.input_file_name:
|
||||
osm_data: OSMData = OSMData()
|
||||
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:
|
||||
configuration: MapConfiguration = MapConfiguration.from_options(
|
||||
options, zoom_level
|
||||
)
|
||||
tiles: Tiles = Tiles.from_boundary_box(
|
||||
osm_data.view_box, zoom_level
|
||||
scheme, options, zoom_level
|
||||
)
|
||||
tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level)
|
||||
tiles.draw(directory, Path(options.cache), configuration, osm_data)
|
||||
|
||||
elif options.coordinates:
|
||||
coordinates: List[float] = list(
|
||||
map(float, options.coordinates.strip().split(","))
|
||||
|
@ -501,24 +512,28 @@ def generate_tiles(options: argparse.Namespace) -> None:
|
|||
)
|
||||
try:
|
||||
configuration: MapConfiguration = MapConfiguration.from_options(
|
||||
options, zoom_level
|
||||
scheme, options, zoom_level
|
||||
)
|
||||
tile.draw_with_osm_data(osm_data, directory, configuration)
|
||||
except NetworkError as error:
|
||||
logging.fatal(error.message)
|
||||
|
||||
elif options.tile:
|
||||
zoom_level, x, y = map(int, options.tile.split("/"))
|
||||
tile: Tile = Tile(x, y, zoom_level)
|
||||
configuration: MapConfiguration = MapConfiguration.from_options(
|
||||
options, zoom_level
|
||||
scheme, options, zoom_level
|
||||
)
|
||||
tile.draw(directory, Path(options.cache), configuration)
|
||||
|
||||
elif options.boundary_box:
|
||||
boundary_box: Optional[BoundaryBox] = BoundaryBox.from_text(
|
||||
options.boundary_box
|
||||
)
|
||||
if boundary_box is None:
|
||||
logging.fatal("Failed to parse boundary box.")
|
||||
sys.exit(1)
|
||||
|
||||
min_tiles: Tiles = Tiles.from_boundary_box(boundary_box, min_zoom_level)
|
||||
try:
|
||||
osm_data: OSMData = min_tiles.load_osm_data(Path(options.cache))
|
||||
|
@ -531,9 +546,10 @@ def generate_tiles(options: argparse.Namespace) -> None:
|
|||
else:
|
||||
tiles: Tiles = Tiles.from_boundary_box(boundary_box, zoom_level)
|
||||
configuration: MapConfiguration = MapConfiguration.from_options(
|
||||
options, zoom_level
|
||||
scheme, options, zoom_level
|
||||
)
|
||||
tiles.draw(directory, Path(options.cache), configuration, osm_data)
|
||||
|
||||
else:
|
||||
logging.fatal(
|
||||
"Specify either --coordinates, --boundary-box, --tile, or --input."
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
OSM address tag processing.
|
||||
"""
|
||||
"""OSM address tag processing."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
"""
|
||||
User interface.
|
||||
"""
|
||||
"""User interface."""
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Command-line user interface.
|
||||
"""
|
||||
"""Command-line user interface."""
|
||||
import argparse
|
||||
from typing import Dict, List
|
||||
|
||||
|
@ -21,7 +19,7 @@ COMMAND_LINES: Dict[str, List[str]] = {
|
|||
],
|
||||
"icons": ["icons"],
|
||||
"mapcss": ["mapcss"],
|
||||
"element": ["element", "--node", "amenity=bench,material=wood"],
|
||||
"draw": ["draw", "node", "amenity=bench,material=wood"],
|
||||
"tile": ["tile", "--coordinates", "50.000,40.000"],
|
||||
}
|
||||
COMMANDS: List[str] = [
|
||||
|
@ -88,9 +86,9 @@ def parse_arguments(args: List[str]) -> argparse.Namespace:
|
|||
help="run tile server",
|
||||
)
|
||||
)
|
||||
add_element_arguments(
|
||||
add_draw_arguments(
|
||||
subparser.add_parser(
|
||||
"element",
|
||||
"draw",
|
||||
description="Draw map element separately.",
|
||||
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:
|
||||
"""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(
|
||||
"--buildings",
|
||||
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."""
|
||||
parser.add_argument("-n", "--node")
|
||||
parser.add_argument("-w", "--way")
|
||||
parser.add_argument("-r", "--relation")
|
||||
parser.add_argument("type")
|
||||
parser.add_argument("tags")
|
||||
parser.add_argument("-o", "--output-file", default="out/element.svg")
|
||||
|
||||
|
||||
def add_render_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
|
|
|
@ -70,7 +70,7 @@ def completion_commands() -> str:
|
|||
cli.add_tile_arguments(parser)
|
||||
cli.add_map_arguments(parser)
|
||||
elif command == "element":
|
||||
cli.add_element_arguments(parser)
|
||||
cli.add_draw_arguments(parser)
|
||||
elif command == "mapcss":
|
||||
cli.add_mapcss_arguments(parser)
|
||||
else:
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Utility file.
|
||||
"""
|
||||
"""Utility file."""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
"""
|
||||
File and directory path in the project.
|
||||
"""
|
||||
"""File and directory path in the project."""
|
||||
from pathlib import Path
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
from typing import Optional
|
||||
|
||||
HERE: Path = Path(__file__).parent
|
||||
|
||||
|
||||
|
@ -42,6 +42,33 @@ class Workspace:
|
|||
self._mapcss_path: Path = output_path / "map_machine_mapcss"
|
||||
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:
|
||||
"""Directory for the icon files named by identifiers."""
|
||||
return check_and_create(self._icons_by_id_path)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test boundary box.
|
||||
"""
|
||||
"""Test boundary box."""
|
||||
from map_machine.geometry.boundary_box import BoundaryBox
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test color functions.
|
||||
"""
|
||||
"""Test color functions."""
|
||||
from colour import Color
|
||||
|
||||
from map_machine.color import get_gradient_color, is_bright
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test command line commands.
|
||||
"""
|
||||
"""Test command line commands."""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from subprocess import PIPE, Popen
|
||||
|
@ -46,8 +44,8 @@ def test_wrong_render_arguments() -> None:
|
|||
"""Test `render` command with wrong arguments."""
|
||||
error_run(
|
||||
["render", "-z", "17"],
|
||||
b"CRITICAL Specify either --input, or --boundary-box, or --coordinates "
|
||||
b"and --size.\n",
|
||||
b"CRITICAL Specify either --input, or --boundary-box, or "
|
||||
b"--coordinates.\n",
|
||||
)
|
||||
|
||||
|
||||
|
@ -115,20 +113,21 @@ def test_mapcss() -> None:
|
|||
assert (out_path / "icons" / "LICENSE").is_file()
|
||||
|
||||
|
||||
def test_element() -> None:
|
||||
"""Test `element` command."""
|
||||
def test_draw() -> None:
|
||||
"""Test `draw` command."""
|
||||
run(
|
||||
COMMAND_LINES["element"],
|
||||
b"INFO Element is written to out/element.svg.\n",
|
||||
COMMAND_LINES["draw"],
|
||||
LOG + b"INFO Map is drawn to out/element.svg.\n",
|
||||
)
|
||||
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(
|
||||
["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)
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
"""
|
||||
Test Fish shell completion.
|
||||
"""
|
||||
"""Test Fish shell completion."""
|
||||
from map_machine.ui.completion import completion_commands
|
||||
|
||||
|
||||
def test_completion() -> None:
|
||||
"""Test Fish shell completion generation."""
|
||||
commands: str = completion_commands()
|
||||
assert commands.startswith("set -l")
|
||||
assert completion_commands().startswith("set -l")
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test direction processing.
|
||||
"""
|
||||
"""Test direction processing."""
|
||||
import numpy as np
|
||||
|
||||
from map_machine.feature.direction import DirectionSet, parse_vector, Sector
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test coordinates computation.
|
||||
"""
|
||||
"""Test coordinates computation."""
|
||||
import numpy as np
|
||||
|
||||
from map_machine.geometry.flinger import (
|
||||
|
|
|
@ -11,6 +11,7 @@ from typing import Optional
|
|||
|
||||
from colour import Color
|
||||
|
||||
from map_machine.map_configuration import MapConfiguration
|
||||
from map_machine.osm.osm_reader import Tags
|
||||
from map_machine.pictogram.icon import IconSet, ShapeSpecification, Icon
|
||||
from map_machine.pictogram.icon_collection import IconCollection
|
||||
|
@ -20,6 +21,7 @@ __author__ = "Sergey Vartanov"
|
|||
__email__ = "me@enzet.ru"
|
||||
|
||||
|
||||
CONFIGURATION: MapConfiguration = MapConfiguration(SCHEME)
|
||||
COLLECTION: IconCollection = IconCollection.from_scheme(SCHEME, SHAPE_EXTRACTOR)
|
||||
DEFAULT_COLOR: Color = SCHEME.get_default_color()
|
||||
EXTRA_COLOR: Color = SCHEME.get_extra_color()
|
||||
|
@ -50,14 +52,16 @@ def test_icons_by_name() -> None:
|
|||
def get_icon(tags: Tags) -> IconSet:
|
||||
"""Construct icon from tags."""
|
||||
processed: Set[str] = set()
|
||||
icon, _ = SCHEME.get_icon(SHAPE_EXTRACTOR, tags, processed)
|
||||
icon, _ = CONFIGURATION.get_icon(SHAPE_EXTRACTOR, tags, processed)
|
||||
return icon
|
||||
|
||||
|
||||
def test_no_icons() -> None:
|
||||
"""
|
||||
Tags that has no description in scheme and should be visualized with default
|
||||
shape.
|
||||
Test icon creation for tags not described in the scheme.
|
||||
|
||||
Tags that has no description in the scheme and should be visualized with
|
||||
default shape.
|
||||
"""
|
||||
icon: IconSet = get_icon({"aaa": "bbb"})
|
||||
assert icon.main_icon.is_default()
|
||||
|
@ -66,6 +70,8 @@ def test_no_icons() -> 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
|
||||
visualized with default shape with the given color.
|
||||
"""
|
||||
|
@ -77,11 +83,14 @@ def test_no_icons_but_color() -> None:
|
|||
def check_icon_set(
|
||||
tags: Tags,
|
||||
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:
|
||||
"""Check icon set using simple specification."""
|
||||
icon: IconSet = get_icon(tags)
|
||||
|
||||
if extra_specifications is None:
|
||||
extra_specifications = []
|
||||
|
||||
if not main_specification:
|
||||
assert icon.main_icon.is_default()
|
||||
else:
|
||||
|
@ -110,7 +119,7 @@ def test_icon() -> None:
|
|||
Tags that should be visualized with single main icon and without extra
|
||||
icons.
|
||||
"""
|
||||
check_icon_set({"natural": "tree"}, [("tree", Color("#98AC64"))], [])
|
||||
check_icon_set({"natural": "tree"}, [("tree", Color("#98AC64"))])
|
||||
|
||||
|
||||
def test_icon_1_extra() -> None:
|
||||
|
@ -165,7 +174,6 @@ def test_icon_regex() -> None:
|
|||
check_icon_set(
|
||||
{"traffic_sign": "maxspeed", "maxspeed": "42"},
|
||||
[("circle_11", DEFAULT_COLOR), ("digit_4", WHITE), ("digit_2", WHITE)],
|
||||
[],
|
||||
)
|
||||
|
||||
|
||||
|
@ -178,15 +186,30 @@ def test_vending_machine() -> None:
|
|||
check_icon_set(
|
||||
{"amenity": "vending_machine"},
|
||||
[("vending_machine", DEFAULT_COLOR)],
|
||||
[],
|
||||
)
|
||||
check_icon_set(
|
||||
{"amenity": "vending_machine", "vending": "drinks"},
|
||||
[("vending_bottle", DEFAULT_COLOR)],
|
||||
[],
|
||||
)
|
||||
check_icon_set(
|
||||
{"vending": "drinks"},
|
||||
[("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)],
|
||||
)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test label generation for nodes.
|
||||
"""
|
||||
"""Test label generation for nodes."""
|
||||
from typing import Dict, List, Set
|
||||
|
||||
from map_machine.map_configuration import LabelMode
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test MapCSS generation.
|
||||
"""
|
||||
"""Test MapCSS generation."""
|
||||
from map_machine.mapcss import MapCSSWriter
|
||||
from map_machine.scheme import NodeMatcher
|
||||
from tests import SCHEME
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test OSM XML parsing.
|
||||
"""
|
||||
"""Test OSM XML parsing."""
|
||||
import numpy as np
|
||||
|
||||
from map_machine.osm.osm_reader import (
|
||||
|
|
|
@ -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 pathlib import Path
|
||||
|
||||
|
||||
def test_requirements() -> None:
|
||||
"""Test whether `requirements.txt` has the same packages as `setup.py`."""
|
||||
requirements: List[str]
|
||||
with Path("requirements.txt").open(encoding="utf-8") as requirements_file:
|
||||
requirements = list(
|
||||
map(lambda x: x[:-1], requirements_file.readlines())
|
||||
)
|
||||
|
||||
assert requirements == REQUIREMENTS
|
||||
with Path("requirements.txt").open() as requirements_file:
|
||||
assert [x[:-1] for x in requirements_file.readlines()] == REQUIREMENTS
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
"""
|
||||
Test scheme parsing.
|
||||
"""
|
||||
"""Test scheme parsing."""
|
||||
from typing import Any
|
||||
|
||||
from map_machine.scheme import Scheme
|
||||
|
||||
|
||||
def test_verification() -> None:
|
||||
def test_verification_right() -> None:
|
||||
"""Test verification process of tags in scheme."""
|
||||
|
||||
tags: dict[str, Any] = {
|
||||
|
@ -15,7 +13,9 @@ def test_verification() -> None:
|
|||
}
|
||||
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] = {
|
||||
"colors": {"default": "#444444"},
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test style constructing for ways and areas.
|
||||
"""
|
||||
"""Test style constructing for ways and areas."""
|
||||
from tests import SCHEME
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Tests for length tag parsing.
|
||||
"""
|
||||
"""Tests for length tag parsing."""
|
||||
from typing import Optional
|
||||
|
||||
from map_machine.osm.osm_reader import Tagged
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test Taginfo project generation.
|
||||
"""
|
||||
"""Test Taginfo project generation."""
|
||||
from pathlib import Path
|
||||
|
||||
from map_machine.doc.taginfo import TaginfoProjectFile
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test text generation.
|
||||
"""
|
||||
"""Test text generation."""
|
||||
from map_machine.text import format_voltage
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test vector operations.
|
||||
"""
|
||||
"""Test vector operations."""
|
||||
import numpy as np
|
||||
|
||||
from map_machine.geometry.vector import compute_angle, turn_by_angle
|
||||
|
|
|
@ -9,22 +9,24 @@ import numpy as np
|
|||
from map_machine.constructor import Constructor
|
||||
from map_machine.figure import Figure
|
||||
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.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags
|
||||
from tests import SCHEME, SHAPE_EXTRACTOR
|
||||
|
||||
CONFIGURATION: MapConfiguration = MapConfiguration(SCHEME)
|
||||
|
||||
|
||||
def get_constructor(osm_data: OSMData) -> Constructor:
|
||||
"""
|
||||
Get custom constructor for bounds (-0.01, -0.01, 0.01, 0.01) and zoom level
|
||||
18.
|
||||
"""
|
||||
flinger: Flinger = Flinger(
|
||||
flinger: MercatorFlinger = MercatorFlinger(
|
||||
BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length
|
||||
)
|
||||
constructor: Constructor = Constructor(
|
||||
osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, MapConfiguration()
|
||||
osm_data, flinger, SHAPE_EXTRACTOR, CONFIGURATION
|
||||
)
|
||||
constructor.construct_ways()
|
||||
return constructor
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Test zoom level specification parsing.
|
||||
"""
|
||||
"""Test zoom level specification parsing."""
|
||||
from map_machine.slippy.tile import (
|
||||
ScaleConfigurationException,
|
||||
parse_zoom_level,
|
||||
|
|