diff --git a/roentgen/constructor.py b/roentgen/constructor.py index be1f919..f9f5123 100644 --- a/roentgen/constructor.py +++ b/roentgen/constructor.py @@ -13,11 +13,14 @@ from typing import Any, Dict, List, Optional, Set from roentgen import ui from roentgen.extract_icon import DEFAULT_SMALL_SHAPE_ID from roentgen.flinger import Flinger -from roentgen.osm_reader import OSMMember, OSMRelation, OSMWay, OSMNode +from roentgen.osm_reader import Map, OSMMember, OSMRelation, OSMWay, OSMNode, Tagged from roentgen.scheme import IconSet, Scheme -from roentgen.util import MinMax +from roentgen.util import MinMax, get_gradient_color DEBUG: bool = False +TIME_COLOR_SCALE: List[Color] = [ + Color("#581845"), Color("#900C3F"), Color("#C70039"), Color("#FF5733"), + Color("#FFC300"), Color("#DAF7A6")] def is_clockwise(polygon: List[OSMNode]) -> bool: @@ -47,23 +50,23 @@ def make_counter_clockwise(polygon: List[OSMNode]) -> List[OSMNode]: return list(reversed(polygon)) -class Node: +class Node(Tagged): """ Node in Röntgen terms. """ def __init__( self, icon_set: IconSet, tags: Dict[str, str], point: np.array, coordinates: np.array, - priority: int = 0, is_for_node: bool = True): + priority: float = 0, is_for_node: bool = True): assert point is not None self.icon_set: IconSet = icon_set - self.tags = tags + self.tags: Dict[str, str] = tags self.point: np.array = point self.coordinates: np.array = coordinates - self.priority = priority - self.layer = 0 - self.is_for_node = is_for_node + self.priority: float = priority + self.layer: float = 0 + self.is_for_node: bool = is_for_node def get_tag(self, key: str): if key in self.tags: @@ -137,33 +140,14 @@ def get_user_color(text: str, seed: str) -> Color: """ if text == "": return Color("black") - rgb = sha256((seed + text).encode("utf-8")).hexdigest()[-6:] - r = int(rgb[0:2], 16) - g = int(rgb[2:4], 16) - b = int(rgb[4:6], 16) - c = (r + g + b) / 3. - cc = 0 - r = r * (1 - cc) + c * cc - g = g * (1 - cc) + c * cc - b = b * (1 - cc) + c * cc - h = hex(int(r))[2:] + hex(int(g))[2:] + hex(int(b))[2:] - return Color("#" + "0" * (6 - len(h)) + h) + return Color("#" + sha256((seed + text).encode("utf-8")).hexdigest()[-6:]) -def get_time_color(time: Optional[datetime]) -> Color: +def get_time_color(time: Optional[datetime], boundaries: MinMax) -> Color: """ Generate color based on time. """ - if time is None: - return Color("black") - delta = (datetime.now() - time).total_seconds() - time_color = hex(0xFF - min(0xFF, int(delta / 500000.)))[2:] - i_time_color = hex(min(0xFF, int(delta / 500000.)))[2:] - if len(time_color) == 1: - time_color = "0" + time_color - if len(i_time_color) == 1: - i_time_color = "0" + i_time_color - return Color("#" + time_color + "AA" + i_time_color) + return get_gradient_color(time, boundaries, TIME_COLOR_SCALE) def glue(ways: List[OSMWay]) -> List[List[OSMNode]]: @@ -224,13 +208,13 @@ class Constructor: Röntgen node and way constructor. """ def __init__( - self, check_level, mode: str, seed: str, map_, flinger: Flinger, - scheme: Scheme): + self, check_level, mode: str, seed: str, map_: Map, + flinger: Flinger, scheme: Scheme): self.check_level = check_level self.mode: str = mode self.seed: str = seed - self.map_ = map_ + self.map_: Map = map_ self.flinger: Flinger = flinger self.scheme: Scheme = scheme @@ -300,7 +284,7 @@ class Constructor: if self.mode == "time": if not way: return - time_color = get_time_color(way.timestamp) + time_color = get_time_color(way.timestamp, self.map_.time) self.ways.append( Way(inners, outers, {"fill": "none", "stroke": time_color.hex, diff --git a/roentgen/flinger.py b/roentgen/flinger.py index e0838a2..4c97e7e 100644 --- a/roentgen/flinger.py +++ b/roentgen/flinger.py @@ -1,12 +1,14 @@ """ Author: Sergey Vartanov (me@enzet.ru) + +Geo projection. """ import numpy as np from roentgen.util import MinMax -EQUATOR_LENGTH: float = 40_075_017 +EQUATOR_LENGTH: float = 40_075_017 # (in meters) def pseudo_mercator(coordinates: np.array) -> np.array: @@ -51,6 +53,8 @@ class Flinger: def fling(self, coordinates: np.array) -> np.array: """ + Convert geo coordinates into SVG position points. + :param coordinates: vector to fling """ result: np.array = self.ratio * ( @@ -63,5 +67,10 @@ class Flinger: return result def get_scale(self, coordinates: np.array) -> float: + """ + Return pixels per meter ratio for the given geo coordinates. + + :param coordinates: geo coordinates + """ scale_factor = 1 / np.cos(coordinates[0] / 180 * np.pi) return self.pixels_per_meter * scale_factor diff --git a/roentgen/util.py b/roentgen/util.py index c6429b3..89280ed 100644 --- a/roentgen/util.py +++ b/roentgen/util.py @@ -1,3 +1,5 @@ +from typing import Any, List + from colour import Color @@ -23,8 +25,12 @@ class MinMax: return self.max_ - self.min_ def center(self): + """ + Get middle point between minimum and maximum. + """ return (self.min_ + self.max_) / 2 + def is_bright(color: Color) -> bool: """ Is color bright enough to have black outline instead of white. @@ -33,3 +39,26 @@ def is_bright(color: Color) -> bool: 0.2126 * color.red * 256 + 0.7152 * color.green * 256 + 0.0722 * color.blue * 256 > 200) + + +def get_gradient_color(value: Any, bounds: MinMax, colors: List[Color]): + """ + Get color from the color scale for the value. + + :param value: given value (should be in bounds) + :param bounds: maximum and minimum values + :param colors: color scale + """ + color_length: int = len(colors) - 1 + scale = colors + [Color("black")] + + coefficient: float = ( + 0 if bounds.max_ == bounds.min_ else + (value - bounds.min_) / (bounds.max_ - bounds.min_)) + coefficient = min(1.0, max(0.0, coefficient)) + m: int = int(coefficient * color_length) + color_coefficient = (coefficient - m / color_length) * color_length + + return Color(rgb=[ + scale[m].rgb[i] + color_coefficient * + (scale[m + 1].rgb[i] - scale[m].rgb[i]) for i in range(3)])