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.
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
------------------------
@ -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`.
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.
\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} {}
\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}.
@ -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}.
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:
\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}.
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)
processed: set[str] = set()
processed: Set[str] = set()
priority: int
icon_set: IconSet
icon_set, priority = self.scheme.get_icon(
self.extractor, line.tags, processed, self.configuration
)
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
)
point: Point = Point(
@ -529,6 +529,10 @@ class Constructor:
)
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:
"""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__":
convert(Path("doc/readme.moi"), Path("README.md"))
convert(Path("doc/contributing.moi"), Path(".github/CONTRIBUTING.md"))
for id_ in "readme", "contributing":
convert(Path("doc") / f"{id_}.moi", Path(f"{id_.upper()}.md"))

View file

View file

@ -1,8 +1,7 @@
"""
Figures displayed on the map.
"""
from typing import Any, Dict, Iterator, List, Optional
from svgwrite import Drawing
from typing import Dict, List
import numpy as np
@ -56,25 +55,6 @@ class Figure(Tagged):
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):
"""Figure with stroke and fill style."""

View file

@ -47,7 +47,7 @@ def main() -> None:
mapcss.generate_mapcss(arguments)
elif arguments.command == "element":
from map_machine.element import draw_element
from map_machine.element.element import draw_element
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.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.vector import Segment
@ -59,16 +58,13 @@ class Map:
self.svg.add(
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...")
for way in ways:
path_commands: str = way.get_path(self.flinger)
for figure in constructor.get_sorted_figures():
path_commands: str = figure.get_path(self.flinger)
if path_commands:
path: SVGPath = SVGPath(d=path_commands)
path.update(way.line_style.style)
path.update(figure.line_style.style)
self.svg.add(path)
constructor.roads.draw(self.svg, self.flinger)
@ -135,7 +131,7 @@ class Map:
building.draw_shade(building_shade, self.flinger)
self.svg.add(building_shade)
walls: dict[Segment, Building] = {}
walls: Dict[Segment, Building] = {}
for building in constructor.buildings:
for part in building.parts:

View file

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

View file

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

View file

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