Merge main.

This commit is contained in:
Sergey Vartanov 2022-05-15 20:13:58 +03:00
commit 518e8f9590
14 changed files with 192 additions and 102 deletions

View file

@ -3,6 +3,50 @@ Contributing
Thank you for your interest in the Map Machine project. Since the primary goal of the project is to cover as many tags as possible, the project is crucially depend on contributions as OpenStreetMap itself. Thank you for your interest in the Map Machine project. Since the primary goal of the project is to cover as many tags as possible, the project is crucially depend on contributions as OpenStreetMap itself.
Modify the code
---------------
**IMPORTANT** ❗ Before committing please enable Git hooks:
```shell
git config --local core.hooksPath data/githooks
```
This will allow you to automatically check your commit message and code before committing and pushing changes. This will crucially speed up pull request merging and make Git history neat and uniform.
### First configure your workspace ###
Make sure you have Python 3.9 development tools. E.g., for Ubuntu, run `apt install python3.9-dev python3.9-venv`.
Activate virtual environment. E.g. for fish shell, run `source venv/bin/activate.fish`.
Install the project in editable mode:
```shell
pip install -e .
```
Install formatter, linter and test system: `pip install black flake8 mypy pytest pytest-cov`.
If you are using PyCharm, you may want to set up user dictionary as well:
* `cp data/dictionary.xml .idea/dictionaries/<user name>.xml`
* in `.idea/dictionaries/<user name>.xml` change `%USERNAME%` to your username,
* restart PyCharm if it is launched.
### Code style ###
We use [Black](http://github.com/psf/black) code formatter with maximum 80 characters line length for all Python files within the project. Reformat a file is as simple as `black -l 80 <file name>`. Reformat everything with `black -l 80 map_machine tests`.
If you create new Python file, make sure you add `__author__ = "<first name> <second name>"` and `__email__ = "<author e-mail>"` string variables.
### Commit message format ###
The project uses commit messages that starts with a verb in infinitive form with first letter in uppercase, ends with a dot, and is not longer than 50 characters. E.g. `Add new icon.` or `Fix labels.`
If some issues or pull requests are referenced, commit message should starts with prefix such as `PR #123: `, `Issue #42: `, or `Fix #13: ` with the next letter in lowercase. E.g. `PR #123: refactor elements.` or `Issue #42: add icon for natural=tree.`
Suggest a tag to support Suggest a tag to support
------------------------ ------------------------
@ -18,39 +62,3 @@ Fix a typo in documentation
This action is not that easy as it supposed to be. We use [Moire](http://github.com/enzet/Moire) markup and converter to automatically generate documentation for GitHub, website, and [OpenStreetMap wiki](http://wiki.openstreetmap.org/). That's why editing Markdown files is not allowed. To fix a typo, open corresponding Moire file in `doc` directory (e.g. `doc/readme.moi` for `README.md`), modify it, and run `python map_machine/moire_manager.py`. This action is not that easy as it supposed to be. We use [Moire](http://github.com/enzet/Moire) markup and converter to automatically generate documentation for GitHub, website, and [OpenStreetMap wiki](http://wiki.openstreetmap.org/). That's why editing Markdown files is not allowed. To fix a typo, open corresponding Moire file in `doc` directory (e.g. `doc/readme.moi` for `README.md`), modify it, and run `python map_machine/moire_manager.py`.
Modify the code
---------------
### First configure your workspace ###
Make sure you have Python 3.9 development tools. E.g., for Ubuntu, run `apt install python3.9-dev python3.9-venv`.
Activate virtual environment. E.g. for fish shell, run `source venv/bin/activate.fish`.
Install the project in editable mode:
```shell
pip install -e .
```
Install formatter, linter and test system: `pip install black flake8 mypy pytest pytest-cov`.
Be sure to enable Git hooks:
```shell
git config --local core.hooksPath data/githooks
```
If you are using PyCharm, you may want to set up user dictionary as well:
* `cp data/dictionary.xml .idea/dictionaries/<user name>.xml`
* in `.idea/dictionaries/<user name>.xml` change `%USERNAME%` to your username,
* restart PyCharm if it is launched.
### Code style ###
We use [Black](http://github.com/psf/black) code formatter with maximum 80 characters line length for all Python files within the project. Reformat a file is as simple as `black -l 80 <file name>`. Reformat everything with `black -l 80 map_machine tests`.
If you create new Python file, make sure you add `__author__ = "<first name> <second name>"` and `__email__ = "<author e-mail>"` string variables.

View file

@ -2,25 +2,15 @@
Thank you for your interest in the Map Machine project. Since the primary goal of the project is to cover as many tags as possible, the project is crucially depend on contributions as OpenStreetMap itself. Thank you for your interest in the Map Machine project. Since the primary goal of the project is to cover as many tags as possible, the project is crucially depend on contributions as OpenStreetMap itself.
\2 {Suggest a tag to support} {}
Please, create an issue describing how you would like the feature to be visualized.
/*
\2 {Add an icon} {}
*/
\2 {Report a bug} {}
Please, create an issue describing the current behavior, expected behavior, and environment (most importantly, the OS version and Python version if it was not the recommended one).
\2 {Fix a typo in documentation} {}
This action is not that easy as it supposed to be. We use \ref {http://github.com/enzet/Moire} {Moire} markup and converter to automatically generate documentation for GitHub, website, and \ref {http://wiki.openstreetmap.org/} {OpenStreetMap wiki}. That's why editing Markdown files is not allowed. To fix a typo, open corresponding Moire file in \m {doc} directory (e.g. \m {doc/readme.moi} for \m {README.md}), modify it, and run \m {python map_machine/moire_manager.py}.
\2 {Modify the code} {} \2 {Modify the code} {}
\3 {First configure your workspace} ❗ \b {IMPORTANT} ❗ Before committing please enable Git hooks:
\code {git config --local core.hooksPath data/githooks} {shell}
This will allow you to automatically check your commit message and code before committing and pushing changes. This will crucially speed up pull request merging and make Git history neat and uniform.
\3 {First configure your workspace} {}
Make sure you have Python 3.9 development tools. E.g., for Ubuntu, run \m {apt install python3.9-dev python3.9-venv}. Make sure you have Python 3.9 development tools. E.g., for Ubuntu, run \m {apt install python3.9-dev python3.9-venv}.
@ -32,10 +22,6 @@ Install the project in editable mode:
Install formatter, linter and test system\: \m {pip install black flake8 mypy pytest pytest-cov}. Install formatter, linter and test system\: \m {pip install black flake8 mypy pytest pytest-cov}.
Be sure to enable Git hooks:
\code {git config --local core.hooksPath data/githooks} {shell}
If you are using PyCharm, you may want to set up user dictionary as well: If you are using PyCharm, you may want to set up user dictionary as well:
\list \list
@ -48,3 +34,21 @@ If you are using PyCharm, you may want to set up user dictionary as well:
We use \ref {http://github.com/psf/black} {Black} code formatter with maximum 80 characters line length for all Python files within the project. Reformat a file is as simple as \m {black -l 80 \formal {file name}}. Reformat everything with \m {black -l 80 map_machine tests}. We use \ref {http://github.com/psf/black} {Black} code formatter with maximum 80 characters line length for all Python files within the project. Reformat a file is as simple as \m {black -l 80 \formal {file name}}. Reformat everything with \m {black -l 80 map_machine tests}.
If you create new Python file, make sure you add \m {__author__ = "\formal {first name} \formal {second name}"} and \m {__email__ = "\formal {author e-mail}"} string variables. If you create new Python file, make sure you add \m {__author__ = "\formal {first name} \formal {second name}"} and \m {__email__ = "\formal {author e-mail}"} string variables.
\3 {Commit message format} {}
The project uses commit messages that starts with a verb in infinitive form with first letter in uppercase, ends with a dot, and is not longer than 50 characters. E.g. \m {Add new icon.} or \m {Fix labels.}
If some issues or pull requests are referenced, commit message should starts with prefix such as \m {PR #123: }, \m {Issue #42: }, or \m {Fix #13: } with the next letter in lowercase. E.g. \m {PR #123: refactor elements.} or \m {Issue #42: add icon for natural=tree.}
\2 {Suggest a tag to support} {}
Please, create an issue describing how you would like the feature to be visualized.
\2 {Report a bug} {}
Please, create an issue describing the current behavior, expected behavior, and environment (most importantly, the OS version and Python version if it was not the recommended one).
\2 {Fix a typo in documentation} {}
This action is not that easy as it supposed to be. We use \ref {http://github.com/enzet/Moire} {Moire} markup and converter to automatically generate documentation for GitHub, website, and \ref {http://wiki.openstreetmap.org/} {OpenStreetMap wiki}. That's why editing Markdown files is not allowed. To fix a typo, open corresponding Moire file in \m {doc} directory (e.g. \m {doc/readme.moi} for \m {README.md}), modify it, and run \m {python map_machine/moire_manager.py}.

View file

@ -351,14 +351,14 @@ class Constructor:
) )
self.figures.append(figure) self.figures.append(figure)
processed: set[str] = set() processed: Set[str] = set()
priority: int priority: int
icon_set: IconSet icon_set: IconSet
icon_set, priority = self.scheme.get_icon( icon_set, priority = self.scheme.get_icon(
self.extractor, line.tags, processed, self.configuration self.extractor, line.tags, processed, self.configuration
) )
if icon_set is not None: if icon_set is not None:
labels: list[Label] = self.text_constructor.construct_text( labels: List[Label] = self.text_constructor.construct_text(
line.tags, processed, self.configuration.label_mode line.tags, processed, self.configuration.label_mode
) )
point: Point = Point( point: Point = Point(
@ -529,6 +529,10 @@ class Constructor:
) )
self.points.append(point) self.points.append(point)
def get_sorted_figures(self) -> List[StyledFigure]:
"""Get all figures sorted by priority."""
return sorted(self.figures, key=lambda x: x.line_style.priority)
def check_level_number(tags: Tags, level: float) -> bool: def check_level_number(tags: Tags, level: float) -> bool:
"""Check if element described by tags is no the specified level.""" """Check if element described by tags is no the specified level."""

View file

@ -303,5 +303,5 @@ def convert(input_path: Path, output_path: Path) -> None:
if __name__ == "__main__": if __name__ == "__main__":
convert(Path("doc/readme.moi"), Path("README.md")) for id_ in "readme", "contributing":
convert(Path("doc/contributing.moi"), Path(".github/CONTRIBUTING.md")) convert(Path("doc") / f"{id_}.moi", Path(f"{id_.upper()}.md"))

View file

View file

@ -1,8 +1,7 @@
""" """
Figures displayed on the map. Figures displayed on the map.
""" """
from typing import Any, Dict, Iterator, List, Optional from typing import Dict, List
from svgwrite import Drawing
import numpy as np import numpy as np
@ -56,25 +55,6 @@ class Figure(Tagged):
return path return path
if levels:
self.min_height = float(levels) * BUILDING_HEIGHT_SCALE
height: Optional[float] = self.get_length("height")
if height:
self.height = height
height: Optional[float] = self.get_length("min_height")
if height:
self.min_height = height
def draw(self, svg: Drawing, flinger: Flinger) -> None:
"""Draw simple building shape."""
path: Path = Path(d=self.get_path(flinger))
path.update(self.line_style.style)
path.update({"stroke-linejoin": "round"})
svg.add(path)
class StyledFigure(Figure): class StyledFigure(Figure):
"""Figure with stroke and fill style.""" """Figure with stroke and fill style."""

View file

@ -47,7 +47,7 @@ def main() -> None:
mapcss.generate_mapcss(arguments) mapcss.generate_mapcss(arguments)
elif arguments.command == "element": elif arguments.command == "element":
from map_machine.element import draw_element from map_machine.element.element import draw_element
draw_element(arguments) draw_element(arguments)

View file

@ -18,7 +18,6 @@ from map_machine.constructor import Constructor
from map_machine.drawing import draw_text from map_machine.drawing import draw_text
from map_machine.feature.building import Building, draw_walls, BUILDING_SCALE from map_machine.feature.building import Building, draw_walls, BUILDING_SCALE
from map_machine.feature.road import Intersection, Road, RoadPart from map_machine.feature.road import Intersection, Road, RoadPart
from map_machine.figure import StyledFigure
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger from map_machine.geometry.flinger import Flinger
from map_machine.geometry.vector import Segment from map_machine.geometry.vector import Segment
@ -59,16 +58,13 @@ class Map:
self.svg.add( self.svg.add(
Rect((0.0, 0.0), self.flinger.size, fill=self.background_color) Rect((0.0, 0.0), self.flinger.size, fill=self.background_color)
) )
ways: List[StyledFigure] = sorted(
constructor.figures, key=lambda x: x.line_style.priority
)
logging.info("Drawing ways...") logging.info("Drawing ways...")
for way in ways: for figure in constructor.get_sorted_figures():
path_commands: str = way.get_path(self.flinger) path_commands: str = figure.get_path(self.flinger)
if path_commands: if path_commands:
path: SVGPath = SVGPath(d=path_commands) path: SVGPath = SVGPath(d=path_commands)
path.update(way.line_style.style) path.update(figure.line_style.style)
self.svg.add(path) self.svg.add(path)
constructor.roads.draw(self.svg, self.flinger) constructor.roads.draw(self.svg, self.flinger)
@ -135,7 +131,7 @@ class Map:
building.draw_shade(building_shade, self.flinger) building.draw_shade(building_shade, self.flinger)
self.svg.add(building_shade) self.svg.add(building_shade)
walls: dict[Segment, Building] = {} walls: Dict[Segment, Building] = {}
for building in constructor.buildings: for building in constructor.buildings:
for part in building.parts: for part in building.parts:

View file

@ -122,7 +122,7 @@ class Tagged:
return is_well_formed return is_well_formed
@dataclass @dataclass(eq=False)
class OSMNode(Tagged): class OSMNode(Tagged):
""" """
OpenStreetMap node. OpenStreetMap node.
@ -182,6 +182,19 @@ class OSMNode(Tagged):
def __hash__(self) -> int: def __hash__(self) -> int:
return self.id_ return self.id_
def __eq__(self, other) -> bool:
if not isinstance(other, OSMNode):
return False
return (
self.id_ == other.id_
and np.array_equal(self.coordinates, other.coordinates)
and self.visible == other.visible
and self.changeset == other.changeset
and self.timestamp == other.timestamp
and self.user == other.user
and self.uid == other.uid
)
@dataclass @dataclass
class OSMWay(Tagged): class OSMWay(Tagged):
@ -343,9 +356,11 @@ class OSMData:
def add_node(self, node: OSMNode) -> None: def add_node(self, node: OSMNode) -> None:
"""Add node and update map parameters.""" """Add node and update map parameters."""
if node.id_ in self.nodes: if node.id_ in self.nodes:
if node != self.nodes[node.id_]:
raise NotWellFormedOSMDataException( raise NotWellFormedOSMDataException(
f"Node with duplicate id {node.id_}." f"Node with duplicate id {node.id_}."
) )
return
self.nodes[node.id_] = node self.nodes[node.id_] = node
if node.user: if node.user:
self.authors.add(node.user) self.authors.add(node.user)
@ -360,9 +375,11 @@ class OSMData:
def add_way(self, way: OSMWay) -> None: def add_way(self, way: OSMWay) -> None:
"""Add way and update map parameters.""" """Add way and update map parameters."""
if way.id_ in self.ways: if way.id_ in self.ways:
if way != self.ways[way.id_]:
raise NotWellFormedOSMDataException( raise NotWellFormedOSMDataException(
f"Way with duplicate id {way.id_}." f"Way with duplicate id {way.id_}."
) )
return
self.ways[way.id_] = way self.ways[way.id_] = way
if way.user: if way.user:
self.authors.add(way.user) self.authors.add(way.user)
@ -374,9 +391,11 @@ class OSMData:
def add_relation(self, relation: OSMRelation) -> None: def add_relation(self, relation: OSMRelation) -> None:
"""Add relation and update map parameters.""" """Add relation and update map parameters."""
if relation.id_ in self.relations: if relation.id_ in self.relations:
if relation != self.relations[relation.id_]:
raise NotWellFormedOSMDataException( raise NotWellFormedOSMDataException(
f"Relation with duplicate id {relation.id_}." f"Relation with duplicate id {relation.id_}."
) )
return
self.relations[relation.id_] = relation self.relations[relation.id_] = relation
def parse_overpass(self, file_name: Path) -> None: def parse_overpass(self, file_name: Path) -> None:

View file

@ -5,7 +5,7 @@ import logging
import shutil import shutil
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Set, Union from typing import Dict, List, Optional, Set
import numpy as np import numpy as np
from colour import Color from colour import Color

View file

@ -83,6 +83,7 @@ colors:
track_color: "#A88A64" track_color: "#A88A64"
trunk_color: "#97612b" trunk_color: "#97612b"
tree_color: "#98AC64" tree_color: "#98AC64"
village_green_color: "#DDEEBB"
wall_bottom_1_color: "#AAAAAA" wall_bottom_1_color: "#AAAAAA"
wall_bottom_2_color: "#C3C3C3" wall_bottom_2_color: "#C3C3C3"
wall_color: "#E8E8E8" wall_color: "#E8E8E8"
@ -2389,6 +2390,9 @@ ways:
- tags: {landuse: farmland} - tags: {landuse: farmland}
style: {fill: farmland_color, stroke: farmland_border_color} style: {fill: farmland_color, stroke: farmland_border_color}
priority: 20.0 priority: 20.0
- tags: {landuse: greenhouse_horticulture}
style: {fill: farmland_color, stroke: farmland_border_color}
priority: 20.0
- tags: {landuse: farmyard} - tags: {landuse: farmyard}
style: {fill: farmland_color, stroke: farmland_border_color} # FIXME style: {fill: farmland_color, stroke: farmland_border_color} # FIXME
priority: 20.0 priority: 20.0
@ -2409,6 +2413,10 @@ ways:
style: style:
fill: parking_color fill: parking_color
priority: 21.0 priority: 21.0
- tags: {landuse: village_green}
style:
fill: village_green_color
priority: 20.0
- tags: {landuse: grass} - tags: {landuse: grass}
style: style:
fill: grass_color fill: grass_color
@ -2487,24 +2495,29 @@ ways:
style: style:
stroke: water_color stroke: water_color
stroke-width: 2.5 stroke-width: 2.5
priority: 22.0
- tags: {waterway: canal} - tags: {waterway: canal}
style: style:
stroke: water_color stroke: water_color
stroke-width: 2.0 stroke-width: 2.0
priority: 22.0
- tags: {waterway: stream} - tags: {waterway: stream}
style: style:
stroke: water_color stroke: water_color
stroke-width: 1.5 stroke-width: 1.5
priority: 22.0
- tags: {waterway: riverbank} - tags: {waterway: riverbank}
style: style:
fill: water_color fill: water_color
stroke: water_border_color stroke: water_border_color
stroke-width: 1.0 stroke-width: 1.0
priority: 22.0
- tags: {waterway: ditch} - tags: {waterway: ditch}
style: style:
fill: water_color fill: water_color
stroke: water_color stroke: water_color
stroke-width: 2.0 stroke-width: 2.0
priority: 22.0
- tags: {railway: subway} - tags: {railway: subway}
style: style:
@ -2542,6 +2555,13 @@ ways:
stroke-width: 3.0 stroke-width: 3.0
stroke: "#BBBBBB" stroke: "#BBBBBB"
priority: 42.0 priority: 42.0
- tags: {railway: construction}
style:
stroke-width: 3.0
stroke: "#000000"
stroke-dasharray: 3,3
opacity: 0.3
priority: 42.0
- tags: {railway: rail} - tags: {railway: rail}
style: style:
@ -2601,6 +2621,10 @@ ways:
style: style:
fill: grass_color fill: grass_color
opacity: 0.5 opacity: 0.5
- tags: {leisure: recreation_ground}
style:
fill: grass_color
opacity: 0.5
- tags: {leisure: stadium} - tags: {leisure: stadium}
style: style:
fill: grass_color fill: grass_color

55
tests/test_ways.py Normal file
View file

@ -0,0 +1,55 @@
import numpy as np
from map_machine.figure import Figure
from map_machine.geometry.boundary_box import BoundaryBox
from map_machine.geometry.flinger import Flinger
from map_machine.map_configuration import MapConfiguration
from map_machine.constructor import Constructor
from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode
from tests import SCHEME, SHAPE_EXTRACTOR
def get_constructor(osm_data: OSMData) -> Constructor:
flinger: Flinger = Flinger(
BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length
)
constructor: Constructor = Constructor(
osm_data, flinger, SCHEME, SHAPE_EXTRACTOR, MapConfiguration()
)
constructor.construct_ways()
return constructor
def test_river_and_wood() -> None:
"""
Check that river is above the wood.
See https://github.com/enzet/map-machine/issues/126
"""
nodes_1: list[OSMNode] = [
OSMNode({}, 1, np.array((-0.01, -0.01))),
OSMNode({}, 2, np.array((0.01, 0.01))),
]
nodes_2: list[OSMNode] = [
OSMNode({}, 3, np.array((-0.01, -0.01))),
OSMNode({}, 4, np.array((0.01, 0.01))),
]
osm_data: OSMData = OSMData()
osm_data.add_way(OSMWay({"natural": "wood"}, 1, nodes_1))
osm_data.add_way(OSMWay({"waterway": "river"}, 2, nodes_2))
figures: list[Figure] = get_constructor(osm_data).get_sorted_figures()
assert len(figures) == 2
assert figures[0].tags["natural"] == "wood"
assert figures[1].tags["waterway"] == "river"
def test_empty_ways() -> None:
"""Ways without nodes."""
osm_data: OSMData = OSMData()
osm_data.add_way(OSMWay({"natural": "wood"}, 1))
osm_data.add_way(OSMWay({"waterway": "river"}, 2))
assert not get_constructor(osm_data).get_sorted_figures()