From 4346927c3a70a5ae7bbccf88653cdb0a9dcf90fb Mon Sep 17 00:00:00 2001 From: Akkana Peck Date: Sat, 4 Nov 2023 11:03:31 -0600 Subject: [PATCH] Add the ability to colorize tracks by speed or elevation. --- pytopo/MapWindow.py | 87 ++++++++++++++++++++++++++++++++++++++----- pytopo/TrackPoints.py | 36 ++++++++++++++++-- 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/pytopo/MapWindow.py b/pytopo/MapWindow.py index 66e5ed2..3836296 100644 --- a/pytopo/MapWindow.py +++ b/pytopo/MapWindow.py @@ -43,14 +43,37 @@ from pkg_resources import resource_filename +from enum import Enum + # As of GTK3 there's no longer any HSV support, because cairo is -# solely RGB. Use colorsys instead +# solely RGB. Use colorsys instead. import colorsys import traceback GPS_MARKER_RADIUS=10 +# Track colorization styles +class TrackColor(Enum): + SEPARATE_COLORS = 1 + SPEED_COLORS = 2 + ELEVATION_COLORS = 3 + + +def is_color(color): + try: + return len(color) == 3 + except: + return False + + +def colorize_by_value(val): + """val is a float between 0 and 1. + Return a color (a triplet of floats between 0 and 1) + from a smooth gradient between blue (low) and red (high). + """ + return (val, 0., 1. - val) + class MapWindow(object): @@ -71,6 +94,12 @@ class MapWindow(object): but if you want to, contact me and I'll help you figure it out.) """ + TRACK_COLOR_VIEW_MENU = OrderedDict([ + ("Each track a different color", TrackColor.SEPARATE_COLORS), + ("By Elevation", TrackColor.ELEVATION_COLORS), + ("By Speed", TrackColor.SPEED_COLORS) + ]) + def __init__(self, _controller): """Initialize variables, but don't create the window yet.""" @@ -97,6 +126,9 @@ def __init__(self, _controller): self.selected_track = None self.selected_waypoint = None + # By default, each track is a different colors + self.track_colorize = TrackColor.SEPARATE_COLORS + # No redraws initially scheduled self.redraw_scheduled = False @@ -461,7 +493,12 @@ def draw_trackpoint_segment(self, start, linecolor, linewidth=2, Stop drawing if we reach another start string, and return the index of that string. Return None if we reach the end of the list. """ - self.cr.set_source_rgb (*linecolor) + if self.track_colorize == TrackColor.SEPARATE_COLORS and \ + is_color(linecolor): + self.cr.set_source_rgb (*linecolor) + elif self.track_colorize == TrackColor.SPEED_COLORS or\ + self.track_colorize == TrackColor.ELEVATION_COLORS: + linewidth *= 2 cur_x = None cur_y = None @@ -482,6 +519,28 @@ def draw_trackpoint_segment(self, start, linecolor, linewidth=2, if self.trackpoints.is_attributes(pt): continue + # If specified, colorize according to speed or altitude: + try: + if self.track_colorize == TrackColor.SPEED_COLORS: + speedfrac = float(pt.speed) / self.trackpoints.max_speed + linecolor = colorize_by_value(speedfrac) + self.cr.set_source_rgb (*linecolor) + + elif (self.track_colorize == TrackColor.ELEVATION_COLORS and + self.trackpoints.min_ele and self.trackpoints.max_ele): + elefrac = ((pt.ele - self.trackpoints.min_ele) / + (self.trackpoints.max_ele + - self.trackpoints.min_ele)) + linecolor = colorize_by_value(elefrac) + # print("(ele)", int(elefrac * 100), + # ": Setting color to (%.2f, %.2f, %.2f)" + # % linecolor) + self.cr.set_source_rgb (*linecolor) + + except Exception as e: + # print("can't set color:", e) + pass + x = int((pt.lon - self.center_lon) * self.collection.xscale + self.win_width / 2) y = int((self.center_lat - pt.lat) * self.collection.yscale @@ -1106,12 +1165,12 @@ def context_menu(self, event): ("Remove point from track", self.remove_trackpoint), ("Undo", self.undo), ("Save GPX or GeoJSON...", self.save_all_tracks_as), - # ("Save Area as GPX...", self.save_area_tracks_as), ("View", SEPARATOR), ("Zoom here...", self.zoom), (draw_track_label, self.toggle_track_drawing), (show_waypoint_label, self.toggle_show_waypoints), + ("Colorize tracks", None), ("Rest", SEPARATOR), ("Download Area...", self.download_area), @@ -1141,6 +1200,17 @@ def context_menu(self, event): item.set_submenu(submenu) + elif itemname == "Colorize tracks": + submenu = gtk.Menu() + for name in self.TRACK_COLOR_VIEW_MENU: + subitem = gtk.MenuItem(name) + subitem.connect("activate", self.change_track_colorize, + self.TRACK_COLOR_VIEW_MENU[name]) + submenu.append(subitem) + subitem.show() + + item.set_submenu(submenu) + elif contextmenu[itemname]: item.connect("activate", contextmenu[itemname]) @@ -1163,6 +1233,10 @@ def context_menu(self, event): # Not since the epoch. menu.popup(None, None, None, None, button, t) + def change_track_colorize(self, widget, whichcolorize): + self.track_colorize = whichcolorize + self.draw_map() + def change_collection(self, widget, name): if self.collection: savezoom = self.collection.zoomlevel @@ -1407,13 +1481,6 @@ def save_all_tracks_as(self, widget): """Prompt for a filename to save all tracks and waypoints.""" return self.save_tracks_as(widget, False) - def save_area_tracks_as(self, widget): - """Prompt for a filename to save all tracks and waypoints, - then let the user drag out an area with the mouse - and save all tracks that are completely within that area. - """ - return self.save_tracks_as(widget, True) - def save_tracks_as(self, widget, select_area=False): """Prompt for a filename to save all tracks and waypoints. Then either let the user drag out an area with the mouse diff --git a/pytopo/TrackPoints.py b/pytopo/TrackPoints.py index 588b026..2f30733 100644 --- a/pytopo/TrackPoints.py +++ b/pytopo/TrackPoints.py @@ -193,6 +193,10 @@ def __init__(self): # in case there are no track- or waypoints self.outer_bbox = BoundingBox() + self.max_speed = 0 + self.min_ele = 0 + self.max_ele = 0 + # Remember which files each set of points came from self.srcfiles = {} @@ -250,8 +254,11 @@ def attributes(self, trackindex): """ # Currently attributes are represented by a dictionary # as the next item after the name in the trackpoints list. - if self.is_attributes(self.points[trackindex + 1]): - return self.points[trackindex + 1] + try: + if self.is_attributes(self.points[trackindex + 1]): + return self.points[trackindex + 1] + except: + pass return None def handle_track_point(self, lat, lon, ele=None, speed=None, @@ -266,6 +273,20 @@ def handle_track_point(self, lat, lon, ele=None, speed=None, point = GeoPoint(lat, lon, ele=ele, speed=speed, timestamp=timestamp, name=waypoint_name, attrs=attrs) + try: + if point.ele: + if not self.min_ele or point.ele < self.min_ele: + self.min_ele = point.ele + if point.ele > self.max_ele: + self.max_ele = point.ele + except: + pass + + try: + self.max_speed = max(self.max_speed, float(speed)) + except: + pass + if waypoint_name: self.waypoints.append(point) else: @@ -429,9 +450,14 @@ def GPX_point_coords(self, pointnode): """ lat = float(pointnode.getAttribute("lat")) lon = float(pointnode.getAttribute("lon")) - ele = self.get_DOM_text(pointnode, "ele") time = self.get_DOM_text(pointnode, "time") + ele = self.get_DOM_text(pointnode, "ele") + try: + ele = float(ele) + except: + ele = 0. + # Python dom and minidom have no easy way to combine sub-nodes # into a dictionary, or to serialize them. # Also nodes are mostly undocumented. @@ -447,6 +473,10 @@ def GPX_point_coords(self, pointnode): # Old versions of osmand used a node, but now it's inside # . speed = self.get_DOM_text(pointnode, SPEEDRE) + try: + speed = float(speed) + except: + speed = None # If there were no extra attributes, pass None, not an empty dict. if not attrs: