From c8929a34ecdd32d8b3a808b39f7ff2aa00bf6640 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 10:47:19 +0200 Subject: [PATCH 01/29] examples: rename DummyLayer gdkdrawable leftover GTK2 name. cr is the widget cairo_t already passed in. --- examples/mapviewer.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 5313edc..e5e1eb0 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -38,7 +38,7 @@ assert osmgpsmap._version == "1.0" class DummyMapNoGpsPoint(osmgpsmap.Map): - def do_draw_gps_point(self, drawable): + def do_draw_gps_point(self, cr): pass @@ -49,7 +49,8 @@ class DummyLayer(GObject.GObject, osmgpsmap.MapLayer): def __init__(self): GObject.GObject.__init__(self) - def do_draw(self, gpsmap, gdkdrawable): + def do_draw(self, gpsmap, cr): + # TODO: DummyLayer claims a custom overlay but still draws nothing pass def do_render(self, gpsmap): @@ -58,7 +59,7 @@ def do_render(self, gpsmap): def do_busy(self): return False - def do_button_press(self, gpsmap, gdkeventbutton): + def do_button_press(self, gpsmap, event): return False From 868e22d6f3cfdc436bbad7625fd0cfc89491fd88 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 11:28:41 +0200 Subject: [PATCH 02/29] examples: DummyLayer paint overlay in do_draw Was pass, so the advertised custom-layer demo drew nothing. --- examples/mapviewer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index e5e1eb0..8c82584 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -17,6 +17,7 @@ along with this program; if not, see . """ +import math import random import gi @@ -50,8 +51,11 @@ def __init__(self): GObject.GObject.__init__(self) def do_draw(self, gpsmap, cr): - # TODO: DummyLayer claims a custom overlay but still draws nothing - pass + pt = osmgpsmap.point_new_degrees(-44.39, 171.25) + x, y = gpsmap.convert_geographic_to_screen(pt) + cr.set_source_rgba(1.0, 0.0, 0.0, 0.6) + cr.arc(x, y, 12, 0, 2 * math.pi) + cr.fill() def do_render(self, gpsmap): pass From ce881ca4ae4ea708e519cc506bcb784dd02c253c Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 12:14:07 +0200 Subject: [PATCH 03/29] examples: use MapPoint.new_degrees in DummyLayer point_new_degrees is the C symbol; GI exposes MapPoint.new_degrees. --- examples/mapviewer.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 8c82584..1f2e127 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -51,7 +51,8 @@ def __init__(self): GObject.GObject.__init__(self) def do_draw(self, gpsmap, cr): - pt = osmgpsmap.point_new_degrees(-44.39, 171.25) + # TODO: default view is 0,0 so this circle is off-screen until Home + pt = osmgpsmap.MapPoint.new_degrees(-44.39, 171.25) x, y = gpsmap.convert_geographic_to_screen(pt) cr.set_source_rgba(1.0, 0.0, 0.0, 0.6) cr.arc(x, y, 12, 0, 2 * math.pi) @@ -245,6 +246,7 @@ def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip, data=None): return False if self.show_tooltips: + # TODO: same GI name as DummyLayer; convert arity untested p = osmgpsmap.point_new_degrees(0.0, 0.0) self.osm.convert_screen_to_geographic(x, y, p) lat, lon = p.get_degrees() From d5abcba26202f6d19a5f7b8a55fdbc7fe28939a1 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 12:52:33 +0200 Subject: [PATCH 04/29] examples: start mapviewer.py at Home Default 0,0 hid the DummyLayer circle until Home was clicked. --- examples/mapviewer.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 1f2e127..0783a43 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -38,6 +38,12 @@ assert osmgpsmap._version == "1.0" +# Timaru. DummyLayer draws here; click Home or start here to see it. +HOME_LAT = -44.39 +HOME_LON = 171.25 +HOME_ZOOM = 12 + + class DummyMapNoGpsPoint(osmgpsmap.Map): def do_draw_gps_point(self, cr): pass @@ -51,8 +57,7 @@ def __init__(self): GObject.GObject.__init__(self) def do_draw(self, gpsmap, cr): - # TODO: default view is 0,0 so this circle is off-screen until Home - pt = osmgpsmap.MapPoint.new_degrees(-44.39, 171.25) + pt = osmgpsmap.MapPoint.new_degrees(HOME_LAT, HOME_LON) x, y = gpsmap.convert_geographic_to_screen(pt) cr.set_source_rgba(1.0, 0.0, 0.0, 0.6) cr.arc(x, y, 12, 0, 2 * math.pi) @@ -93,6 +98,7 @@ def __init__(self): ) self.osm.set_property("map-source", osmgpsmap.MapSource_t.OPENSTREETMAP) self.osm.layer_add(DummyLayer()) + self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) self.last_image = None @@ -239,7 +245,7 @@ def zoom_out_clicked(self, button): self.osm.set_zoom(self.osm.props.zoom - 1) def home_clicked(self, button): - self.osm.set_center_and_zoom(-44.39, 171.25, 12) + self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip, data=None): if keyboard_tip: @@ -297,6 +303,7 @@ def on_button_press(self, osm, event): pb = GdkPixbuf.Pixbuf.new_from_file_at_size("poi.png", 24, 24) self.last_image = self.osm.image_add(lat, lon, pb) if right: + # TODO: C mapviewer uses track_add here; Python still no-ops pass From 28f4def56d16ac1f05376490eeb91f41ea7d0f2f Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 14:22:18 +0200 Subject: [PATCH 05/29] docs: layer_draw gets widget cairo_t People were creating an ImageSurface to match the map. Do not. --- examples/mapviewer.py | 7 +++++++ src/osm-gps-map-layer.c | 10 ++++++++-- src/osm-gps-map-layer.h | 10 ++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 0783a43..8604de8 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -53,6 +53,12 @@ def do_draw_gps_point(self, cr): class DummyLayer(GObject.GObject, osmgpsmap.MapLayer): + """Cairo overlay. The map passes its cairo_t; do not create a surface. + + For paths use track_add, for markers image_add. This class is the + live-cairo case: draw in widget pixels after convert_geographic_to_screen. + """ + def __init__(self): GObject.GObject.__init__(self) @@ -64,6 +70,7 @@ def do_draw(self, gpsmap, cr): cr.fill() def do_render(self, gpsmap): + # OSD caches bitmaps here. Geo overlays paint in do_draw instead. pass def do_busy(self): diff --git a/src/osm-gps-map-layer.c b/src/osm-gps-map-layer.c index 3da68ee..91c8248 100644 --- a/src/osm-gps-map-layer.c +++ b/src/osm-gps-map-layer.c @@ -24,8 +24,14 @@ * @include: osm-gps-map.h * * #OsmGpsMapLayer is an interface implemented by objects that wish - * to draw on top of the map respond to button press events. The most - * common implementation of this interface is #OsmGpsMapOsd + * to draw on top of the map and respond to button press events. The most + * common implementation of this interface is #OsmGpsMapOsd. + * + * The map already provides a cairo context in osm_gps_map_layer_draw(). + * Convert lat/lon to widget pixels with + * osm_gps_map_convert_geographic_to_screen(). See DummyLayer in + * examples/mapviewer.py. For tracks and images use osm_gps_map_track_add() + * and osm_gps_map_image_add() instead of a custom layer. **/ #include "osm-gps-map-layer.h" diff --git a/src/osm-gps-map-layer.h b/src/osm-gps-map-layer.h index a2d248c..88fa5ad 100644 --- a/src/osm-gps-map-layer.h +++ b/src/osm-gps-map-layer.h @@ -66,7 +66,9 @@ GType osm_gps_map_layer_get_type (void); * @self: (in): a #OsmGpsMapLayer object * @map: (in): a #OsmGpsMap widget * - * Render layer on map + * Called when the offscreen map pixmap is rebuilt. Use this to update + * cached drawing (as #OsmGpsMapOsd does). Geographic overlays can leave + * this empty and paint in osm_gps_map_layer_draw() instead. * * Since: 0.6.0 **/ @@ -78,7 +80,11 @@ void osm_gps_map_layer_render (OsmGpsMapLayer *self, OsmGpsMap * @map: (in): a #OsmGpsMap widget * @cr: (in): a cairo context to draw to * - * Draw layer on map + * During #GtkWidget::draw the map passes its cairo context here. Draw in + * widget pixel coordinates. Convert geographic positions with + * osm_gps_map_convert_geographic_to_screen(). Do not create your own + * cairo surface; the map already has one. For GPS tracks and markers + * prefer osm_gps_map_track_add() and osm_gps_map_image_add(). * * Since: 0.6.0 **/ From c611bddcdcff43a2ea297aaf512caa45d294be5b Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 15:17:46 +0200 Subject: [PATCH 06/29] examples: Python right-double uses track_add Handler was pass; C demo already adds points on right-double. --- examples/mapviewer.c | 1 + examples/mapviewer.py | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 7f15eb5..64fa090 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -75,6 +75,7 @@ on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_d } if (right_button) { osm_gps_map_track_remove(map, othertrack); + /* TODO: track is gone; later add_point writes to an undrawn object */ } } else if (event->type == GDK_2BUTTON_PRESS) { if (left_button) { diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 8604de8..a721881 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -107,6 +107,8 @@ def __init__(self): self.osm.layer_add(DummyLayer()) self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) + self.click_track = osmgpsmap.MapTrack() + self.osm.track_add(self.click_track) self.last_image = None self.osm.connect("button_press_event", self.on_button_press) @@ -283,6 +285,8 @@ def on_map_change(self, osm): ) def on_button_press(self, osm, event): + # Double-click: left GPS, middle image_add, right track_add. + # Triple-click: middle remove last image, right clear the track. state = event.get_state() lat, lon = self.osm.get_event_location(event).get_degrees() @@ -303,6 +307,10 @@ def on_button_press(self, osm, event): if self.last_image is not None: self.osm.image_remove(self.last_image) self.last_image = None + if right: + self.osm.track_remove(self.click_track) + self.click_track = osmgpsmap.MapTrack() + self.osm.track_add(self.click_track) elif event.type == GDK_2BUTTON_PRESS: if left: self.osm.gps_add(lat, lon, heading=random.random() * 360) @@ -310,8 +318,8 @@ def on_button_press(self, osm, event): pb = GdkPixbuf.Pixbuf.new_from_file_at_size("poi.png", 24, 24) self.last_image = self.osm.image_add(lat, lon, pb) if right: - # TODO: C mapviewer uses track_add here; Python still no-ops - pass + pt = osmgpsmap.MapPoint.new_degrees(lat, lon) + self.click_track.add_point(pt) if __name__ == "__main__": From 0e631f393f45eb80e2de147c4c218ac4c7e151e0 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 16:03:29 +0200 Subject: [PATCH 07/29] test: MapLayer.do_draw gets widget cairo_t --- tests/test.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/tests/test.py b/tests/test.py index 49db29c..19010a6 100755 --- a/tests/test.py +++ b/tests/test.py @@ -7,7 +7,7 @@ gi.require_version('OsmGpsMap', '1.0') from gi.repository import OsmGpsMap -from gi.repository import Gdk, GdkPixbuf, Gtk +from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk class TestOsmGpsMap(unittest.TestCase): def setUp(self): @@ -62,6 +62,54 @@ def test_layer(self): osd = OsmGpsMap.MapOsd(show_zoom=True, show_coordinates=False, show_scale=False, show_dpad=True, show_gps_in_dpad=True) self.osm.layer_add(osd) self.osm.layer_remove(osd) + + def test_custom_layer_draw(self): + # MapLayer.do_draw gets the widget cairo_t; convert lat/lon in pixels. + class Layer(GObject.GObject, OsmGpsMap.MapLayer): + def __init__(self): + GObject.GObject.__init__(self) + self.drew = False + self.xy = None + + def do_draw(self, gpsmap, cr): + pt = OsmGpsMap.MapPoint.new_degrees(50.0, 13.0) + x, y = gpsmap.convert_geographic_to_screen(pt) + cr.set_source_rgba(1, 0, 0, 1) + cr.arc(x, y, 4, 0, 6.28) + cr.fill() + self.xy = (x, y) + self.drew = True + + def do_render(self, gpsmap): + pass + + def do_busy(self): + return False + + def do_button_press(self, gpsmap, event): + return False + + GObject.type_register(Layer) + layer = Layer() + window = Gtk.Window() + window.set_size_request(320, 240) + window.add(self.osm) + self.osm.layer_add(layer) + self.osm.set_center_and_zoom(50.0, 13.0, 10) + window.show_all() + + deadline = GLib.get_monotonic_time() + 500000 + while GLib.get_monotonic_time() < deadline and not layer.drew: + Gtk.main_iteration_do(False) + + self.assertTrue(layer.drew) + self.assertIsNotNone(layer.xy) + alloc = self.osm.get_allocation() + self.assertAlmostEqual(layer.xy[0], alloc.width / 2, delta=40) + self.assertAlmostEqual(layer.xy[1], alloc.height / 2, delta=40) + self.osm.layer_remove(layer) + window.remove(self.osm) + window.destroy() def test_image(self): size = 16 From 3516601918aad9f4449ac5bf17b2af0e396e1613 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 16:48:11 +0200 Subject: [PATCH 08/29] examples: polygon.c points via new_degrees Radians were unexplained magic. Also ship mapviewer.py in dist. --- examples/Makefile.am | 2 +- examples/editable_track.c | 1 + examples/polygon.c | 6 +++--- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/Makefile.am b/examples/Makefile.am index f51846d..df176ec 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -63,5 +63,5 @@ editable_track_LDADD = \ $(top_builddir)/src/libosmgpsmap-1.0.la ## Misc -EXTRA_DIST = poi.png mapviewer.ui mapviewer.js README +EXTRA_DIST = poi.png mapviewer.ui mapviewer.py mapviewer.js README diff --git a/examples/editable_track.c b/examples/editable_track.c index 07613aa..f5e35e2 100644 --- a/examples/editable_track.c +++ b/examples/editable_track.c @@ -21,6 +21,7 @@ main (int argc, OsmGpsMapTrack* track = osm_gps_map_track_new(); OsmGpsMapPoint* p1, *p2; + /* TODO: same unexplained radians as polygon.c */ p1 = osm_gps_map_point_new_radians(1.25663706, -0.488692191); p2 = osm_gps_map_point_new_radians(1.06465084, -0.750491578); diff --git a/examples/polygon.c b/examples/polygon.c index b8d1e89..ead7cdc 100644 --- a/examples/polygon.c +++ b/examples/polygon.c @@ -23,9 +23,9 @@ main (int argc, OsmGpsMapTrack* polytrack = osm_gps_map_track_new(); OsmGpsMapPoint* p1, *p2, *p3; - p1 = osm_gps_map_point_new_radians(1.25663706, -0.488692191); - p2 = osm_gps_map_point_new_radians(1.06465084, -0.750491578); - p3 = osm_gps_map_point_new_radians(1.064650849, -0.191986218); + p1 = osm_gps_map_point_new_degrees(72.0, -28.0); + p2 = osm_gps_map_point_new_degrees(61.0, -43.0); + p3 = osm_gps_map_point_new_degrees(61.0, -11.0); osm_gps_map_track_add_point(polytrack, p1); osm_gps_map_track_add_point(polytrack, p2); From d98325313bab054b9d8a78586b3cd2dc506364a5 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 17:31:44 +0200 Subject: [PATCH 09/29] examples: editable_track.c uses new_degrees --- examples/editable_track.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/editable_track.c b/examples/editable_track.c index f5e35e2..48222a4 100644 --- a/examples/editable_track.c +++ b/examples/editable_track.c @@ -21,9 +21,8 @@ main (int argc, OsmGpsMapTrack* track = osm_gps_map_track_new(); OsmGpsMapPoint* p1, *p2; - /* TODO: same unexplained radians as polygon.c */ - p1 = osm_gps_map_point_new_radians(1.25663706, -0.488692191); - p2 = osm_gps_map_point_new_radians(1.06465084, -0.750491578); + p1 = osm_gps_map_point_new_degrees(72.0, -28.0); + p2 = osm_gps_map_point_new_degrees(61.0, -43.0); osm_gps_map_track_add_point(track, p1); osm_gps_map_track_add_point(track, p2); From 287fe04e9e14af3622041e0adc7be269fde28f52 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 18:22:08 +0200 Subject: [PATCH 10/29] examples: stop printing osmgpsmap.__file__ GI module has no __file__; demo died before DummyLayer ran. --- examples/mapviewer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index a721881..d3a1b3c 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -34,7 +34,7 @@ OsmGpsMap as osmgpsmap, ) # noqa -print(f"using library: {osmgpsmap.__file__} (version {osmgpsmap._version})") +print(f"using {osmgpsmap} (version {osmgpsmap._version})") assert osmgpsmap._version == "1.0" From 0aa0c28fc085b1d20ea01c29baaa6714a1bc65e0 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 19:21:36 +0200 Subject: [PATCH 11/29] examples: image_add from cairo pixbuf Demo only loaded poi.png; #71 asked for images built at runtime. --- examples/mapviewer.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index d3a1b3c..0c7d7eb 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -19,6 +19,8 @@ import math import random + +import cairo import gi gi.require_version("Gdk", "3.0") @@ -315,12 +317,23 @@ def on_button_press(self, osm, event): if left: self.osm.gps_add(lat, lon, heading=random.random() * 360) if middle: - pb = GdkPixbuf.Pixbuf.new_from_file_at_size("poi.png", 24, 24) - self.last_image = self.osm.image_add(lat, lon, pb) + self.last_image = self.osm.image_add( + lat, lon, self._marker_pixbuf() + ) if right: pt = osmgpsmap.MapPoint.new_degrees(lat, lon) self.click_track.add_point(pt) + def _marker_pixbuf(self): + # TODO: poi.png unused; this is the programmatic image_add path + size = 24 + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) + cr = cairo.Context(surface) + cr.set_source_rgba(0.1, 0.4, 0.9, 0.9) + cr.arc(size / 2, size / 2, size / 2 - 2, 0, 2 * math.pi) + cr.fill() + return Gdk.pixbuf_get_from_surface(surface, 0, 0, size, size) + if __name__ == "__main__": u = UI() From e0269dfafec14b9a138214aaa010f2c5addf4b23 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 22 Aug 2026 20:41:17 +0200 Subject: [PATCH 12/29] docs: README how to draw on the map DummyLayer, track_add, image_add, gps_add were not documented as a set. --- examples/README | 44 +++++++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/examples/README b/examples/README index 1ccb183..eb1f373 100644 --- a/examples/README +++ b/examples/README @@ -1,17 +1,39 @@ osm-gps-map example applications +================================ + +All demos must be run from this directory so they find mapviewer.ui and poi.png. + +If running uninstalled, you probably need to tell GObject introspection +where to find the bindings. From this directory: + + export LD_LIBRARY_PATH=../src/.libs/ + export GI_TYPELIB_PATH=../src/ + +Applications +------------ * ./mapviewer - C based demo app with many options. See '--help' for a list. Note, this - must be executed from the examples directory so it can find the mapviewer.ui - file. + C based demo app with many options. See '--help' for a list. + --editable-tracks makes the right-click track draggable. * ./polygon - This example demonstrates editable polygons. Vertex points can be dragged. + Editable polygons. Vertex points can be dragged. Clicking mid-points divides the line and creates another vertex. + * ./editable_track + One editable track. Uses osm_gps_map_point_new_degrees like polygon. * ./mapviewer.py - Python version of the C demo app, with examples showing how to do custom - layers. + Python version of the C demo. DummyLayer shows a cairo overlay. * ./mapviewer.js - Javascript example using seed + gobject-introspection. If running uninstalled, - you probbably need to tell GObject introspection where to find the - bindings. Do the following from the example directory - # export LD_LIBRARY_PATH=../src/.libs/ - # export GI_TYPELIB_PATH=../src/ + Javascript example using seed + gobject-introspection. + +Drawing on the map +------------------ +Do not create a cairo ImageSurface matching the map. The widget already +has a cairo_t and passes it to OsmGpsMapLayer.do_draw. + + * Live cairo: DummyLayer in mapviewer.py paints a red circle at Home + (Timaru) via convert_geographic_to_screen. + * Paths: double-right-click (or Ctrl+double-left) uses track_add. + Triple-right clears the track. C currently does not put a new track + back on the map after remove. + * Images: double-middle-click (or Shift+double-left) uses image_add. + Python paints a cairo circle into a GdkPixbuf; C loads poi.png. + * GPS: double-left-click calls gps_add (blue ball + random heading). From 806d840a2a1605f06f0d802d4f3c3a46f4ebcf96 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 11:49:22 +0200 Subject: [PATCH 13/29] test: GI convert_screen_to_geographic arity Out-arg is returned, not passed in. Tooltip still uses the C calling style. --- tests/test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test.py b/tests/test.py index 19010a6..e0d8b29 100755 --- a/tests/test.py +++ b/tests/test.py @@ -177,6 +177,14 @@ def test_insert_point(self): track.insert_point(point, 0) self.assertEqual(track.n_points(), 1) + def test_convert_screen_to_geographic(self): + # GI returns the MapPoint; do not pass one in. + pt = self.osm.convert_screen_to_geographic(0, 0) + self.assertEqual(type(pt), OsmGpsMap.MapPoint) + lat, lon = pt.get_degrees() + self.assertTrue(-90.0 <= lat <= 90.0) + self.assertTrue(-180.0 <= lon <= 180.0) + def test_zoom_fit_bbox_point(self): # Degenerate bbox (one geotag). Must not crash; zoom clamps to max. self.osm.zoom_fit_bbox(self.lat, self.lat, self.lon, self.lon) From c85baf2dc0f1441e530bd03e5c6adaf367d02f9e Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 12:18:07 +0200 Subject: [PATCH 14/29] examples: fix tooltip convert_screen_to_geographic GI takes x,y and returns MapPoint; passing a point was TypeError. --- examples/mapviewer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 0c7d7eb..d603b46 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -263,9 +263,8 @@ def on_query_tooltip(self, widget, x, y, keyboard_tip, tooltip, data=None): return False if self.show_tooltips: - # TODO: same GI name as DummyLayer; convert arity untested - p = osmgpsmap.point_new_degrees(0.0, 0.0) - self.osm.convert_screen_to_geographic(x, y, p) + # GI treats the MapPoint as an out-arg, so it is returned. + p = self.osm.convert_screen_to_geographic(x, y) lat, lon = p.get_degrees() tooltip.set_markup(f"{lat:+.4f}, {lon:+.4f}") return True From 0a844a948b1c89b535b61013d362b1b74d095dbb Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 16:31:14 +0200 Subject: [PATCH 15/29] examples: C triple-right recreates click track track_remove dropped it; later add_point wrote to an undrawn object. --- examples/mapviewer.c | 33 ++++++++++++++++++++------------- examples/mapviewer.py | 1 + 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 64fa090..104ded2 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -52,16 +52,26 @@ static GOptionEntry debug_entries[] = static GdkPixbuf *g_star_image = NULL; static OsmGpsMapImage *g_last_image = NULL; +static OsmGpsMapTrack *g_click_track = NULL; + +static OsmGpsMapTrack * +click_track_new (void) +{ + OsmGpsMapTrack *track = osm_gps_map_track_new (); + if (opt_editable_tracks) + g_object_set (track, "editable", TRUE, NULL); + return track; +} static gboolean -on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_data) +on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_data G_GNUC_UNUSED) { OsmGpsMapPoint coord; float lat, lon; OsmGpsMap *map = OSM_GPS_MAP(widget); - OsmGpsMapTrack *othertrack = OSM_GPS_MAP_TRACK(user_data); int left_button = (event->button == 1) && (event->state == 0); + /* TODO: 2BUTTON_PRESS often has BUTTON1_MASK so left gps_add never runs */ int middle_button = (event->button == 2) || ((event->button == 1) && (event->state & GDK_SHIFT_MASK)); int right_button = (event->button == 3) || ((event->button == 1) && (event->state & GDK_CONTROL_MASK)); @@ -74,8 +84,10 @@ on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_d osm_gps_map_image_remove (map, g_last_image); } if (right_button) { - osm_gps_map_track_remove(map, othertrack); - /* TODO: track is gone; later add_point writes to an undrawn object */ + osm_gps_map_track_remove (map, g_click_track); + g_object_unref (g_click_track); + g_click_track = click_track_new (); + osm_gps_map_track_add (map, g_click_track); } } else if (event->type == GDK_2BUTTON_PRESS) { if (left_button) { @@ -91,7 +103,7 @@ on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_d g_star_image); } if (right_button) { - osm_gps_map_track_add_point(othertrack, &coord); + osm_gps_map_track_add_point(g_click_track, &coord); } } @@ -248,7 +260,6 @@ main (int argc, char **argv) GtkAccelGroup *ag; OsmGpsMap *map; OsmGpsMapLayer *osd; - OsmGpsMapTrack *rightclicktrack; const char *repo_uri; char *cachedir, *cachebasedir; GError *error = NULL; @@ -319,12 +330,8 @@ main (int argc, char **argv) osm_gps_map_layer_add(OSM_GPS_MAP(map), osd); g_object_unref(G_OBJECT(osd)); - //Add a second track for right clicks - rightclicktrack = osm_gps_map_track_new(); - - if(opt_editable_tracks) - g_object_set(rightclicktrack, "editable", TRUE, NULL); - osm_gps_map_track_add(OSM_GPS_MAP(map), rightclicktrack); + g_click_track = click_track_new (); + osm_gps_map_track_add (OSM_GPS_MAP (map), g_click_track); g_free(cachedir); g_free(cachebasedir); @@ -405,7 +412,7 @@ main (int argc, char **argv) gtk_builder_get_object(builder, "star_yalign_adjustment"), "value-changed", G_CALLBACK (on_star_align_changed), (gpointer) "y-align"); g_signal_connect (G_OBJECT (map), "button-press-event", - G_CALLBACK (on_button_press_event), (gpointer) rightclicktrack); + G_CALLBACK (on_button_press_event), NULL); g_signal_connect (G_OBJECT (map), "changed", G_CALLBACK (on_map_changed_event), (gpointer) gtk_builder_get_object(builder, "text_entry")); diff --git a/examples/mapviewer.py b/examples/mapviewer.py index d603b46..4710a96 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -291,6 +291,7 @@ def on_button_press(self, osm, event): state = event.get_state() lat, lon = self.osm.get_event_location(event).get_degrees() + # TODO: 2BUTTON_PRESS often has BUTTON1_MASK so gps_add never runs left = event.button == 1 and state == 0 middle = event.button == 2 or ( event.button == 1 and state & Gdk.ModifierType.SHIFT_MASK From 165c02ae402879b88e262533556cf691639d1746 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 17:14:52 +0200 Subject: [PATCH 16/29] examples: ignore button masks on double-click 2BUTTON_PRESS often has BUTTON1_MASK so gps_add never ran. --- examples/mapviewer.c | 7 +++++-- examples/mapviewer.py | 10 ++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 104ded2..c9c7dcb 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -69,9 +69,10 @@ on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_d OsmGpsMapPoint coord; float lat, lon; OsmGpsMap *map = OSM_GPS_MAP(widget); + /* 2BUTTON_PRESS often has BUTTONn_MASK set; ignore those, keep Shift/Ctrl. */ + GdkModifierType mods = event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK); - int left_button = (event->button == 1) && (event->state == 0); - /* TODO: 2BUTTON_PRESS often has BUTTON1_MASK so left gps_add never runs */ + int left_button = (event->button == 1) && (mods == 0); int middle_button = (event->button == 2) || ((event->button == 1) && (event->state & GDK_SHIFT_MASK)); int right_button = (event->button == 3) || ((event->button == 1) && (event->state & GDK_CONTROL_MASK)); @@ -330,6 +331,8 @@ main (int argc, char **argv) osm_gps_map_layer_add(OSM_GPS_MAP(map), osd); g_object_unref(G_OBJECT(osd)); + /* TODO: auto-center jumps gps_add under the OSD crosshair */ + g_click_track = click_track_new (); osm_gps_map_track_add (OSM_GPS_MAP (map), g_click_track); diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 4710a96..2768e40 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -108,6 +108,7 @@ def __init__(self): self.osm.set_property("map-source", osmgpsmap.MapSource_t.OPENSTREETMAP) self.osm.layer_add(DummyLayer()) self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) + # TODO: auto-center jumps gps_add under the OSD crosshair self.click_track = osmgpsmap.MapTrack() self.osm.track_add(self.click_track) @@ -291,8 +292,13 @@ def on_button_press(self, osm, event): state = event.get_state() lat, lon = self.osm.get_event_location(event).get_degrees() - # TODO: 2BUTTON_PRESS often has BUTTON1_MASK so gps_add never runs - left = event.button == 1 and state == 0 + # 2BUTTON_PRESS often has BUTTONn_MASK set; ignore those, keep Shift/Ctrl. + mods = state & ( + Gdk.ModifierType.SHIFT_MASK + | Gdk.ModifierType.CONTROL_MASK + | Gdk.ModifierType.MOD1_MASK + ) + left = event.button == 1 and mods == 0 middle = event.button == 2 or ( event.button == 1 and state & Gdk.ModifierType.SHIFT_MASK ) From 437a930533a23a68fd38d87a5c6f79314afea8f5 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 17:51:27 +0200 Subject: [PATCH 17/29] examples: disable auto-center in mapviewers gps_add jumped the blob under the OSD crosshair. --- examples/mapviewer.c | 3 ++- examples/mapviewer.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index c9c7dcb..b81bc45 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -331,7 +331,8 @@ main (int argc, char **argv) osm_gps_map_layer_add(OSM_GPS_MAP(map), osd); g_object_unref(G_OBJECT(osd)); - /* TODO: auto-center jumps gps_add under the OSD crosshair */ + /* Stay put on gps_add so the blue blob is not under the OSD crosshair. */ + g_object_set (map, "auto-center", FALSE, NULL); g_click_track = click_track_new (); osm_gps_map_track_add (OSM_GPS_MAP (map), g_click_track); diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 2768e40..8737ada 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -108,7 +108,8 @@ def __init__(self): self.osm.set_property("map-source", osmgpsmap.MapSource_t.OPENSTREETMAP) self.osm.layer_add(DummyLayer()) self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) - # TODO: auto-center jumps gps_add under the OSD crosshair + # Stay put on gps_add so the blue blob is not under the OSD crosshair. + self.osm.props.auto_center = False self.click_track = osmgpsmap.MapTrack() self.osm.track_add(self.click_track) From ac58f06b3ecc6cd5640bf6088779cbb7bcdfe7c4 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 18:33:09 +0200 Subject: [PATCH 18/29] examples: poi.png or cairo star for markers Blue circle hid poi.png. Uncheck the box for the programmatic pixbuf path. --- examples/mapviewer.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 8737ada..8f3838e 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -114,6 +114,7 @@ def __init__(self): self.click_track = osmgpsmap.MapTrack() self.osm.track_add(self.click_track) self.last_image = None + self.use_poi_png = True self.osm.connect("button_press_event", self.on_button_press) self.osm.connect("changed", self.on_map_change) @@ -210,6 +211,11 @@ def __init__(self): cb.connect("toggled", self.on_show_tooltips_toggled) self.vbox.pack_end(cb, False, True, 0) + cb = Gtk.CheckButton(label="Use poi.png for markers") + cb.props.active = self.use_poi_png + cb.connect("toggled", self.on_use_poi_png_toggled) + self.vbox.pack_end(cb, False, True, 0) + cb = Gtk.CheckButton(label="Disable Cache") cb.props.active = False cb.connect("toggled", self.disable_cache_toggled) @@ -230,6 +236,9 @@ def disable_cache_toggled(self, btn): def on_show_tooltips_toggled(self, btn): self.show_tooltips = btn.props.active + def on_use_poi_png_toggled(self, btn): + self.use_poi_png = btn.props.active + def load_map_clicked(self, button): uri = self.repouri_entry.get_text() format = self.image_format_entry.get_text() @@ -332,13 +341,35 @@ def on_button_press(self, osm, event): self.click_track.add_point(pt) def _marker_pixbuf(self): - # TODO: poi.png unused; this is the programmatic image_add path - size = 24 + if self.use_poi_png: + try: + return GdkPixbuf.Pixbuf.new_from_file_at_size("poi.png", 24, 24) + except GLib.Error as err: + print("poi.png:", err) + return self._star_pixbuf() + + def _star_pixbuf(self, size=24): + # Programmatic image: cairo star -> pixbuf -> image_add. + # Live cairo on the map is DummyLayer, not this. surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) cr = cairo.Context(surface) - cr.set_source_rgba(0.1, 0.4, 0.9, 0.9) - cr.arc(size / 2, size / 2, size / 2 - 2, 0, 2 * math.pi) - cr.fill() + cx = cy = size / 2.0 + outer, inner = size / 2.0 - 1.0, size / 5.0 + for i in range(10): + r = outer if i % 2 == 0 else inner + ang = -math.pi / 2.0 + i * math.pi / 5.0 + x = cx + r * math.cos(ang) + y = cy + r * math.sin(ang) + if i == 0: + cr.move_to(x, y) + else: + cr.line_to(x, y) + cr.close_path() + cr.set_source_rgb(1.0, 0.85, 0.0) + cr.fill_preserve() + cr.set_source_rgb(0.75, 0.45, 0.0) + cr.set_line_width(1) + cr.stroke() return Gdk.pixbuf_get_from_surface(surface, 0, 0, size, size) From d302aec66cb70bace91f78226effd1d2a6f5d6a4 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sun, 23 Aug 2026 19:12:41 +0200 Subject: [PATCH 19/29] docs: README drawing how-to from examples/ --- examples/README | 43 +++++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/examples/README b/examples/README index eb1f373..3910d1f 100644 --- a/examples/README +++ b/examples/README @@ -1,39 +1,38 @@ osm-gps-map example applications -================================ -All demos must be run from this directory so they find mapviewer.ui and poi.png. +Run C and Python demos from this directory (examples) so they find mapviewer.ui +and poi.png. -If running uninstalled, you probably need to tell GObject introspection -where to find the bindings. From this directory: +If running uninstalled, you probbably need to tell GObject introspection where +to find the bindings. Do the following from the example directory: export LD_LIBRARY_PATH=../src/.libs/ export GI_TYPELIB_PATH=../src/ -Applications ------------- * ./mapviewer - C based demo app with many options. See '--help' for a list. + C based demo app with many options. See '--help' for a list of options. --editable-tracks makes the right-click track draggable. * ./polygon - Editable polygons. Vertex points can be dragged. + This example demonstrates editable polygons. Vertices can be dragged. Clicking mid-points divides the line and creates another vertex. * ./editable_track - One editable track. Uses osm_gps_map_point_new_degrees like polygon. + One editable track. Same degree constructor as polygon. * ./mapviewer.py - Python version of the C demo. DummyLayer shows a cairo overlay. + Python version of the C demo app, with examples showing how to do custom + layers: DummyLayer class for cairo overlay, GPS, programmatic image_add, + and track_add. * ./mapviewer.js Javascript example using seed + gobject-introspection. -Drawing on the map ------------------- -Do not create a cairo ImageSurface matching the map. The widget already -has a cairo_t and passes it to OsmGpsMapLayer.do_draw. +Examples for drawing on the map: - * Live cairo: DummyLayer in mapviewer.py paints a red circle at Home - (Timaru) via convert_geographic_to_screen. - * Paths: double-right-click (or Ctrl+double-left) uses track_add. - Triple-right clears the track. C currently does not put a new track - back on the map after remove. - * Images: double-middle-click (or Shift+double-left) uses image_add. - Python paints a cairo circle into a GdkPixbuf; C loads poi.png. - * GPS: double-left-click calls gps_add (blue ball + random heading). + * Double-left-click places a GPS point (blue ball+random heading). + * DummyLayer in mapviewer.py uses live cairo to draw red circle + at Home location (Timaru) + * Double-right-click (or Ctrl+double-left) adds points to tracks. + Triple right click clears the track (a new empty track stays on the map). + This shows the use of track_add, add_point and track_remove + * Double-middle-click (or Shift+double-left) adds a marker via image_add. + Python: poi.png by default, or uncheck "Use poi.png for markers" to + paint a cairo star (programmatic pixbuf). Triple middle click removes + the last image. From da7b1fe19525f1ad399b3dbff8d6760845bb3489 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Mon, 24 Aug 2026 10:40:24 +0200 Subject: [PATCH 20/29] docs: fix typos in examples and layer header --- examples/README | 2 +- examples/mapviewer.c | 2 +- examples/mapviewer.js | 2 +- examples/mapviewer.py | 2 +- src/osm-gps-map-layer.h | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/README b/examples/README index 3910d1f..3a04387 100644 --- a/examples/README +++ b/examples/README @@ -3,7 +3,7 @@ osm-gps-map example applications Run C and Python demos from this directory (examples) so they find mapviewer.ui and poi.png. -If running uninstalled, you probbably need to tell GObject introspection where +If running uninstalled, you probably need to tell GObject introspection where to find the bindings. Do the following from the example directory: export LD_LIBRARY_PATH=../src/.libs/ diff --git a/examples/mapviewer.c b/examples/mapviewer.c index b81bc45..c83c30b 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -119,7 +119,7 @@ on_map_changed_event (GtkWidget *widget, gpointer user_data) OsmGpsMap *map = OSM_GPS_MAP(widget); g_object_get(map, "latitude", &lat, "longitude", &lon, NULL); - gchar *msg = g_strdup_printf("Map Centre: lattitude %f longitude %f",lat,lon); + gchar *msg = g_strdup_printf("Map Centre: latitude %f longitude %f",lat,lon); gtk_entry_set_text(entry, msg); g_free(msg); diff --git a/examples/mapviewer.js b/examples/mapviewer.js index 71958b4..4a860c6 100755 --- a/examples/mapviewer.js +++ b/examples/mapviewer.js @@ -1,6 +1,6 @@ #!/usr/bin/seed -// You probbably need to tell GObject introspection where to find the bindings +// You probably need to tell GObject introspection where to find the bindings // export LD_LIBRARY_PATH=../src/.libs/ // export GI_TYPELIB_PATH=../src/ diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 8f3838e..f84cb92 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -181,7 +181,7 @@ def __init__(self): \t#S\tInverse zoom (max-zoom - #Z) \t#Q\tQuadtree encoded tile (qrts) \t#W\tQuadtree encoded tile (1234) -\t#U\tEncoding not implemeted +\t#U\tEncoding not implemented \t#R\tRandom integer, 0-4""" ) lbl.props.xalign = 0 diff --git a/src/osm-gps-map-layer.h b/src/osm-gps-map-layer.h index 88fa5ad..80aa048 100644 --- a/src/osm-gps-map-layer.h +++ b/src/osm-gps-map-layer.h @@ -109,7 +109,7 @@ gboolean osm_gps_map_layer_busy (OsmGpsMapLayer *self); * * Handle button event * - * Returns: whether even had been handled + * Returns: whether the event had been handled * Since: 0.6.0 **/ gboolean osm_gps_map_layer_button_press (OsmGpsMapLayer *self, OsmGpsMap *map, GdkEventButton *event); From 57837b22bc33dd4e6cc58c5410140ef5a1756657 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Tue, 25 Aug 2026 08:33:56 +0200 Subject: [PATCH 21/29] examples: set mapviewer.py user-agent OSMF tile policy. Match C demo's "mapviewer.c". --- examples/mapviewer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index f84cb92..2a125eb 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -99,7 +99,7 @@ def __init__(self): if 0: self.osm = DummyMapNoGpsPoint() else: - self.osm = osmgpsmap.Map() + self.osm = osmgpsmap.Map(user_agent="mapviewer.py") self.osm.layer_add( osmgpsmap.MapOsd(show_dpad=True, show_zoom=True, @@ -247,10 +247,12 @@ def load_map_clicked(self, button): # remove old map self.vbox.remove(self.osm) try: - self.osm = osmgpsmap.Map(repo_uri=uri, image_format=format) + self.osm = osmgpsmap.Map( + repo_uri=uri, image_format=format, user_agent="mapviewer.py" + ) except Exception as e: print("ERROR:", e) - self.osm = osmgpsmap.Map() + self.osm = osmgpsmap.Map(user_agent="mapviewer.py") self.vbox.pack_start(self.osm, True, True, 0) self.osm.show() From 0e9bf62cdd6bbf2ac1a2549b91c458e98563a695 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Tue, 25 Aug 2026 08:39:22 +0200 Subject: [PATCH 22/29] examples: sync C Home with Python Timaru Overlay Home is Timaru. Old C start 0,0 / Christchurch would hide it. --- examples/mapviewer.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index c83c30b..2c0d07c 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -50,6 +50,11 @@ static GOptionEntry debug_entries[] = }; #endif +/* Shared with mapviewer.py DummyLayer Home (Timaru). */ +#define HOME_LAT -44.39 +#define HOME_LON 171.25 +#define HOME_ZOOM 12 + static GdkPixbuf *g_star_image = NULL; static OsmGpsMapImage *g_last_image = NULL; static OsmGpsMapTrack *g_click_track = NULL; @@ -150,7 +155,7 @@ static gboolean on_home_clicked_event (GtkWidget *widget, gpointer user_data) { OsmGpsMap *map = OSM_GPS_MAP(user_data); - osm_gps_map_set_center_and_zoom(map, -43.5326,172.6362,12); + osm_gps_map_set_center_and_zoom(map, HOME_LAT, HOME_LON, HOME_ZOOM); return FALSE; } @@ -336,6 +341,7 @@ main (int argc, char **argv) g_click_track = click_track_new (); osm_gps_map_track_add (OSM_GPS_MAP (map), g_click_track); + osm_gps_map_set_center_and_zoom (map, HOME_LAT, HOME_LON, HOME_ZOOM); g_free(cachedir); g_free(cachebasedir); From 1f78e76488658c4805e6d015de3708f7e1bc78b8 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Tue, 25 Aug 2026 09:03:17 +0200 Subject: [PATCH 23/29] examples: NULL g_last_image after remove --- examples/mapviewer.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 2c0d07c..3ab6590 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -86,8 +86,10 @@ on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_d if (event->type == GDK_3BUTTON_PRESS) { if (middle_button) { - if (g_last_image) + if (g_last_image) { osm_gps_map_image_remove (map, g_last_image); + g_last_image = NULL; + } } if (right_button) { osm_gps_map_track_remove (map, g_click_track); From 121fe6c05c4cf32828f5c40606f8d0c8a1117b66 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Thu, 27 Aug 2026 08:53:14 +0200 Subject: [PATCH 24/29] examples: rename DummyLayer to DrawLayer --- examples/README | 7 ++++--- examples/mapviewer.py | 17 +++++++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/examples/README b/examples/README index 3a04387..411a58b 100644 --- a/examples/README +++ b/examples/README @@ -19,7 +19,7 @@ to find the bindings. Do the following from the example directory: One editable track. Same degree constructor as polygon. * ./mapviewer.py Python version of the C demo app, with examples showing how to do custom - layers: DummyLayer class for cairo overlay, GPS, programmatic image_add, + layers: DrawLayer class for cairo overlay, GPS, programmatic image_add, and track_add. * ./mapviewer.js Javascript example using seed + gobject-introspection. @@ -27,8 +27,9 @@ to find the bindings. Do the following from the example directory: Examples for drawing on the map: * Double-left-click places a GPS point (blue ball+random heading). - * DummyLayer in mapviewer.py uses live cairo to draw red circle - at Home location (Timaru) + * DrawLayer in mapviewer.py uses live cairo to draw a red house + at Home location (Timaru). Peak of the roof is at the lat/lon of + Home location. * Double-right-click (or Ctrl+double-left) adds points to tracks. Triple right click clears the track (a new empty track stays on the map). This shows the use of track_add, add_point and track_remove diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 2a125eb..467cfac 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -40,7 +40,7 @@ assert osmgpsmap._version == "1.0" -# Timaru. DummyLayer draws here; click Home or start here to see it. +# Timaru. DrawLayer paints a house here; click Home or start here to see it. HOME_LAT = -44.39 HOME_LON = 171.25 HOME_ZOOM = 12 @@ -54,7 +54,7 @@ def do_draw_gps_point(self, cr): GObject.type_register(DummyMapNoGpsPoint) -class DummyLayer(GObject.GObject, osmgpsmap.MapLayer): +class DrawLayer(GObject.GObject, osmgpsmap.MapLayer): """Cairo overlay. The map passes its cairo_t; do not create a surface. For paths use track_add, for markers image_add. This class is the @@ -68,7 +68,12 @@ def do_draw(self, gpsmap, cr): pt = osmgpsmap.MapPoint.new_degrees(HOME_LAT, HOME_LON) x, y = gpsmap.convert_geographic_to_screen(pt) cr.set_source_rgba(1.0, 0.0, 0.0, 0.6) - cr.arc(x, y, 12, 0, 2 * math.pi) + cr.move_to(x, y) + cr.line_to(x + 12, y + 10) + cr.line_to(x + 12, y + 24) + cr.line_to(x - 12, y + 24) + cr.line_to(x - 12, y + 10) + cr.close_path() cr.fill() def do_render(self, gpsmap): @@ -82,7 +87,7 @@ def do_button_press(self, gpsmap, event): return False -GObject.type_register(DummyLayer) +GObject.type_register(DrawLayer) class UI(Gtk.Window): @@ -106,7 +111,7 @@ def __init__(self): show_crosshair=True) ) self.osm.set_property("map-source", osmgpsmap.MapSource_t.OPENSTREETMAP) - self.osm.layer_add(DummyLayer()) + self.osm.layer_add(DrawLayer()) self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) # Stay put on gps_add so the blue blob is not under the OSD crosshair. self.osm.props.auto_center = False @@ -352,7 +357,7 @@ def _marker_pixbuf(self): def _star_pixbuf(self, size=24): # Programmatic image: cairo star -> pixbuf -> image_add. - # Live cairo on the map is DummyLayer, not this. + # Live cairo on the map is DrawLayer, not this. surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) cr = cairo.Context(surface) cx = cy = size / 2.0 From c582a58d4c5cd69b77a95aa247d4b37c0bc98df8 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Fri, 28 Aug 2026 09:34:28 +0200 Subject: [PATCH 25/29] examples: add DrawLayer to mapviewer.c C demo had no live cairo overlay. Match the Python house at Home so both mapviewers show the same how-to. --- examples/README | 2 +- examples/mapviewer.c | 82 ++++++++++++++++++++++++++++++++++++++++- src/osm-gps-map-layer.c | 7 ++-- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/examples/README b/examples/README index 411a58b..5d56ab7 100644 --- a/examples/README +++ b/examples/README @@ -27,7 +27,7 @@ to find the bindings. Do the following from the example directory: Examples for drawing on the map: * Double-left-click places a GPS point (blue ball+random heading). - * DrawLayer in mapviewer.py uses live cairo to draw a red house + * DrawLayer in mapviewer and mapviewer.py uses live cairo to draw a red house at Home location (Timaru). Peak of the roof is at the lat/lon of Home location. * Double-right-click (or Ctrl+double-left) adds points to tracks. diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 3ab6590..4cef7fc 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -50,11 +50,85 @@ static GOptionEntry debug_entries[] = }; #endif -/* Shared with mapviewer.py DummyLayer Home (Timaru). */ +/* Shared with mapviewer.py DrawLayer Home (Timaru). */ #define HOME_LAT -44.39 #define HOME_LON 171.25 #define HOME_ZOOM 12 +typedef struct _DrawLayer { + GObject parent; +} DrawLayer; + +typedef struct _DrawLayerClass { + GObjectClass parent_class; +} DrawLayerClass; + +static void draw_layer_interface_init (OsmGpsMapLayerIface *iface); + +G_DEFINE_TYPE_WITH_CODE (DrawLayer, draw_layer, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (OSM_TYPE_GPS_MAP_LAYER, + draw_layer_interface_init)); + +static void +draw_layer_draw (OsmGpsMapLayer *layer G_GNUC_UNUSED, + OsmGpsMap *map, cairo_t *cr) +{ + OsmGpsMapPoint *pt; + gint x, y; + + pt = osm_gps_map_point_new_degrees (HOME_LAT, HOME_LON); + osm_gps_map_convert_geographic_to_screen (map, pt, &x, &y); + osm_gps_map_point_free (pt); + + cairo_set_source_rgba (cr, 1.0, 0.0, 0.0, 0.6); + cairo_move_to (cr, x, y); + cairo_line_to (cr, x + 12, y + 10); + cairo_line_to (cr, x + 12, y + 24); + cairo_line_to (cr, x - 12, y + 24); + cairo_line_to (cr, x - 12, y + 10); + cairo_close_path (cr); + cairo_fill (cr); +} + +static void +draw_layer_render (OsmGpsMapLayer *layer G_GNUC_UNUSED, + OsmGpsMap *map G_GNUC_UNUSED) +{ +} + +static gboolean +draw_layer_busy (OsmGpsMapLayer *layer G_GNUC_UNUSED) +{ + return FALSE; +} + +static gboolean +draw_layer_button_press (OsmGpsMapLayer *layer G_GNUC_UNUSED, + OsmGpsMap *map G_GNUC_UNUSED, + GdkEventButton *event G_GNUC_UNUSED) +{ + return FALSE; +} + +static void +draw_layer_interface_init (OsmGpsMapLayerIface *iface) +{ + iface->render = draw_layer_render; + iface->draw = draw_layer_draw; + iface->busy = draw_layer_busy; + iface->button_press = draw_layer_button_press; +} + +static void +draw_layer_class_init (DrawLayerClass *klass G_GNUC_UNUSED) +{ +} + +static void +draw_layer_init (DrawLayer *self G_GNUC_UNUSED) +{ +} + static GdkPixbuf *g_star_image = NULL; static OsmGpsMapImage *g_last_image = NULL; static OsmGpsMapTrack *g_click_track = NULL; @@ -267,7 +341,7 @@ main (int argc, char **argv) GtkWidget *widget; GtkAccelGroup *ag; OsmGpsMap *map; - OsmGpsMapLayer *osd; + OsmGpsMapLayer *osd, *draw; const char *repo_uri; char *cachedir, *cachebasedir; GError *error = NULL; @@ -338,6 +412,10 @@ main (int argc, char **argv) osm_gps_map_layer_add(OSM_GPS_MAP(map), osd); g_object_unref(G_OBJECT(osd)); + draw = g_object_new (draw_layer_get_type (), NULL); + osm_gps_map_layer_add (OSM_GPS_MAP (map), draw); + g_object_unref (draw); + /* Stay put on gps_add so the blue blob is not under the OSD crosshair. */ g_object_set (map, "auto-center", FALSE, NULL); diff --git a/src/osm-gps-map-layer.c b/src/osm-gps-map-layer.c index 91c8248..d174c44 100644 --- a/src/osm-gps-map-layer.c +++ b/src/osm-gps-map-layer.c @@ -29,9 +29,10 @@ * * The map already provides a cairo context in osm_gps_map_layer_draw(). * Convert lat/lon to widget pixels with - * osm_gps_map_convert_geographic_to_screen(). See DummyLayer in - * examples/mapviewer.py. For tracks and images use osm_gps_map_track_add() - * and osm_gps_map_image_add() instead of a custom layer. + * osm_gps_map_convert_geographic_to_screen(). See DrawLayer in + * examples/mapviewer.py and examples/mapviewer.c. For tracks and images + * use osm_gps_map_track_add() and osm_gps_map_image_add() instead of a + * custom layer. **/ #include "osm-gps-map-layer.h" From 0d879367fd4fb89301be97ea744305553d6b84fd Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Fri, 28 Aug 2026 09:40:56 +0200 Subject: [PATCH 26/29] examples: fix Load Map URI dropping layers --- examples/mapviewer.py | 83 ++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/examples/mapviewer.py b/examples/mapviewer.py index 467cfac..5852ef2 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -105,45 +105,9 @@ def __init__(self): self.osm = DummyMapNoGpsPoint() else: self.osm = osmgpsmap.Map(user_agent="mapviewer.py") - self.osm.layer_add( - osmgpsmap.MapOsd(show_dpad=True, - show_zoom=True, - show_crosshair=True) - ) self.osm.set_property("map-source", osmgpsmap.MapSource_t.OPENSTREETMAP) - self.osm.layer_add(DrawLayer()) - self.osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) - # Stay put on gps_add so the blue blob is not under the OSD crosshair. - self.osm.props.auto_center = False - - self.click_track = osmgpsmap.MapTrack() - self.osm.track_add(self.click_track) - self.last_image = None self.use_poi_png = True - - self.osm.connect("button_press_event", self.on_button_press) - self.osm.connect("changed", self.on_map_change) - - # connect keyboard shortcuts - self.osm.set_keyboard_shortcut( - osmgpsmap.MapKey_t.FULLSCREEN, Gdk.keyval_from_name("F11") - ) - self.osm.set_keyboard_shortcut( - osmgpsmap.MapKey_t.UP, Gdk.keyval_from_name("Up") - ) - self.osm.set_keyboard_shortcut( - osmgpsmap.MapKey_t.DOWN, Gdk.keyval_from_name("Down") - ) - self.osm.set_keyboard_shortcut( - osmgpsmap.MapKey_t.LEFT, Gdk.keyval_from_name("Left") - ) - self.osm.set_keyboard_shortcut( - osmgpsmap.MapKey_t.RIGHT, Gdk.keyval_from_name("Right") - ) - - # connect to tooltip - self.osm.props.has_tooltip = True - self.osm.connect("query-tooltip", self.on_query_tooltip) + self._wire_map(self.osm) self.latlon_entry = Gtk.Entry() @@ -232,6 +196,44 @@ def __init__(self): GLib.timeout_add(500, self.print_tiles) + def _wire_map(self, osm): + self.osm = osm + osm.layer_add( + osmgpsmap.MapOsd(show_dpad=True, + show_zoom=True, + show_crosshair=True) + ) + osm.layer_add(DrawLayer()) + osm.set_center_and_zoom(HOME_LAT, HOME_LON, HOME_ZOOM) + # Stay put on gps_add so the blue blob is not under the OSD crosshair. + osm.props.auto_center = False + + self.click_track = osmgpsmap.MapTrack() + osm.track_add(self.click_track) + self.last_image = None + + osm.connect("button_press_event", self.on_button_press) + osm.connect("changed", self.on_map_change) + + osm.set_keyboard_shortcut( + osmgpsmap.MapKey_t.FULLSCREEN, Gdk.keyval_from_name("F11") + ) + osm.set_keyboard_shortcut( + osmgpsmap.MapKey_t.UP, Gdk.keyval_from_name("Up") + ) + osm.set_keyboard_shortcut( + osmgpsmap.MapKey_t.DOWN, Gdk.keyval_from_name("Down") + ) + osm.set_keyboard_shortcut( + osmgpsmap.MapKey_t.LEFT, Gdk.keyval_from_name("Left") + ) + osm.set_keyboard_shortcut( + osmgpsmap.MapKey_t.RIGHT, Gdk.keyval_from_name("Right") + ) + + osm.props.has_tooltip = True + osm.connect("query-tooltip", self.on_query_tooltip) + def disable_cache_toggled(self, btn): if btn.props.active: self.osm.props.tile_cache = osmgpsmap.MAP_CACHE_DISABLED @@ -249,16 +251,15 @@ def load_map_clicked(self, button): format = self.image_format_entry.get_text() if uri and format: if self.osm: - # remove old map self.vbox.remove(self.osm) try: - self.osm = osmgpsmap.Map( + osm = osmgpsmap.Map( repo_uri=uri, image_format=format, user_agent="mapviewer.py" ) except Exception as e: print("ERROR:", e) - self.osm = osmgpsmap.Map(user_agent="mapviewer.py") - + osm = osmgpsmap.Map(user_agent="mapviewer.py") + self._wire_map(osm) self.vbox.pack_start(self.osm, True, True, 0) self.osm.show() From 0bf6691e7d32cc1501e345134327d59fdee4170f Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 29 Aug 2026 10:47:35 +0200 Subject: [PATCH 27/29] examples: cairo star if poi.png missing --- examples/README | 10 ++++----- examples/mapviewer.c | 50 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/examples/README b/examples/README index 5d56ab7..530b0b5 100644 --- a/examples/README +++ b/examples/README @@ -27,13 +27,13 @@ to find the bindings. Do the following from the example directory: Examples for drawing on the map: * Double-left-click places a GPS point (blue ball+random heading). - * DrawLayer in mapviewer and mapviewer.py uses live cairo to draw a red house - at Home location (Timaru). Peak of the roof is at the lat/lon of - Home location. * Double-right-click (or Ctrl+double-left) adds points to tracks. Triple right click clears the track (a new empty track stays on the map). This shows the use of track_add, add_point and track_remove * Double-middle-click (or Shift+double-left) adds a marker via image_add. Python: poi.png by default, or uncheck "Use poi.png for markers" to - paint a cairo star (programmatic pixbuf). Triple middle click removes - the last image. + paint a cairo star (programmatic pixbuf). C: poi.png, or a cairo star + if that file is missing. Triple middle click removes the last image. + * DrawLayer in mapviewer and mapviewer.py uses live cairo to draw a red house + at Home location (Timaru). Peak of the roof is at the lat/lon of + Home location. diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 4cef7fc..20c2ebf 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -22,6 +22,7 @@ #include #include #include +#include #include "osm-gps-map.h" @@ -133,6 +134,43 @@ static GdkPixbuf *g_star_image = NULL; static OsmGpsMapImage *g_last_image = NULL; static OsmGpsMapTrack *g_click_track = NULL; +/* Programmatic marker if poi.png is missing. Live cairo overlay is DrawLayer. */ +static GdkPixbuf * +star_pixbuf_new (int size) +{ + cairo_surface_t *surface; + cairo_t *cr; + GdkPixbuf *pixbuf; + int i; + double cx, cy, outer, inner; + + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + cr = cairo_create (surface); + cx = cy = size / 2.0; + outer = size / 2.0 - 1.0; + inner = size / 5.0; + for (i = 0; i < 10; i++) { + double r = (i % 2 == 0) ? outer : inner; + double ang = -G_PI / 2.0 + i * G_PI / 5.0; + double x = cx + r * cos (ang); + double y = cy + r * sin (ang); + if (i == 0) + cairo_move_to (cr, x, y); + else + cairo_line_to (cr, x, y); + } + cairo_close_path (cr); + cairo_set_source_rgb (cr, 1.0, 0.85, 0.0); + cairo_fill_preserve (cr); + cairo_set_source_rgb (cr, 0.75, 0.45, 0.0); + cairo_set_line_width (cr, 1); + cairo_stroke (cr); + pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); + cairo_destroy (cr); + cairo_surface_destroy (surface); + return pixbuf; +} + static OsmGpsMapTrack * click_track_new (void) { @@ -178,7 +216,7 @@ on_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_d lon, g_random_double_range(0,360)); } - if (middle_button) { + if (middle_button && g_star_image) { g_last_image = osm_gps_map_image_add (map, lat, lon, @@ -434,7 +472,15 @@ main (int argc, char **argv) osm_gps_map_set_keyboard_shortcut(map, OSM_GPS_MAP_KEY_RIGHT, GDK_KEY_Right); //Build the UI - g_star_image = gdk_pixbuf_new_from_file_at_size ("poi.png", 24,24,NULL); + { + GError *pixbuf_err = NULL; + g_star_image = gdk_pixbuf_new_from_file_at_size ("poi.png", 24, 24, &pixbuf_err); + if (!g_star_image) { + g_printerr ("poi.png: %s\n", pixbuf_err ? pixbuf_err->message : "missing"); + g_clear_error (&pixbuf_err); + g_star_image = star_pixbuf_new (24); + } + } builder = gtk_builder_new(); gtk_builder_add_from_file (builder, "mapviewer.ui", &error); From 1623853fda675c46f2a4e437b0e599f5e43d8d47 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 29 Aug 2026 13:00:22 +0200 Subject: [PATCH 28/29] examples: draw fallback star with cairo_rotate, not sin/cos GCC turns same-angle sin+cos into sincos, which needs -lm on the mapviewer link line. Rotate in cairo instead so the example does not reference libm. --- examples/mapviewer.c | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 20c2ebf..612c07a 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -18,7 +18,6 @@ */ #include -#include #include #include #include @@ -142,22 +141,17 @@ star_pixbuf_new (int size) cairo_t *cr; GdkPixbuf *pixbuf; int i; - double cx, cy, outer, inner; + double outer = size / 2.0 - 1.0; + double inner = size / 5.0; surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); cr = cairo_create (surface); - cx = cy = size / 2.0; - outer = size / 2.0 - 1.0; - inner = size / 5.0; - for (i = 0; i < 10; i++) { - double r = (i % 2 == 0) ? outer : inner; - double ang = -G_PI / 2.0 + i * G_PI / 5.0; - double x = cx + r * cos (ang); - double y = cy + r * sin (ang); - if (i == 0) - cairo_move_to (cr, x, y); - else - cairo_line_to (cr, x, y); + + cairo_translate (cr, size / 2.0, size / 2.0); + cairo_move_to (cr, 0, -outer); + for (i = 1; i < 10; i++) { + cairo_rotate (cr, G_PI / 5.0); + cairo_line_to (cr, 0, (i % 2 == 0) ? -outer : -inner); } cairo_close_path (cr); cairo_set_source_rgb (cr, 1.0, 0.85, 0.0); @@ -165,6 +159,7 @@ star_pixbuf_new (int size) cairo_set_source_rgb (cr, 0.75, 0.45, 0.0); cairo_set_line_width (cr, 1); cairo_stroke (cr); + pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); cairo_destroy (cr); cairo_surface_destroy (surface); From 89cef3f7aa911a3c7893179b30c396abcf4d80a9 Mon Sep 17 00:00:00 2001 From: Hubert Kowalski Date: Sat, 29 Aug 2026 13:10:05 +0200 Subject: [PATCH 29/29] ci: add python3-gi-cairo for layer draw --- .github/workflows/ci.yml | 1 + tests/test.py | 1 + 2 files changed, 2 insertions(+) mode change 100755 => 100644 tests/test.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea45b7c..4456f32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: libgirepository1.0-dev \ python3-gi \ python3-cairo \ + python3-gi-cairo \ gir1.2-gtk-3.0 \ gir1.2-gdkpixbuf-2.0 \ xvfb diff --git a/tests/test.py b/tests/test.py old mode 100755 new mode 100644 index e0d8b29..0fd8ce4 --- a/tests/test.py +++ b/tests/test.py @@ -5,6 +5,7 @@ import gi gi.require_version('OsmGpsMap', '1.0') +gi.require_foreign('cairo') from gi.repository import OsmGpsMap from gi.repository import Gdk, GdkPixbuf, GLib, GObject, Gtk