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/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/README b/examples/README index 1ccb183..530b0b5 100644 --- a/examples/README +++ b/examples/README @@ -1,17 +1,39 @@ 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 probably 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/ + * ./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 of options. + --editable-tracks makes the right-click track draggable. * ./polygon - This example demonstrates 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. Same degree constructor as polygon. * ./mapviewer.py Python version of the C demo app, with examples showing how to do custom - layers. + layers: DrawLayer class for cairo overlay, GPS, programmatic image_add, + and track_add. * ./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. + +Examples for drawing on the map: + + * Double-left-click places a GPS point (blue ball+random heading). + * 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). 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/editable_track.c b/examples/editable_track.c index 07613aa..48222a4 100644 --- a/examples/editable_track.c +++ b/examples/editable_track.c @@ -21,8 +21,8 @@ main (int argc, OsmGpsMapTrack* track = osm_gps_map_track_new(); OsmGpsMapPoint* p1, *p2; - 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); diff --git a/examples/mapviewer.c b/examples/mapviewer.c index 7f15eb5..612c07a 100644 --- a/examples/mapviewer.c +++ b/examples/mapviewer.c @@ -18,10 +18,10 @@ */ #include -#include #include #include #include +#include #include "osm-gps-map.h" @@ -50,18 +50,141 @@ static GOptionEntry debug_entries[] = }; #endif +/* 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; + +/* 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 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); + + 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); + 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) +{ + 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); + /* 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); + 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)); @@ -70,11 +193,16 @@ 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, othertrack); + 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) { @@ -83,14 +211,14 @@ 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, g_star_image); } if (right_button) { - osm_gps_map_track_add_point(othertrack, &coord); + osm_gps_map_track_add_point(g_click_track, &coord); } } @@ -105,7 +233,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); @@ -136,7 +264,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; } @@ -246,8 +374,7 @@ main (int argc, char **argv) GtkWidget *widget; GtkAccelGroup *ag; OsmGpsMap *map; - OsmGpsMapLayer *osd; - OsmGpsMapTrack *rightclicktrack; + OsmGpsMapLayer *osd, *draw; const char *repo_uri; char *cachedir, *cachebasedir; GError *error = NULL; @@ -318,12 +445,16 @@ 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(); + 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); - 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); + osm_gps_map_set_center_and_zoom (map, HOME_LAT, HOME_LON, HOME_ZOOM); g_free(cachedir); g_free(cachebasedir); @@ -336,7 +467,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); @@ -404,7 +543,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.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 5313edc..5852ef2 100755 --- a/examples/mapviewer.py +++ b/examples/mapviewer.py @@ -17,7 +17,10 @@ along with this program; if not, see . """ +import math import random + +import cairo import gi gi.require_version("Gdk", "3.0") @@ -33,36 +36,58 @@ 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" +# 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 + + class DummyMapNoGpsPoint(osmgpsmap.Map): - def do_draw_gps_point(self, drawable): + def do_draw_gps_point(self, cr): pass 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 + live-cairo case: draw in widget pixels after convert_geographic_to_screen. + """ + def __init__(self): GObject.GObject.__init__(self) - def do_draw(self, gpsmap, gdkdrawable): - pass + 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.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): + # OSD caches bitmaps here. Geo overlays paint in do_draw instead. pass def do_busy(self): return False - def do_button_press(self, gpsmap, gdkeventbutton): + def do_button_press(self, gpsmap, event): return False -GObject.type_register(DummyLayer) +GObject.type_register(DrawLayer) class UI(Gtk.Window): @@ -79,40 +104,10 @@ def __init__(self): if 0: self.osm = DummyMapNoGpsPoint() else: - self.osm = osmgpsmap.Map() - self.osm.layer_add( - osmgpsmap.MapOsd(show_dpad=True, - show_zoom=True, - show_crosshair=True) - ) + self.osm = osmgpsmap.Map(user_agent="mapviewer.py") self.osm.set_property("map-source", osmgpsmap.MapSource_t.OPENSTREETMAP) - self.osm.layer_add(DummyLayer()) - - self.last_image = None - - 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.use_poi_png = True + self._wire_map(self.osm) self.latlon_entry = Gtk.Entry() @@ -155,7 +150,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 @@ -185,6 +180,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) @@ -196,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 @@ -205,19 +243,23 @@ 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() if uri and format: if self.osm: - # remove old map self.vbox.remove(self.osm) try: - self.osm = osmgpsmap.Map(repo_uri=uri, image_format=format) + osm = osmgpsmap.Map( + repo_uri=uri, image_format=format, user_agent="mapviewer.py" + ) except Exception as e: print("ERROR:", e) - self.osm = osmgpsmap.Map() - + osm = osmgpsmap.Map(user_agent="mapviewer.py") + self._wire_map(osm) self.vbox.pack_start(self.osm, True, True, 0) self.osm.show() @@ -233,15 +275,15 @@ 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: return False if self.show_tooltips: - 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 @@ -263,10 +305,18 @@ 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() - 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 ) @@ -283,14 +333,52 @@ 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) 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: - pass + pt = osmgpsmap.MapPoint.new_degrees(lat, lon) + self.click_track.add_point(pt) + + def _marker_pixbuf(self): + 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 DrawLayer, not this. + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) + cr = cairo.Context(surface) + 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) if __name__ == "__main__": 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); diff --git a/src/osm-gps-map-layer.c b/src/osm-gps-map-layer.c index 3da68ee..d174c44 100644 --- a/src/osm-gps-map-layer.c +++ b/src/osm-gps-map-layer.c @@ -24,8 +24,15 @@ * @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 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" diff --git a/src/osm-gps-map-layer.h b/src/osm-gps-map-layer.h index a2d248c..80aa048 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 **/ @@ -103,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); diff --git a/tests/test.py b/tests/test.py old mode 100755 new mode 100644 index 49db29c..0fd8ce4 --- a/tests/test.py +++ b/tests/test.py @@ -5,9 +5,10 @@ import gi gi.require_version('OsmGpsMap', '1.0') +gi.require_foreign('cairo') 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 +63,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 @@ -129,6 +178,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)