mirror of
https://github.com/enzet/map-machine.git
synced 2025-07-03 01:37:48 +02:00
Merge main.
This commit is contained in:
commit
518e8f9590
14 changed files with 192 additions and 102 deletions
|
@ -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.
|
||||
|
|
@ -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}.
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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"))
|
||||
|
|
0
map_machine/element/__init__.py
Normal file
0
map_machine/element/__init__.py
Normal 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."""
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
55
tests/test_ways.py
Normal 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()
|
Loading…
Add table
Add a link
Reference in a new issue