Merge main.

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

1
.gitignore vendored
View file

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

54
CHANGELOG.md Normal file
View file

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

View file

@ -91,6 +91,8 @@
"tags": {},
"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",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 424 KiB

After

Width:  |  Height:  |  Size: 439 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -1,6 +1,4 @@
"""
Map Machine 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",

View file

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

View file

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

View file

@ -1,6 +1,4 @@
"""
Construct Map Machine nodes and ways.
"""
"""Construct Map Machine nodes and ways."""
import logging
import 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:

View file

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

View file

@ -1,6 +1,4 @@
"""
Special icon collections for documentation.
"""
"""Special icon collections for documentation."""
import json
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")

View file

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

View file

@ -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 keyvalue pair of tag."""
"""Add special OSM tag key or keyvalue 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])}>"

View file

@ -1,6 +1,4 @@
"""
Actions to perform before commit: generate PNG images for documentation.
"""
"""Actions to perform before commit: generate PNG images for documentation."""
import logging
import 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,

View file

@ -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 = (

View file

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

View file

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

View file

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

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

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

View file

@ -1,6 +1,4 @@
"""
Drawing separate map elements.
"""
"""Drawing separate map elements."""
import argparse
import 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
View file

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

View file

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

View file

@ -1,7 +1,6 @@
"""
Buildings on the map.
"""
"""Buildings on the map."""
import numpy as np
import 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",
)

View file

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

View file

@ -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(";"))

View file

@ -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

View file

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

View file

@ -1,6 +1,4 @@
"""
Figures displayed on the map.
"""
"""Figures displayed on the map."""
from typing import Dict, List
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:
"""

View file

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

View file

@ -1,6 +1,4 @@
"""
Rectangle that limit space on the map.
"""
"""Rectangle that limit space on the map."""
import logging
import 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.
"""

View file

@ -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)

View file

@ -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]),

View file

@ -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"},

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2 MiB

View file

@ -1,9 +1,18 @@
"""
Map Machine entry point.
"""
"""Map Machine entry point."""
import sys
if sys.version_info.major < 3 or sys.version_info.minor < 8:
print(
"FATAL Python "
+ str(sys.version_info.major)
+ "."
+ str(sys.version_info.minor)
+ " is not supported. Please, use at least Python 3.8."
)
sys.exit(1)
import argparse
import 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)

View file

@ -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,
)

View file

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

View file

@ -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}...")

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,4 @@
"""
Extract icons from SVG file.
"""
"""Extract icons from SVG file."""
import json
import 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"]:

View file

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

View file

@ -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

View file

@ -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"),

View file

@ -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

View file

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

View file

@ -1,6 +1,4 @@
"""
Map Machine tile server for slippy maps.
"""
"""Map Machine tile server for slippy maps."""
import argparse
import 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:

View file

@ -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."

View file

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

View file

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

View file

@ -1,6 +1,4 @@
"""
Command-line user interface.
"""
"""Command-line user interface."""
import argparse
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:

View file

@ -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:

View file

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

View file

@ -1,11 +1,11 @@
"""
File and directory path in the project.
"""
"""File and directory path in the project."""
from pathlib import Path
__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)

View file

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

View file

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

View file

@ -1,6 +1,4 @@
"""
Test command line commands.
"""
"""Test command line commands."""
import argparse
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)

View file

@ -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")

View file

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

View file

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

View file

@ -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)],
)

View file

@ -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

View file

@ -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

View file

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

View file

@ -1,18 +1,7 @@
"""
Check whether `requirements.txt` contains all requirements from `setup.py`.
"""
from pathlib import Path
from typing import List
from map_machine import REQUIREMENTS
from 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

View file

@ -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"},

View file

@ -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"

View file

@ -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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

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