mirror of
https://github.com/enzet/map-machine.git
synced 2025-04-29 18:27:19 +02:00
300 lines
9.2 KiB
Python
300 lines
9.2 KiB
Python
"""Point: node representation on the map."""
|
|
import logging
|
|
from typing import Dict, List, Optional, Set
|
|
|
|
import numpy as np
|
|
import svgwrite
|
|
from colour import Color
|
|
|
|
from map_machine.drawing import draw_text
|
|
from map_machine.map_configuration import LabelMode
|
|
from map_machine.osm.osm_reader import Tagged
|
|
from map_machine.pictogram.icon import Icon, IconSet
|
|
from map_machine.text import Label
|
|
|
|
__author__ = "Sergey Vartanov"
|
|
__email__ = "me@enzet.ru"
|
|
|
|
|
|
class Occupied:
|
|
"""
|
|
Structure that remembers places of the canvas occupied by elements (icons,
|
|
texts, shapes).
|
|
"""
|
|
|
|
def __init__(self, width: int, height: int, overlap: int) -> None:
|
|
self.matrix = np.full((int(width), int(height)), False, dtype=bool)
|
|
try:
|
|
self.matrix = np.full((int(width), int(height)), False, dtype=bool)
|
|
except Exception:
|
|
logging.fatal(
|
|
"Failed to allocate a matrix required by overlap algorithm. "
|
|
"Try to use smallest area or try --overlap=0 options."
|
|
)
|
|
exit(1)
|
|
|
|
self.width: float = width
|
|
self.height: float = height
|
|
self.overlap: int = overlap
|
|
|
|
def check(self, point: np.ndarray) -> bool:
|
|
"""Check whether point is already occupied by other elements."""
|
|
if 0.0 <= point[0] < self.width and 0.0 <= point[1] < self.height:
|
|
return self.matrix[point[0], point[1]]
|
|
return True
|
|
|
|
def register(self, point: np.ndarray) -> None:
|
|
"""Register that point is occupied by an element."""
|
|
if 0.0 <= point[0] < self.width and 0.0 <= point[1] < self.height:
|
|
self.matrix[point[0], point[1]] = True
|
|
assert self.matrix[point[0], point[1]]
|
|
|
|
|
|
class Point(Tagged):
|
|
"""
|
|
Object on the map with no dimensional attributes.
|
|
|
|
It may have icons and labels.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
icon_set: IconSet,
|
|
labels: List[Label],
|
|
tags: Dict[str, str],
|
|
processed: Set[str],
|
|
point: np.ndarray,
|
|
priority: float = 0.0,
|
|
is_for_node: bool = True,
|
|
draw_outline: bool = True,
|
|
add_tooltips: bool = False,
|
|
) -> None:
|
|
super().__init__(tags)
|
|
|
|
assert point is not None
|
|
|
|
self.icon_set: IconSet = icon_set
|
|
self.labels: List[Label] = labels
|
|
self.processed: Set[str] = processed
|
|
self.point: np.ndarray = point
|
|
self.priority: float = priority
|
|
self.layer: float = 0.0
|
|
self.is_for_node: bool = is_for_node
|
|
self.draw_outline: bool = draw_outline
|
|
self.add_tooltips: bool = add_tooltips
|
|
|
|
self.y: float = 0.0
|
|
self.main_icon_painted: bool = False
|
|
|
|
def draw_main_shapes(
|
|
self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None
|
|
) -> None:
|
|
"""Draw main shape for one node."""
|
|
keys_left = [x for x in self.tags.keys() if x not in self.processed]
|
|
if (
|
|
self.icon_set.main_icon.is_default()
|
|
and not self.icon_set.extra_icons
|
|
and (not keys_left or not self.is_for_node)
|
|
):
|
|
return
|
|
|
|
position: np.ndarray = self.point + np.array((0.0, self.y))
|
|
tags: Optional[Dict[str, str]] = (
|
|
self.tags if self.add_tooltips else None
|
|
)
|
|
self.main_icon_painted: bool = self.draw_point_shape(
|
|
svg,
|
|
self.icon_set.main_icon,
|
|
self.icon_set.default_icon,
|
|
position,
|
|
occupied,
|
|
tags=tags,
|
|
)
|
|
if self.main_icon_painted:
|
|
self.y += 16.0
|
|
|
|
def draw_extra_shapes(
|
|
self, svg: svgwrite.Drawing, occupied: Optional[Occupied] = None
|
|
) -> None:
|
|
"""Draw secondary shapes."""
|
|
if not self.icon_set.extra_icons or not self.main_icon_painted:
|
|
return
|
|
|
|
is_place_for_extra: bool = True
|
|
if occupied:
|
|
left: float = -(len(self.icon_set.extra_icons) - 1.0) * 8.0
|
|
for _ in self.icon_set.extra_icons:
|
|
point: np.ndarray = np.array(
|
|
(int(self.point[0] + left), int(self.point[1] + self.y))
|
|
)
|
|
if occupied.check(point):
|
|
is_place_for_extra = False
|
|
break
|
|
left += 16.0
|
|
|
|
if is_place_for_extra:
|
|
left: float = -(len(self.icon_set.extra_icons) - 1.0) * 8.0
|
|
for icon in self.icon_set.extra_icons:
|
|
point: np.ndarray = self.point + np.array((left, self.y))
|
|
self.draw_point_shape(svg, icon, None, point, occupied=occupied)
|
|
left += 16.0
|
|
if self.icon_set.extra_icons:
|
|
self.y += 16.0
|
|
|
|
def draw_point_shape(
|
|
self,
|
|
svg: svgwrite.Drawing,
|
|
icon: Icon,
|
|
default_icon: Optional[Icon],
|
|
position: np.ndarray,
|
|
occupied: Optional[Occupied],
|
|
tags: Optional[Dict[str, str]] = None,
|
|
) -> bool:
|
|
"""Draw one combined icon and its outline."""
|
|
# Down-cast floats to integers to make icons pixel-perfect.
|
|
position: np.ndarray = np.array((int(position[0]), int(position[1])))
|
|
|
|
icon_to_draw: Icon = icon
|
|
is_painted: bool = True
|
|
|
|
if occupied and occupied.check(position):
|
|
if default_icon:
|
|
icon_to_draw = default_icon
|
|
is_painted = False
|
|
else:
|
|
return False
|
|
|
|
if self.draw_outline:
|
|
icon_to_draw.draw(svg, position, outline=True)
|
|
|
|
icon_to_draw.draw(svg, position, tags=tags)
|
|
|
|
if occupied and is_painted:
|
|
overlap: int = occupied.overlap
|
|
for i in range(-overlap, overlap):
|
|
for j in range(-overlap, overlap):
|
|
occupied.register(
|
|
np.array((position[0] + i, position[1] + j))
|
|
)
|
|
|
|
return is_painted
|
|
|
|
def draw_texts(
|
|
self,
|
|
svg: svgwrite.Drawing,
|
|
occupied: Optional[Occupied] = None,
|
|
label_mode: LabelMode = LabelMode.MAIN,
|
|
) -> None:
|
|
"""Draw all labels."""
|
|
labels: List[Label]
|
|
|
|
if label_mode == LabelMode.MAIN:
|
|
labels = self.labels[:1]
|
|
elif label_mode == LabelMode.ALL:
|
|
labels = self.labels
|
|
else:
|
|
return
|
|
|
|
for label in labels:
|
|
text = label.text
|
|
text = text.replace(""", '"')
|
|
text = text.replace("&", "&")
|
|
text = text[:26] + ("..." if len(text) > 26 else "")
|
|
point = self.point + np.array((0.0, self.y + 2.0))
|
|
self.draw_text(
|
|
svg,
|
|
text,
|
|
point,
|
|
occupied,
|
|
label.fill,
|
|
label.size,
|
|
label.out_fill,
|
|
)
|
|
|
|
def draw_text(
|
|
self,
|
|
svg: svgwrite.Drawing,
|
|
text: str,
|
|
point: np.ndarray,
|
|
occupied: Optional[Occupied],
|
|
fill: Color,
|
|
size: float,
|
|
out_fill: Color,
|
|
out_opacity: float = 0.5,
|
|
out_fill_2: Optional[Color] = None,
|
|
out_opacity_2: float = 1.0,
|
|
is_debug: bool = False,
|
|
) -> None:
|
|
"""
|
|
Drawing text.
|
|
|
|
###### ### outline 2
|
|
#------# --- outline 1
|
|
#| Text |#
|
|
#------#
|
|
######
|
|
"""
|
|
length: int = len(text) * 6 # FIXME
|
|
|
|
if occupied:
|
|
is_occupied: bool = False
|
|
for i in range(-int(length / 2.0), int(length / 2.0)):
|
|
text_position: np.ndarray = np.array(
|
|
(int(point[0] + i), int(point[1] - 4.0))
|
|
)
|
|
if occupied.check(text_position):
|
|
is_occupied = True
|
|
break
|
|
|
|
if is_occupied:
|
|
return
|
|
|
|
for i in range(-int(length / 2.0), int(length / 2.0)):
|
|
for j in range(-12, 5):
|
|
occupied.register(
|
|
np.array((int(point[0] + i), int(point[1] + j)))
|
|
)
|
|
if is_debug:
|
|
svg.add(svg.rect((point[0] + i, point[1] + j), (1, 1)))
|
|
|
|
if out_fill_2:
|
|
draw_text(
|
|
svg,
|
|
text,
|
|
point,
|
|
size,
|
|
fill=out_fill_2,
|
|
stroke_width=5.0,
|
|
stroke=out_fill_2,
|
|
opacity=out_opacity_2,
|
|
)
|
|
if out_fill:
|
|
draw_text(
|
|
svg,
|
|
text,
|
|
point,
|
|
size,
|
|
fill,
|
|
stroke_width=3.0,
|
|
stroke=out_fill,
|
|
opacity=out_opacity,
|
|
)
|
|
draw_text(svg, text, point, size, fill)
|
|
|
|
self.y += 11
|
|
|
|
def get_size(self) -> np.ndarray:
|
|
"""
|
|
Get width and height of the point visual representation if there is
|
|
space for all elements.
|
|
"""
|
|
icon_size: int = 16
|
|
width: int = icon_size * (
|
|
1 + max(2, len(self.icon_set.extra_icons) - 1)
|
|
)
|
|
height: int = icon_size * (
|
|
1 + np.ceil(len(self.icon_set.extra_icons) / 3.0)
|
|
)
|
|
if len(self.labels):
|
|
height += 4 + 11 * len(self.labels)
|
|
return np.array((width, height))
|