mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-01 03:07:41 +02:00
Add --coordinates and --size arguments.
Construct boundary box from center coordinates and size.
This commit is contained in:
parent
cb440e8a8b
commit
b715e12924
8 changed files with 156 additions and 114 deletions
|
@ -76,6 +76,45 @@ class BoundaryBox:
|
|||
|
||||
return cls(left, bottom, right, top)
|
||||
|
||||
@classmethod
|
||||
def from_coordinates(
|
||||
cls,
|
||||
coordinates: np.ndarray,
|
||||
zoom_level: float,
|
||||
width: float,
|
||||
height: float,
|
||||
) -> "BoundaryBox":
|
||||
"""
|
||||
Compute boundary box from central coordinates, zoom level and resulting
|
||||
image size.
|
||||
|
||||
:param coordinates: boundary box central coordinates
|
||||
:param zoom_level: resulting image zoom level
|
||||
:param width: resulting image width
|
||||
:param height: resulting image height
|
||||
"""
|
||||
lat_rad: np.ndarray = np.radians(coordinates[0])
|
||||
n: float = 2.0 ** (zoom_level + 8.0)
|
||||
|
||||
x: int = int((coordinates[1] + 180.0) / 360.0 * n)
|
||||
left: float = (x - width / 2) / n * 360.0 - 180.0
|
||||
right: float = (x + width / 2) / n * 360.0 - 180.0
|
||||
|
||||
y: int = (1.0 - np.arcsinh(np.tan(lat_rad)) / np.pi) / 2.0 * n
|
||||
bottom_radians = np.arctan(
|
||||
np.sinh((1.0 - (y + height / 2) * 2.0 / n) * np.pi)
|
||||
)
|
||||
top_radians = np.arctan(
|
||||
np.sinh((1.0 - (y - height / 2) * 2.0 / n) * np.pi)
|
||||
)
|
||||
|
||||
return cls(
|
||||
left,
|
||||
float(np.degrees(bottom_radians)),
|
||||
right,
|
||||
float(np.degrees(top_radians)),
|
||||
)
|
||||
|
||||
def min_(self) -> np.ndarray:
|
||||
"""Get minimum coordinates."""
|
||||
return np.array((self.bottom, self.left))
|
||||
|
@ -113,6 +152,16 @@ class BoundaryBox:
|
|||
<longitude 1>,<latitude 1>,<longitude 2>,<latitude 2>. Coordinates are
|
||||
rounded to three digits after comma.
|
||||
"""
|
||||
return (
|
||||
f"{self.left:.3f},{self.bottom:.3f},{self.right:.3f},{self.top:.3f}"
|
||||
)
|
||||
left: float = np.floor(self.left * 1000) / 1000
|
||||
bottom: float = np.floor(self.bottom * 1000) / 1000
|
||||
right: float = np.ceil(self.right * 1000) / 1000
|
||||
top: float = np.ceil(self.top * 1000) / 1000
|
||||
|
||||
return f"{left:.3f},{bottom:.3f},{right:.3f},{top:.3f}"
|
||||
|
||||
def combine(self, other: "BoundaryBox") -> None:
|
||||
"""Combine with another boundary box."""
|
||||
self.left = min(self.left, other.left)
|
||||
self.right = min(self.right, other.right)
|
||||
self.bottom = min(self.bottom, other.bottom)
|
||||
self.top = min(self.top, other.top)
|
||||
|
|
|
@ -406,7 +406,7 @@ class DirectionSector(Tagged):
|
|||
angle = float(self.get_tag("camera:angle"))
|
||||
if "angle" in self.tags:
|
||||
angle = float(self.get_tag("angle"))
|
||||
direction_radius = 25
|
||||
direction_radius = 50
|
||||
direction_color = scheme.get_color("direction_camera_color")
|
||||
elif self.get_tag("traffic_sign") == "stop":
|
||||
direction = self.get_tag("direction")
|
||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
|||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from map_machine.ui import parse_options
|
||||
from map_machine.ui import parse_arguments
|
||||
from map_machine.workspace import Workspace
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
|
@ -18,7 +18,7 @@ def main() -> None:
|
|||
logging.basicConfig(format="%(levelname)s %(message)s", level=logging.INFO)
|
||||
workspace: Workspace = Workspace(Path("out"))
|
||||
|
||||
arguments: argparse.Namespace = parse_options(sys.argv)
|
||||
arguments: argparse.Namespace = parse_arguments(sys.argv)
|
||||
|
||||
if not arguments.command:
|
||||
logging.fatal("No command provided. See --help.")
|
||||
|
|
|
@ -54,6 +54,7 @@ class MapConfiguration:
|
|||
seed: str = ""
|
||||
show_tooltips: bool = False
|
||||
country: str = "world"
|
||||
ignore_level_matching: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_options(
|
||||
|
|
|
@ -20,7 +20,7 @@ from map_machine.flinger import Flinger
|
|||
from map_machine.icon import ShapeExtractor
|
||||
from map_machine.map_configuration import LabelMode, MapConfiguration
|
||||
from map_machine.osm_getter import NetworkError, get_osm
|
||||
from map_machine.osm_reader import OSMData, OSMNode, OSMReader, OverpassReader
|
||||
from map_machine.osm_reader import OSMData, OSMNode
|
||||
from map_machine.point import Occupied, Point
|
||||
from map_machine.road import Intersection, RoadPart
|
||||
from map_machine.scheme import Scheme
|
||||
|
@ -94,11 +94,11 @@ class Map:
|
|||
for tree in constructor.craters:
|
||||
tree.draw(self.svg, self.flinger)
|
||||
|
||||
self.draw_buildings(constructor)
|
||||
|
||||
for direction_sector in constructor.direction_sectors:
|
||||
direction_sector.draw(self.svg, self.scheme)
|
||||
|
||||
self.draw_buildings(constructor)
|
||||
|
||||
# All other points
|
||||
|
||||
occupied: Optional[Occupied]
|
||||
|
@ -201,74 +201,73 @@ class Map:
|
|||
intersection.draw(self.svg, True)
|
||||
|
||||
|
||||
def ui(options: argparse.Namespace) -> None:
|
||||
def ui(arguments: argparse.Namespace) -> None:
|
||||
"""
|
||||
Map Machine entry point.
|
||||
|
||||
:param options: command-line arguments
|
||||
:param arguments: command-line arguments
|
||||
"""
|
||||
configuration: MapConfiguration = MapConfiguration.from_options(
|
||||
options, int(options.zoom)
|
||||
arguments, int(arguments.zoom)
|
||||
)
|
||||
if not options.boundary_box and not options.input_file_name:
|
||||
logging.fatal("Specify either --boundary-box, or --input.")
|
||||
exit(1)
|
||||
|
||||
if options.boundary_box:
|
||||
boundary_box: BoundaryBox = BoundaryBox.from_text(options.boundary_box)
|
||||
|
||||
cache_path: Path = Path(options.cache)
|
||||
cache_path: Path = Path(arguments.cache)
|
||||
cache_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
input_file_names: list[Path]
|
||||
boundary_box: Optional[BoundaryBox] = None
|
||||
input_file_names: list[Path] = []
|
||||
|
||||
if options.input_file_name:
|
||||
input_file_names = list(map(Path, options.input_file_name))
|
||||
if arguments.input_file_names:
|
||||
input_file_names = list(map(Path, arguments.input_file_names))
|
||||
else:
|
||||
if arguments.boundary_box:
|
||||
boundary_box = BoundaryBox.from_text(arguments.boundary_box)
|
||||
else:
|
||||
coordinates: np.ndarray = np.array(
|
||||
list(map(float, arguments.coordinates.split(",")))
|
||||
)
|
||||
width, height = np.array(
|
||||
list(map(float, arguments.size.split(",")))
|
||||
)
|
||||
boundary_box = BoundaryBox.from_coordinates(
|
||||
coordinates, configuration.zoom_level, width, height
|
||||
)
|
||||
|
||||
try:
|
||||
cache_file_path: Path = (
|
||||
cache_path / f"{boundary_box.get_format()}.osm"
|
||||
)
|
||||
get_osm(boundary_box, cache_file_path)
|
||||
input_file_names = [cache_file_path]
|
||||
except NetworkError as e:
|
||||
logging.fatal(e.message)
|
||||
exit(1)
|
||||
input_file_names = [cache_file_path]
|
||||
|
||||
scheme: Scheme = Scheme(workspace.DEFAULT_SCHEME_PATH)
|
||||
min_: np.ndarray
|
||||
max_: np.ndarray
|
||||
osm_data: OSMData
|
||||
view_box: BoundaryBox
|
||||
|
||||
if input_file_names[0].name.endswith(".json"):
|
||||
reader: OverpassReader = OverpassReader()
|
||||
reader.parse_json_file(input_file_names[0])
|
||||
osm_data: OSMData = OSMData()
|
||||
|
||||
osm_data = reader.osm_data
|
||||
view_box = boundary_box
|
||||
else:
|
||||
osm_reader = OSMReader()
|
||||
|
||||
for file_name in input_file_names:
|
||||
if not file_name.is_file():
|
||||
logging.fatal(f"No such file: {file_name}.")
|
||||
for input_file_name in input_file_names:
|
||||
if not input_file_name.is_file():
|
||||
logging.fatal(f"No such file: {input_file_name}.")
|
||||
exit(1)
|
||||
|
||||
osm_reader.parse_osm_file(file_name)
|
||||
|
||||
osm_data = osm_reader.osm_data
|
||||
|
||||
if options.boundary_box:
|
||||
view_box = boundary_box
|
||||
if input_file_name.name.endswith(".json"):
|
||||
osm_data.parse_overpass(input_file_name)
|
||||
else:
|
||||
view_box = osm_data.view_box
|
||||
osm_data.parse_osm_file(input_file_name)
|
||||
|
||||
flinger: Flinger = Flinger(view_box, options.zoom, osm_data.equator_length)
|
||||
view_box: BoundaryBox = boundary_box if boundary_box else osm_data.view_box
|
||||
|
||||
flinger: Flinger = Flinger(
|
||||
view_box, arguments.zoom, osm_data.equator_length
|
||||
)
|
||||
size: np.ndarray = flinger.size
|
||||
|
||||
svg: svgwrite.Drawing = svgwrite.Drawing(
|
||||
options.output_file_name, size=size
|
||||
arguments.output_file_name, size=size
|
||||
)
|
||||
icon_extractor: ShapeExtractor = ShapeExtractor(
|
||||
workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
|
||||
|
@ -288,6 +287,6 @@ def ui(options: argparse.Namespace) -> None:
|
|||
)
|
||||
painter.draw(constructor)
|
||||
|
||||
logging.info(f"Writing output SVG to {options.output_file_name}...")
|
||||
with open(options.output_file_name, "w") as output_file:
|
||||
logging.info(f"Writing output SVG to {arguments.output_file_name}...")
|
||||
with open(arguments.output_file_name, "w") as output_file:
|
||||
svg.write(output_file)
|
||||
|
|
|
@ -355,119 +355,97 @@ class OSMData:
|
|||
)
|
||||
self.relations[relation.id_] = relation
|
||||
|
||||
|
||||
class OverpassReader:
|
||||
def parse_overpass(self, file_name: Path) -> None:
|
||||
"""
|
||||
Reader for JSON structure extracted from Overpass API.
|
||||
Parse JSON structure extracted from Overpass API.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/Overpass_API
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.osm_data = OSMData()
|
||||
|
||||
def parse_json_file(self, file_name: Path) -> OSMData:
|
||||
"""Parse JSON structure from the file and construct map."""
|
||||
with file_name.open() as input_file:
|
||||
structure = json.load(input_file)
|
||||
|
||||
node_map = {}
|
||||
way_map = {}
|
||||
node_map: dict[int, OSMNode] = {}
|
||||
way_map: dict[int, OSMWay] = {}
|
||||
|
||||
for element in structure["elements"]:
|
||||
if element["type"] == "node":
|
||||
node = OSMNode.parse_from_structure(element)
|
||||
node_map[node.id_] = node
|
||||
self.osm_data.add_node(node)
|
||||
self.add_node(node)
|
||||
|
||||
for element in structure["elements"]:
|
||||
if element["type"] == "way":
|
||||
way = OSMWay.parse_from_structure(element, node_map)
|
||||
way_map[way.id_] = way
|
||||
self.osm_data.add_way(way)
|
||||
self.add_way(way)
|
||||
|
||||
for element in structure["elements"]:
|
||||
if element["type"] == "relation":
|
||||
relation = OSMRelation.parse_from_structure(element)
|
||||
self.osm_data.add_relation(relation)
|
||||
self.add_relation(relation)
|
||||
|
||||
return self.osm_data
|
||||
|
||||
|
||||
class OSMReader:
|
||||
"""
|
||||
OpenStreetMap XML file parser.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/OSM_XML
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parse_nodes: bool = True,
|
||||
parse_ways: bool = True,
|
||||
parse_relations: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
:param parse_nodes: whether nodes should be parsed
|
||||
:param parse_ways: whether ways should be parsed
|
||||
:param parse_relations: whether relations should be parsed
|
||||
"""
|
||||
self.osm_data = OSMData()
|
||||
self.parse_nodes: bool = parse_nodes
|
||||
self.parse_ways: bool = parse_ways
|
||||
self.parse_relations: bool = parse_relations
|
||||
|
||||
def parse_osm_file(self, file_name: Path) -> OSMData:
|
||||
def parse_osm_file(self, file_name: Path) -> None:
|
||||
"""
|
||||
Parse OSM XML file.
|
||||
|
||||
See https://wiki.openstreetmap.org/wiki/OSM_XML
|
||||
|
||||
:param file_name: input XML file
|
||||
:return: parsed map
|
||||
"""
|
||||
return self.parse_osm(ElementTree.parse(file_name).getroot())
|
||||
self.parse_osm(ElementTree.parse(file_name).getroot())
|
||||
|
||||
def parse_osm_text(self, text: str) -> OSMData:
|
||||
def parse_osm_text(self, text: str) -> None:
|
||||
"""
|
||||
Parse OSM XML data from text representation.
|
||||
|
||||
:param text: XML text representation
|
||||
:return: parsed map
|
||||
"""
|
||||
return self.parse_osm(ElementTree.fromstring(text))
|
||||
self.parse_osm(ElementTree.fromstring(text))
|
||||
|
||||
def parse_osm(self, root: Element) -> OSMData:
|
||||
def parse_osm(
|
||||
self,
|
||||
root: Element,
|
||||
parse_nodes: bool = True,
|
||||
parse_ways: bool = True,
|
||||
parse_relations: bool = True,
|
||||
) -> None:
|
||||
"""
|
||||
Parse OSM XML data.
|
||||
|
||||
:param root: top element of XML data
|
||||
:return: parsed map
|
||||
:param parse_nodes: whether nodes should be parsed
|
||||
:param parse_ways: whether ways should be parsed
|
||||
:param parse_relations: whether relations should be parsed
|
||||
"""
|
||||
for element in root:
|
||||
if element.tag == "bounds":
|
||||
self.parse_bounds(element)
|
||||
elif element.tag == "object":
|
||||
self.parse_object(element)
|
||||
elif element.tag == "node" and self.parse_nodes:
|
||||
elif element.tag == "node" and parse_nodes:
|
||||
node = OSMNode.from_xml_structure(element)
|
||||
self.osm_data.add_node(node)
|
||||
elif element.tag == "way" and self.parse_ways:
|
||||
self.osm_data.add_way(
|
||||
OSMWay.from_xml_structure(element, self.osm_data.nodes)
|
||||
)
|
||||
elif element.tag == "relation" and self.parse_relations:
|
||||
self.osm_data.add_relation(
|
||||
OSMRelation.from_xml_structure(element)
|
||||
)
|
||||
return self.osm_data
|
||||
self.add_node(node)
|
||||
elif element.tag == "way" and parse_ways:
|
||||
self.add_way(OSMWay.from_xml_structure(element, self.nodes))
|
||||
elif element.tag == "relation" and parse_relations:
|
||||
self.add_relation(OSMRelation.from_xml_structure(element))
|
||||
|
||||
def parse_bounds(self, element: Element) -> None:
|
||||
"""Parse view box from XML element."""
|
||||
attributes = element.attrib
|
||||
self.osm_data.view_box = BoundaryBox(
|
||||
boundary_box: BoundaryBox = BoundaryBox(
|
||||
float(attributes["minlon"]),
|
||||
float(attributes["minlat"]),
|
||||
float(attributes["maxlon"]),
|
||||
float(attributes["maxlat"]),
|
||||
)
|
||||
if self.view_box:
|
||||
self.view_box.combine(boundary_box)
|
||||
else:
|
||||
self.view_box = boundary_box
|
||||
|
||||
def parse_object(self, element: Element) -> None:
|
||||
"""Parse astronomical object properties from XML element."""
|
||||
self.osm_data.equator_length = float(element.get("equator"))
|
||||
self.equator_length = float(element.get("equator"))
|
||||
|
|
|
@ -414,7 +414,10 @@ class Scheme:
|
|||
continue
|
||||
if not matcher.is_matched(tags, configuration):
|
||||
continue
|
||||
if not matcher.check_zoom_level(configuration.zoom_level):
|
||||
if (
|
||||
not configuration.ignore_level_matching
|
||||
and not matcher.check_zoom_level(configuration.zoom_level)
|
||||
):
|
||||
return None, 0
|
||||
matcher_tags: set[str] = set(matcher.tags.keys())
|
||||
priority = len(self.node_matchers) - index
|
||||
|
|
|
@ -29,8 +29,8 @@ COMMANDS: dict[str, list[str]] = {
|
|||
}
|
||||
|
||||
|
||||
def parse_options(args: list[str]) -> argparse.Namespace:
|
||||
"""Parse Map Machine command-line options."""
|
||||
def parse_arguments(args: list[str]) -> argparse.Namespace:
|
||||
"""Parse Map Machine command-line arguments."""
|
||||
parser: argparse.ArgumentParser = argparse.ArgumentParser(
|
||||
description="Map Machine. OpenStreetMap renderer with custom icon set"
|
||||
)
|
||||
|
@ -203,7 +203,7 @@ def add_render_arguments(parser: argparse.ArgumentParser) -> None:
|
|||
parser.add_argument(
|
||||
"-i",
|
||||
"--input",
|
||||
dest="input_file_name",
|
||||
dest="input_file_names",
|
||||
metavar="<path>",
|
||||
nargs="*",
|
||||
help="input XML file name or names (if not specified, file will be "
|
||||
|
@ -238,6 +238,18 @@ def add_render_arguments(parser: argparse.ArgumentParser) -> None:
|
|||
help="OSM zoom level",
|
||||
default=18,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--coordinates",
|
||||
metavar="<latitude>,<longitude>",
|
||||
help="coordinates of any location inside the tile",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--size",
|
||||
metavar="<width>,<height>",
|
||||
help="resulted image size",
|
||||
)
|
||||
|
||||
|
||||
def add_mapcss_arguments(parser: argparse.ArgumentParser) -> None:
|
||||
|
|
Loading…
Add table
Reference in a new issue