Refactor flinger.

Element drawing module was using flinger for Mercator projection, which
is not exactly what is wanted.
This commit is contained in:
Sergey Vartanov 2022-09-02 23:56:55 +03:00
parent 77c8ec0044
commit c0879bff36
9 changed files with 90 additions and 62 deletions

View file

@ -55,7 +55,7 @@ TIME_COLOR_SCALE: list[Color] = [
def line_center( def line_center(
nodes: list[OSMNode], flinger: Flinger nodes: list[OSMNode], flinger: Flinger
) -> (np.ndarray, np.ndarray): ) -> tuple[np.ndarray, np.ndarray]:
""" """
Get geometric center of nodes set. Get geometric center of nodes set.

View file

@ -9,7 +9,7 @@ import svgwrite
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
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 MercatorFlinger
from map_machine.map_configuration import ( from map_machine.map_configuration import (
BuildingMode, BuildingMode,
DrawingMode, DrawingMode,
@ -21,16 +21,16 @@ from map_machine.osm.osm_getter import get_osm
from map_machine.osm.osm_reader import OSMData from map_machine.osm.osm_reader import OSMData
from map_machine.pictogram.icon import ShapeExtractor from map_machine.pictogram.icon import ShapeExtractor
from map_machine.scheme import Scheme from map_machine.scheme import Scheme
from map_machine.workspace import workspace
doc_path: Path = Path("doc") doc_path: Path = Path("doc")
cache: Path = Path("cache") cache: Path = Path("cache")
cache.mkdir(exist_ok=True) 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( EXTRACTOR: ShapeExtractor = ShapeExtractor(
Path("map_machine/icons/icons.svg"), workspace.ICONS_PATH, workspace.ICONS_CONFIG_PATH
Path("map_machine/icons/config.json"),
) )
@ -46,7 +46,7 @@ def draw(
osm_data: OSMData = OSMData() osm_data: OSMData = OSMData()
osm_data.parse_osm_file(input_file_name) osm_data.parse_osm_file(input_file_name)
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
boundary_box, configuration.zoom_level, osm_data.equator_length boundary_box, configuration.zoom_level, osm_data.equator_length
) )
constructor: Constructor = Constructor( constructor: Constructor = Constructor(

View file

@ -6,7 +6,7 @@ from map_machine.osm.osm_reader import Tags, OSMNode
def draw_node(tags: Tags, path: Path): def draw_node(tags: Tags, path: Path):
grid: Grid = Grid(x_step=0.0003, show_credit=False, margin=0.5) grid: Grid = Grid(show_credit=False, margin=0.5)
grid.add_node(tags, 0, 0) grid.add_node(tags, 0, 0)
grid.draw(path) grid.draw(path)
@ -16,7 +16,7 @@ def draw_way():
def draw_area(tags: Tags, path: Path): def draw_area(tags: Tags, path: Path):
grid: Grid = Grid(x_step=0.0003, show_credit=False, margin=0.5) grid: Grid = Grid(show_credit=False, margin=0.5)
node: OSMNode = grid.add_node({}, 0, 0) node: OSMNode = grid.add_node({}, 0, 0)
nodes: list[OSMNode] = [ nodes: list[OSMNode] = [
node, node,

View file

@ -6,8 +6,7 @@ from svgwrite import Drawing
from svgwrite.text import Text from svgwrite.text import Text
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
from map_machine.geometry.boundary_box import BoundaryBox from map_machine.geometry.flinger import Flinger, TranslateFlinger
from map_machine.geometry.flinger import Flinger
from map_machine.map_configuration import MapConfiguration from map_machine.map_configuration import MapConfiguration
from map_machine.mapper import Map from map_machine.mapper import Map
from map_machine.osm.osm_reader import ( from map_machine.osm.osm_reader import (
@ -36,8 +35,8 @@ class Grid:
def __init__( def __init__(
self, self,
x_step: float = 0.0002, x_step: float = 20.0,
y_step: float = 0.0003, y_step: float = 20.0,
show_credit: bool = True, show_credit: bool = True,
margin: float = 1.5, margin: float = 1.5,
) -> None: ) -> None:
@ -49,8 +48,8 @@ class Grid:
self.index: int = 0 self.index: int = 0
self.nodes: dict[OSMNode, tuple[int, int]] = {} self.nodes: dict[OSMNode, tuple[int, int]] = {}
self.max_j: float = 0 self.max_j: float = 0.0
self.max_i: float = 0 self.max_i: float = 0.0
self.way_id: int = 0 self.way_id: int = 0
self.relation_id: int = 0 self.relation_id: int = 0
@ -61,15 +60,11 @@ class Grid:
def add_node(self, tags: Tags, i: int, j: int) -> OSMNode: def add_node(self, tags: Tags, i: int, j: int) -> OSMNode:
"""Add OSM node to the grid.""" """Add OSM node to the grid."""
self.index += 1 self.index += 1
node: OSMNode = OSMNode( node: OSMNode = OSMNode(tags, self.index, np.array((i, j)))
tags,
self.index,
np.array((-i * self.y_step, j * self.x_step)),
)
self.nodes[node] = (j, i) self.nodes[node] = (j, i)
self.osm_data.add_node(node) self.osm_data.add_node(node)
self.max_j = max(self.max_j, j * self.x_step) self.max_j = max(self.max_j, j)
self.max_i = max(self.max_i, i * self.y_step) self.max_i = max(self.max_i, i)
return node return node
def add_way(self, tags: Tags, nodes: list[OSMNode]) -> OSMWay: def add_way(self, tags: Tags, nodes: list[OSMNode]) -> OSMWay:
@ -90,24 +85,22 @@ class Grid:
"""Add simple text label to the grid.""" """Add simple text label to the grid."""
self.texts.append((text, i, j)) self.texts.append((text, i, j))
def get_boundary_box(self) -> BoundaryBox: def draw(self, output_path: Path) -> None:
"""Compute resulting boundary box with margin of one grid step."""
return BoundaryBox(
-self.x_step * self.margin,
-self.max_i - self.y_step * self.margin,
self.max_j + self.x_step * self.margin,
self.y_step * self.margin,
)
def draw(self, output_path: Path, zoom: float = DEFAULT_ZOOM) -> None:
"""Draw grid.""" """Draw grid."""
configuration: MapConfiguration = MapConfiguration( configuration: MapConfiguration = MapConfiguration(
SCHEME, level="all", credit=None, show_credit=self.show_credit SCHEME, level="all", credit=None, show_credit=self.show_credit
) )
flinger: Flinger = Flinger( size = (
self.get_boundary_box(), zoom, self.osm_data.equator_length (self.max_i + self.margin * 2.0) * self.x_step,
(self.max_j + self.margin * 2.0) * self.y_step,
) )
svg: Drawing = Drawing(output_path.name, flinger.size)
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( constructor: Constructor = Constructor(
self.osm_data, flinger, SHAPE_EXTRACTOR, configuration self.osm_data, flinger, SHAPE_EXTRACTOR, configuration
) )
@ -119,7 +112,7 @@ class Grid:
svg.add( svg.add(
Text( Text(
text, text,
flinger.fling((-i * self.y_step, j * self.x_step)) + (0, 3), flinger.fling((i, j)) + (0, 3),
font_family="JetBrains Mono", font_family="JetBrains Mono",
font_size=12, font_size=12,
) )

View file

@ -67,17 +67,17 @@ def draw_overlapped_ways(types: list[dict[str, str]], path: Path) -> None:
The goal is to show check priority. The goal is to show check priority.
""" """
grid: Grid = Grid(0.00012, 0.00012) grid: Grid = Grid()
for index, tags in enumerate(types): for index, tags in enumerate(types):
node_1: OSMNode = grid.add_node({}, index + 1, 8) node_1: OSMNode = grid.add_node({}, 8, index + 1)
node_2: OSMNode = grid.add_node({}, index + 1, len(types) + 9) node_2: OSMNode = grid.add_node({}, len(types) + 9, index + 1)
grid.add_way(tags, [node_1, node_2]) grid.add_way(tags, [node_1, node_2])
grid.add_text(", ".join(f"{k}={tags[k]}" for k in tags), index + 1, 0) grid.add_text(", ".join(f"{k}={tags[k]}" for k in tags), 0, index + 1)
for index, tags in enumerate(types): for index, tags in enumerate(types):
node_1: OSMNode = grid.add_node({}, 0, index + 9) node_1: OSMNode = grid.add_node({}, index + 9, 0)
node_2: OSMNode = grid.add_node({}, len(types) + 1, index + 9) node_2: OSMNode = grid.add_node({}, index + 9, len(types) + 1)
grid.add_way(tags, [node_1, node_2]) grid.add_way(tags, [node_1, node_2])
grid.draw(path) grid.draw(path)
@ -106,7 +106,7 @@ def draw_road_features(
def draw_multipolygon(path: Path) -> None: def draw_multipolygon(path: Path) -> None:
"""Draw simple multipolygon with one outer and one inner way.""" """Draw simple multipolygon with one outer and one inner way."""
grid: Grid = Grid(y_step=0.0002) grid: Grid = Grid()
outer_node: OSMNode = grid.add_node({}, 0, 0) outer_node: OSMNode = grid.add_node({}, 0, 0)
outer_nodes: list[OSMNode] = [ outer_nodes: list[OSMNode] = [

View file

@ -13,15 +13,19 @@ def pseudo_mercator(coordinates: np.ndarray) -> np.ndarray:
""" """
Use spherical pseudo-Mercator projection to convert geo coordinates. 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) :param coordinates: geo positional in the form of (latitude, longitude)
:return: position on the plane in the form of (x, y) :return: position on the plane in the form of (x, y)
""" """
latitude, longitude = coordinates
y: float = ( y: float = (
180.0 180.0 / np.pi * np.log(np.tan(np.pi / 4.0 + latitude * np.pi / 360.0))
/ np.pi
* np.log(np.tan(np.pi / 4.0 + coordinates[0] * np.pi / 360.0))
) )
return np.array((coordinates[1], y)) return np.array((longitude, y))
def osm_zoom_level_to_pixels_per_meter( def osm_zoom_level_to_pixels_per_meter(
@ -40,7 +44,21 @@ def osm_zoom_level_to_pixels_per_meter(
class Flinger: 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__( def __init__(
self, self,
@ -57,24 +75,28 @@ class Flinger:
""" """
self.geo_boundaries: BoundaryBox = geo_boundaries self.geo_boundaries: BoundaryBox = geo_boundaries
self.ratio: float = 2.0**zoom_level * 256.0 / 360.0 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.max_())
- pseudo_mercator(self.geo_boundaries.min_()) - pseudo_mercator(self.geo_boundaries.min_())
) )
self.pixels_per_meter: float = osm_zoom_level_to_pixels_per_meter( self.pixels_per_meter: float = osm_zoom_level_to_pixels_per_meter(
zoom_level, equator_length 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: 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 * ( result: np.ndarray = (
pseudo_mercator(coordinates) self.ratio * pseudo_mercator(coordinates) - self.min_
- pseudo_mercator(self.geo_boundaries.min_())
) )
# Invert y axis on coordinate plane. # Invert y axis on coordinate plane.
@ -86,7 +108,8 @@ class Flinger:
""" """
Return pixels per meter ratio for the given geo coordinates. 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: if coordinates is None:
# Get pixels per meter ratio for the center of the boundary box. # 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)) scale_factor: float = abs(1.0 / np.cos(coordinates[0] / 180.0 * np.pi))
return self.pixels_per_meter * scale_factor 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

@ -19,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.feature.road import Intersection, Road, RoadPart
from map_machine.figure import StyledFigure 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, MercatorFlinger
from map_machine.geometry.vector import Segment from map_machine.geometry.vector import Segment
from map_machine.map_configuration import LabelMode, MapConfiguration from map_machine.map_configuration import LabelMode, MapConfiguration
from map_machine.osm.osm_getter import NetworkError, get_osm from map_machine.osm.osm_getter import NetworkError, get_osm
@ -345,7 +345,7 @@ def render_map(arguments: argparse.Namespace) -> None:
# Render the map. # Render the map.
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
boundary_box, arguments.zoom, osm_data.equator_length boundary_box, arguments.zoom, osm_data.equator_length
) )
size: np.ndarray = flinger.size size: np.ndarray = flinger.size

View file

@ -17,7 +17,7 @@ from PIL import Image
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
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 MercatorFlinger
from map_machine.map_configuration import MapConfiguration from map_machine.map_configuration import MapConfiguration
from map_machine.mapper import Map from map_machine.mapper import Map
from map_machine.osm.osm_getter import NetworkError, get_osm 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 self.x + 1, self.y + 1, self.zoom_level
).get_coordinates() ).get_coordinates()
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
BoundaryBox(left, bottom, right, top), BoundaryBox(left, bottom, right, top),
self.zoom_level, self.zoom_level,
osm_data.equator_length, osm_data.equator_length,
@ -381,7 +381,7 @@ class Tiles:
self.zoom_level, self.zoom_level,
).get_coordinates() ).get_coordinates()
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
BoundaryBox(left, bottom, right, top), BoundaryBox(left, bottom, right, top),
self.zoom_level, self.zoom_level,
osm_data.equator_length, osm_data.equator_length,

View file

@ -9,7 +9,7 @@ import numpy as np
from map_machine.constructor import Constructor from map_machine.constructor import Constructor
from map_machine.figure import Figure from map_machine.figure import Figure
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 MercatorFlinger
from map_machine.map_configuration import MapConfiguration from map_machine.map_configuration import MapConfiguration
from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags from map_machine.osm.osm_reader import OSMData, OSMWay, OSMNode, Tags
from tests import SCHEME, SHAPE_EXTRACTOR from tests import SCHEME, SHAPE_EXTRACTOR
@ -22,7 +22,7 @@ def get_constructor(osm_data: OSMData) -> Constructor:
Get custom constructor for bounds (-0.01, -0.01, 0.01, 0.01) and zoom level Get custom constructor for bounds (-0.01, -0.01, 0.01, 0.01) and zoom level
18. 18.
""" """
flinger: Flinger = Flinger( flinger: MercatorFlinger = MercatorFlinger(
BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length BoundaryBox(-0.01, -0.01, 0.01, 0.01), 18, osm_data.equator_length
) )
constructor: Constructor = Constructor( constructor: Constructor = Constructor(