[Git][debian-gis-team/osm-gps-map][upstream] New upstream version 1.2.2

Bas Couwenberg (@sebastic) gitlab at salsa.debian.org
Mon Oct 12 04:32:17 BST 2026



Bas Couwenberg pushed to branch upstream at Debian GIS Project / osm-gps-map


Commits:
04511e21 by Bas Couwenberg at 2026-10-12T05:21:28+02:00
New upstream version 1.2.2
- - - - -


28 changed files:

- .github/workflows/ci.yml
- .github/workflows/pages.yml
- .gitignore
- Makefile.am
- NEWS
- configure.ac
- examples/Makefile.am
- examples/README
- examples/editable_track.c
- examples/mapviewer.c
- examples/mapviewer.js
- examples/mapviewer.py
- examples/polygon.c
- catalog.xml → osmgpsmap.xml
- + scripts/releases-data.py
- src/converter.c
- src/osm-gps-map-layer.c
- src/osm-gps-map-layer.h
- src/osm-gps-map-track.c
- src/osm-gps-map-widget.c
- tests/test.py
- + website/Gemfile
- + website/Gemfile.lock
- + website/_config.yml
- + website/_data/releases.json
- + website/index.md
- + website/screenshot-osd.png
- + website/screenshot.png


Changes:

=====================================
.github/workflows/ci.yml
=====================================
@@ -10,7 +10,7 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout at v5
+      - uses: actions/checkout at v7
 
       - name: Install dependencies
         run: |
@@ -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


=====================================
.github/workflows/pages.yml
=====================================
@@ -1,28 +1,32 @@
 name: Pages
 
-# Deploy gtk-doc HTML to the gh-pages branch on every published release
-# (RC or final). Landing page + screenshots on gh-pages are preserved.
+# Tag is enough to publish (gtk-doc + Jekyll). A GitHub Release is
+# required only for the landing-page Download / News list.
 on:
-  release:
-    types: [published]
+  push:
+    branches:
+      - master
+    tags:
+      - "*"
+  workflow_dispatch:
 
 permissions:
-  contents: write
+  contents: read
+  pages: write
+  id-token: write
+
+concurrency:
+  group: pages
+  cancel-in-progress: false
 
 jobs:
-  docs:
+  build:
     runs-on: ubuntu-latest
     steps:
-      - name: Checkout release
-        uses: actions/checkout at v5
-
-      - name: Checkout gh-pages
-        uses: actions/checkout at v5
-        with:
-          ref: gh-pages
-          path: _gh-pages
+      - name: Checkout
+        uses: actions/checkout at v7
 
-      - name: Install dependencies
+      - name: Install gtk-doc dependencies
         run: |
           sudo apt-get update
           sudo apt-get install -y \
@@ -41,21 +45,51 @@ jobs:
         run: |
           ./autogen.sh --prefix=/usr --enable-gtk-doc
           make -j"$(nproc)"
+          test -d docs/reference/html
 
-      - name: Update API docs on gh-pages
+      - name: Write releases data
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         run: |
-          set -e
-          test -d docs/reference/html
-          rm -rf _gh-pages/docs/reference/html
-          mkdir -p _gh-pages/docs/reference
-          cp -a docs/reference/html _gh-pages/docs/reference/
-          cd _gh-pages
-          git config user.name "github-actions[bot]"
-          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
-          git add -A docs/reference/html
-          if git diff --staged --quiet; then
-            echo "No docs changes"
-            exit 0
-          fi
-          git commit -m "docs: API reference for ${{ github.event.release.tag_name }}"
-          git push origin HEAD:gh-pages
+          python3 scripts/releases-data.py \
+            --repo "${{ github.repository }}" \
+            --out website/_data/releases.json
+
+      - name: Setup Ruby
+        uses: ruby/setup-ruby at v1
+        with:
+          ruby-version: "3.3"
+          bundler-cache: true
+          working-directory: website
+
+      - name: Setup Pages
+        id: pages
+        uses: actions/configure-pages at v6
+
+      - name: Build with Jekyll
+        working-directory: website
+        env:
+          JEKYLL_ENV: production
+        run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
+
+      - name: Copy gtk-doc into site
+        run: |
+          mkdir -p website/_site/docs/reference
+          rm -rf website/_site/docs/reference/html
+          cp -a docs/reference/html website/_site/docs/reference/
+
+      - name: Upload artifact
+        uses: actions/upload-pages-artifact at v5
+        with:
+          path: website/_site
+
+  deploy:
+    environment:
+      name: github-pages
+      url: ${{ steps.deployment.outputs.page_url }}
+    runs-on: ubuntu-latest
+    needs: build
+    steps:
+      - name: Deploy to GitHub Pages
+        id: deployment
+        uses: actions/deploy-pages at v5


=====================================
.gitignore
=====================================
@@ -58,3 +58,8 @@ examples/editable_track
 examples/polygon
 m4/gnome-compiler-flags.m4
 m4/pkg.m4
+website/_site/
+website/.jekyll-cache/
+website/.jekyll-metadata
+website/vendor/
+website/.bundle/


=====================================
Makefile.am
=====================================
@@ -1,8 +1,11 @@
 ## Process this file with automake to produce Makefile.in
 
 SUBDIRS = src examples docs
+# website/ is GitHub Pages (Jekyll). Not in SUBDIRS or EXTRA_DIST so
+# release tarballs stay the library (src, examples, gtk-doc).
 AM_DISTCHECK_CONFIGURE_FLAGS =                  \
-    --enable-gtk-doc
+    --enable-gtk-doc                            \
+    --enable-glade-catalog
 
 ACLOCAL_AMFLAGS = -I m4 ${ACLOCAL_FLAGS}
 
@@ -16,8 +19,12 @@ doc_DATA =                                      \
 pkgconfigdir = $(libdir)/pkgconfig
 pkgconfig_DATA = osmgpsmap-1.0.pc
 
-# Unit tests ship in the tarball (CI/git also run them in-tree).
-EXTRA_DIST = tests/test.py
+if ENABLE_GLADE_CATALOG
+gladecatalog_DATA = osmgpsmap.xml
+endif
+
+# tests/test.py and osmgpsmap.xml ship in the tarball even if not installed.
+EXTRA_DIST = tests/test.py osmgpsmap.xml
 
 DISTCLEANFILES = gtk-doc.make
 


=====================================
NEWS
=====================================
@@ -1,4 +1,20 @@
+Changes in master
+=================
+
+Changes in 1.2.2
+================
+  * Don't use floats for division because of rounding errors; fixes center hopping (znuh)
+  * Avoid div-by-zero in latlon2zoom when a latitude or longitude distance is zero (Hubert Kowalski)
+  * Floor tile origin for negative map-x so overlays stay aligned (Hubert Kowalski)
+  * Fix insert_point double-free (Hubert Kowalski)
+  * Hold the map until tile downloads complete so leftover Soup callbacks cannot touch freed tile state (Hubert Kowalski)
+  * Fix map redraw using width as height (Hubert Kowalski)
+  * Show drawing on the map in the examples (Hubert Kowalski)
+  * Add optional Glade catalog installation (Hubert Kowalski)
+  * Correctly support LFS (David Paleino, Bas Couwenberg)
+
 Changes in 1.2.1
+======================
   * Replace deprecated G_TYPE_INSTANCE_GET_PRIVATE and g_type_class_add_private (Patrick Salecker)
   * Add python unit tests (Patrick Salecker)
   * Fix python example addressing requiring map source (Jochen Sprickerhof)


=====================================
configure.ac
=====================================
@@ -1,6 +1,6 @@
 AC_PREREQ([2.63])
 AC_INIT([osm-gps-map],
-        [1.2.1],
+        [1.2.2],
         [http://github.com/nzjrs/osm-gps-map/issues],
         [osm-gps-map])
 
@@ -21,6 +21,8 @@ AM_PROG_LIBTOOL
 
 AM_SILENT_RULES([yes])
 
+AC_SYS_LARGEFILE
+
 # Library dependencies
 PKG_CHECK_MODULES(GLIB,     [glib-2.0 >= 2.16])
 PKG_CHECK_MODULES(GTK,      [gtk+-3.0 >= 3.0])
@@ -31,6 +33,35 @@ PKG_CHECK_MODULES(SOUP30,   [libsoup-3.0])
 # libgthread-2.0.
 PKG_CHECK_MODULES(GTHREAD, [gthread-2.0])
 
+AC_ARG_ENABLE([glade-catalog],
+              [AS_HELP_STRING([--enable-glade-catalog],
+                              [install the Glade catalog (default=no)])],
+              [enable_glade_catalog=$enableval],
+              [enable_glade_catalog=no])
+case "$enable_glade_catalog" in
+  yes|no) ;;
+  *) AC_MSG_ERROR([--enable-glade-catalog accepts only yes or no]) ;;
+esac
+AM_CONDITIONAL([ENABLE_GLADE_CATALOG],
+               [test "x$enable_glade_catalog" = xyes])
+
+AC_ARG_WITH([glade-catalogdir],
+            [AS_HELP_STRING([--with-glade-catalogdir=PATH],
+                            [Glade catalog directory (default=DATADIR/glade/catalogs)])],
+            [gladecatalogdir=$withval],
+            [gladecatalogdir='${datadir}/glade/catalogs'])
+if test "${with_glade_catalogdir+set}" = set; then
+  case "$with_glade_catalogdir" in
+    yes|no|'')
+      AC_MSG_ERROR([--with-glade-catalogdir requires a directory path])
+      ;;
+  esac
+  if test "x$enable_glade_catalog" != xyes; then
+    AC_MSG_ERROR([--with-glade-catalogdir requires --enable-glade-catalog])
+  fi
+fi
+AC_SUBST([gladecatalogdir])
+
 AC_CHECK_FUNCS(gdk_event_get_scroll_deltas)
 
 AC_MSG_CHECKING([for Win32])
@@ -52,7 +83,7 @@ AM_CONDITIONAL(OS_WIN32, [test $os_win32 = yes])
 # - If binary compatibility has been broken (eg removed or changed interfaces)
 #   change to C+1:0:0
 # - If the interface is the same as the previous version, change to C:R+1:A
-LT_VERSION_INFO=2:1:1
+LT_VERSION_INFO=2:2:1
 AC_SUBST(LT_VERSION_INFO)
 
 GOBJECT_INTROSPECTION_REQS=0.10.0
@@ -76,4 +107,8 @@ echo
 echo Prefix............... : $prefix
 echo Introspection support : ${found_introspection}
 echo gtk-doc documentation : ${enable_gtk_doc}
+echo Glade catalog........ : ${enable_glade_catalog}
+if test "x$enable_glade_catalog" = xyes; then
+  echo Glade catalog dir.... : ${gladecatalogdir}
+fi
 echo


=====================================
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
 


=====================================
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.


=====================================
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);


=====================================
examples/mapviewer.c
=====================================
@@ -18,10 +18,10 @@
  */
 
 #include <stdlib.h>
-#include <math.h>
 #include <glib.h>
 #include <gtk/gtk.h>
 #include <gdk/gdkkeysyms.h>
+#include <cairo.h>
 
 #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"));


=====================================
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/
 


=====================================
examples/mapviewer.py
=====================================
@@ -17,7 +17,10 @@ You should have received a copy of the GNU General Public License
 along with this program; if not, see <http://www.gnu.org/licenses/>.
 """
 
+import math
 import random
+
+import cairo
 import gi
 
 gi.require_version("Gdk", "3.0")
@@ -33,36 +36,58 @@ from gi.repository import (
     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 @@ class UI(Gtk.Window):
         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 @@ from in the box below. Special metacharacters may be included in this url
 \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 @@ from in the box below. Special metacharacters may be included in this url
         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 @@ from in the box below. Special metacharacters may be included in this url
 
         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 @@ from in the box below. Special metacharacters may be included in this url
     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 @@ from in the box below. Special metacharacters may be included in this url
         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 @@ from in the box below. Special metacharacters may be included in this url
         )
 
     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 @@ from in the box below. Special metacharacters may be included in this url
                 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__":


=====================================
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);


=====================================
catalog.xml → osmgpsmap.xml
=====================================
@@ -1,11 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<glade-catalog name="osmgpsmap" library="osmgpsmap-1.0" depends="gtk+" book="osm-gps-map">
+<glade-catalog name="osmgpsmap" library="osmgpsmap-1.0" depends="gtk+" book="osm-gps-map" supports="gtkbuilder">
     <glade-widget-classes>
         <glade-widget-class name="OsmGpsMap" generic-name="map" title="Map">
             <signals>
-                <signal id="point-added" />
-                <signal id="key_press_event" />
-                <signal id="point-changed" />
+                <signal id="changed" />
             </signals>
         </glade-widget-class>
     </glade-widget-classes>


=====================================
scripts/releases-data.py
=====================================
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+"""Write website/_data/releases.json from the GitHub Releases API.
+
+Download list is Releases only. Tag-only tags are omitted on purpose.
+News bullets come from the release body (`## Changes in …`), not from
+re-parsing NEWS in CI.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import re
+import sys
+import urllib.error
+import urllib.request
+from pathlib import Path
+
+CHANGES_HEAD = re.compile(r"^#{1,2} Changes in\b")
+BULLET = re.compile(r"^[\*\-] (.+)$")
+
+
+def _api(url: str, token: str | None) -> object:
+    headers = {
+        "Accept": "application/vnd.github+json",
+        "User-Agent": "osm-gps-map-pages",
+    }
+    if token:
+        headers["Authorization"] = f"Bearer {token}"
+    req = urllib.request.Request(url, headers=headers)
+    with urllib.request.urlopen(req, timeout=30) as resp:
+        return json.load(resp)
+
+
+def tar_asset(rel: dict) -> str | None:
+    for asset in rel.get("assets") or []:
+        name = asset.get("name") or ""
+        url = asset.get("browser_download_url") or ""
+        if name.endswith(".tar.gz") and url:
+            return str(url)
+    return None
+
+
+def archive_url(repo: str, tag: str, asset: str | None) -> str:
+    if asset:
+        return asset
+    return f"https://github.com/{repo}/archive/refs/tags/{tag}.tar.gz"
+
+
+def changes_from_body(body: str) -> list[str]:
+    items: list[str] = []
+    in_changes = False
+    for line in (body or "").splitlines():
+        if CHANGES_HEAD.match(line.strip()):
+            in_changes = True
+            continue
+        if in_changes and line.startswith("## "):
+            break
+        if not in_changes:
+            continue
+        b = BULLET.match(line.strip())
+        if b:
+            items.append(b.group(1).strip())
+    return items
+
+
+def fetch_releases(repo: str, token: str | None) -> list[dict]:
+    out: list[dict] = []
+    page = 1
+    while True:
+        data = _api(
+            f"https://api.github.com/repos/{repo}/releases?per_page=100&page={page}",
+            token,
+        )
+        if not isinstance(data, list) or not data:
+            break
+        for item in data:
+            if item.get("draft"):
+                continue
+            tag = item.get("tag_name")
+            if not tag:
+                continue
+            body = item.get("body") or ""
+            out.append(
+                {
+                    "tag": str(tag),
+                    "date": (item.get("published_at") or "")[:10],
+                    "prerelease": bool(item.get("prerelease")),
+                    "html_url": str(
+                        item.get("html_url")
+                        or f"https://github.com/{repo}/releases/tag/{tag}"
+                    ),
+                    "archive_url": archive_url(repo, str(tag), tar_asset(item)),
+                    "changes": changes_from_body(body),
+                }
+            )
+        if len(data) < 100:
+            break
+        page += 1
+    return out
+
+
+def payload(repo: str, rels: list[dict]) -> dict:
+    published = [r for r in rels if not r["prerelease"]]
+    latest = published[0] if published else (rels[0] if rels else None)
+    return {
+        "repo": repo,
+        "clone_url": f"https://github.com/{repo}.git",
+        "master_tar": f"https://github.com/{repo}/archive/refs/heads/master.tar.gz",
+        "master_zip": f"https://github.com/{repo}/archive/refs/heads/master.zip",
+        "latest": latest,
+        "archives": rels,
+    }
+
+
+def main(argv: list[str] | None = None) -> int:
+    p = argparse.ArgumentParser()
+    p.add_argument("--repo", required=True)
+    p.add_argument("--out", type=Path, required=True)
+    p.add_argument("--token", default=os.environ.get("GITHUB_TOKEN", ""))
+    args = p.parse_args(argv)
+    try:
+        rels = fetch_releases(args.repo, args.token or None)
+    except urllib.error.HTTPError as exc:
+        raise SystemExit(f"releases API failed: {exc}") from exc
+    args.out.parent.mkdir(parents=True, exist_ok=True)
+    args.out.write_text(
+        json.dumps(payload(args.repo, rels), indent=2) + "\n",
+        encoding="utf-8",
+    )
+    return 0
+
+
+SAMPLE_BODY = """# osm-gps-map lab-1
+
+## Changes in lab-1
+
+- jekyll: boilerplate landing page
+- ci: Actions deploys Pages
+
+## Source tarball
+
+- File: `osm-gps-map-lab-1.tar.gz`
+"""
+
+SAMPLE_STAR_BODY = """# osm-gps-map 1.2.1
+
+## Changes in 1.2.1
+  * Replace deprecated G_TYPE_INSTANCE_GET_PRIVATE
+  * Add python unit tests (Patrick Salecker)
+"""
+
+
+def _self_check() -> None:
+    assert changes_from_body(SAMPLE_BODY) == [
+        "jekyll: boilerplate landing page",
+        "ci: Actions deploys Pages",
+    ]
+    assert changes_from_body(SAMPLE_STAR_BODY) == [
+        "Replace deprecated G_TYPE_INSTANCE_GET_PRIVATE",
+        "Add python unit tests (Patrick Salecker)",
+    ]
+    assert changes_from_body("no heading\n- nope") == []
+    data = payload(
+        "nzjrs/osm-gps-map",
+        [
+            {
+                "tag": "lab-1",
+                "date": "2026-09-10",
+                "prerelease": False,
+                "html_url": "https://github.com/nzjrs/osm-gps-map/releases/tag/lab-1",
+                "archive_url": archive_url("nzjrs/osm-gps-map", "lab-1", None),
+                "changes": ["jekyll: boilerplate landing page"],
+            }
+        ],
+    )
+    assert data["latest"]["tag"] == "lab-1"
+    assert len(data["archives"]) == 1
+    empty = payload("nzjrs/osm-gps-map", [])
+    assert empty["latest"] is None
+    assert empty["archives"] == []
+    print("releases-data self-check ok")
+
+
+if __name__ == "__main__":
+    if len(sys.argv) > 1 and sys.argv[1] == "--self-check":
+        _self_check()
+    else:
+        raise SystemExit(main())


=====================================
src/converter.c
=====================================
@@ -22,6 +22,7 @@
 #include <math.h>
 #include <stdio.h>
 #include <float.h>
+#include <limits.h>
 
 #include "private.h"
 #include "converter.h"
@@ -126,7 +127,14 @@ latlon2zoom(int pix_height,
 {
     float lat1_m = atanh(sin(lat1));
     float lat2_m = atanh(sin(lat2));
-    int zoom_lon = LOG2((double)(2 * pix_width * M_PI) / (TILESIZE * (lon2 - lon1)));
-    int zoom_lat = LOG2((double)(2 * pix_height * M_PI) / (TILESIZE * (lat2_m - lat1_m)));
+    float d_lon = lon2 - lon1;
+    float d_lat = lat2_m - lat1_m;
+    int zoom_lon = INT_MAX;
+    int zoom_lat = INT_MAX;
+
+    if (d_lon != 0.0f)
+        zoom_lon = LOG2((double)(2 * pix_width * M_PI) / (TILESIZE * d_lon));
+    if (d_lat != 0.0f)
+        zoom_lat = LOG2((double)(2 * pix_height * M_PI) / (TILESIZE * d_lat));
     return MIN(zoom_lon, zoom_lat);
 }


=====================================
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"


=====================================
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);


=====================================
src/osm-gps-map-track.c
=====================================
@@ -312,11 +312,16 @@ int osm_gps_map_track_n_points(OsmGpsMapTrack* track)
     return g_slist_length(track->priv->track);
 }
 
-void osm_gps_map_track_insert_point(OsmGpsMapTrack* track, OsmGpsMapPoint* np, int pos)
+void
+osm_gps_map_track_insert_point (OsmGpsMapTrack* track, OsmGpsMapPoint* np, int pos)
 {
-    OsmGpsMapTrackPrivate* priv = track->priv;
-    priv->track = g_slist_insert(priv->track, np, pos);
-    g_signal_emit(track, signals[POINT_INSERTED], 0, pos);
+    // TODO: const OsmGpsMapPoint * like add_point (1.3)
+    g_return_if_fail (OSM_GPS_MAP_IS_TRACK (track));
+    OsmGpsMapTrackPrivate *priv = track->priv;
+
+    OsmGpsMapPoint *p = g_boxed_copy (OSM_TYPE_GPS_MAP_POINT, np);
+    priv->track = g_slist_insert (priv->track, p, pos);
+    g_signal_emit (track, signals[POINT_INSERTED], 0, pos);
 }
 
 OsmGpsMapPoint* osm_gps_map_track_get_point(OsmGpsMapTrack* track, int pos)


=====================================
src/osm-gps-map-widget.c
=====================================
@@ -326,6 +326,13 @@ cached_tile_free (OsmCachedTile *tile)
     g_slice_free (OsmCachedTile, tile);
 }
 
+static gboolean
+unref_in_idle (gpointer data)
+{
+    g_object_unref (data);
+    return FALSE;
+}
+
 /*
  * Description:
  *   Find and replace text within a string.
@@ -744,13 +751,10 @@ osm_gps_map_tile_download_complete (SoupSession *session, GAsyncResult *result,
 
     GError *error = NULL;
     SoupMessage *msg = soup_session_get_async_result_message(session, result);
-    SoupStatus soup_status = soup_message_get_status(msg);
+    SoupStatus soup_status = msg ? soup_message_get_status(msg) : 0;
     GBytes *body = soup_session_send_and_read_finish (session, result, &error);
 
-    GCancellable *cancellable = (GCancellable *)g_hash_table_lookup(priv->tile_queue, dl->uri);
-    g_object_unref (cancellable);
-
-    if (SOUP_STATUS_IS_SUCCESSFUL (soup_status)) {
+    if (!priv->is_disposed && body && SOUP_STATUS_IS_SUCCESSFUL (soup_status)) {
         /* save tile into cachedir if one has been specified */
         if (priv->cache_dir) {
             if (g_mkdir_with_parents(dl->folder,0700) == 0) {
@@ -816,11 +820,7 @@ osm_gps_map_tile_download_complete (SoupSession *session, GAsyncResult *result,
         }
         g_hash_table_remove(priv->tile_queue, dl->uri);
         g_object_notify(G_OBJECT(map), "tiles-queued");
-
-        g_free(dl->folder);
-        g_free(dl->filename);
-        g_free(dl);
-    } else {
+    } else if (!priv->is_disposed) {
         if ((soup_status == SOUP_STATUS_NOT_FOUND) || (soup_status == SOUP_STATUS_FORBIDDEN)) {
             g_hash_table_insert(priv->missing_tiles, dl->uri, NULL);
             g_hash_table_remove(priv->tile_queue, dl->uri);
@@ -842,7 +842,15 @@ osm_gps_map_tile_download_complete (SoupSession *session, GAsyncResult *result,
         }
     }
 
-
+    if (body)
+        g_bytes_unref (body);
+    g_clear_error (&error);
+    g_free(dl->folder);
+    g_free(dl->filename);
+    g_free(dl);
+    /* Drop the download ref after this Soup callback returns. Unref from
+     * here disposes the session on the callback stack. */
+    g_idle_add (unref_in_idle, map);
 }
 
 static void
@@ -898,13 +906,14 @@ osm_gps_map_download_tile (OsmGpsMap *map, int zoom, int x, int y, gboolean redr
 
             GCancellable *cancellable = g_cancellable_new ();
             g_hash_table_insert (priv->tile_queue, dl->uri, cancellable);
-            g_object_notify (G_OBJECT (map), "tiles-queued");
+            dl->map = g_object_ref (map);
             /* the soup session unrefs the message when the download finishes */
             soup_session_send_and_read_async(priv->soup_session, msg,
                                     G_PRIORITY_DEFAULT,
                                     cancellable,
                                     (GAsyncReadyCallback)osm_gps_map_tile_download_complete,
                                     dl);
+            g_object_notify (G_OBJECT (map), "tiles-queued");
         } else {
             g_warning("Could not create soup message");
             g_free(dl->uri);
@@ -1111,10 +1120,17 @@ osm_gps_map_fill_tiles_pixel (OsmGpsMap *map, cairo_t *cr)
 
     gtk_widget_get_allocation(GTK_WIDGET(map), &allocation);
 
-    offset_x = - priv->map_x % TILESIZE;
-    offset_y = - priv->map_y % TILESIZE;
-    if (offset_x > 0) offset_x -= TILESIZE;
-    if (offset_y > 0) offset_y -= TILESIZE;
+    /* C / truncates toward 0; tile index must floor so negative map_x/map_y
+     * stay aligned with overlays. Integer math keeps zoom 17-19 exact. */
+    tile_x0 = priv->map_x / TILESIZE;
+    tile_y0 = priv->map_y / TILESIZE;
+    if (priv->map_x < 0 && (priv->map_x % TILESIZE) != 0)
+        tile_x0--;
+    if (priv->map_y < 0 && (priv->map_y % TILESIZE) != 0)
+        tile_y0--;
+
+    offset_x = tile_x0 * TILESIZE - priv->map_x;
+    offset_y = tile_y0 * TILESIZE - priv->map_y;
 
     offset_xn = offset_x + EXTRA_BORDER;
     offset_yn = offset_y + EXTRA_BORDER;
@@ -1122,9 +1138,6 @@ osm_gps_map_fill_tiles_pixel (OsmGpsMap *map, cairo_t *cr)
     tiles_nx = (allocation.width  - offset_x) / TILESIZE + 1;
     tiles_ny = (allocation.height - offset_y) / TILESIZE + 1;
 
-    tile_x0 =  floorf((float)priv->map_x / (float)TILESIZE);
-    tile_y0 =  floorf((float)priv->map_y / (float)TILESIZE);
-
     for (i=tile_x0; i<(tile_x0+tiles_nx);i++)
     {
         for (j=tile_y0;  j<(tile_y0+tiles_ny); j++)
@@ -1493,7 +1506,7 @@ osm_gps_map_map_redraw (OsmGpsMap *map)
 
     /* clear white background */
     w = gtk_widget_get_allocated_width (widget);
-    h = gtk_widget_get_allocated_width (widget);
+    h = gtk_widget_get_allocated_height (widget);
     draw_white_rectangle(cr, 0, 0, w + EXTRA_BORDER * 2, h + EXTRA_BORDER * 2);
 
     osm_gps_map_fill_tiles_pixel(map, cr);
@@ -1716,10 +1729,9 @@ osm_gps_map_init (OsmGpsMap *object)
     priv->soup_session = soup_session_new ();
     soup_session_set_user_agent (priv->soup_session, USER_AGENT);
 
-    /* Hash table which maps tile d/l URIs to SoupMessage requests, the hashtable
-       must free the key, the soup session unrefs the message */
+    /* hash table maps tile download URIs to cancellables and owns both */
     priv->tile_queue = g_hash_table_new_full (g_str_hash, g_str_equal,
-                                              g_free, NULL);
+                                              g_free, g_object_unref);
 
     //Some mapping providers (Google) have varying degrees of tiles at multiple
     //zoom levels
@@ -2242,9 +2254,9 @@ osm_gps_map_button_press (GtkWidget *widget, GdkEventButton *event)
                         dist_sqrd = (event->x - ptx) * (event->x-ptx) + (event->y-pty) * (event->y-pty);
                         if(dist_sqrd <= ((DOT_RADIUS + 1) * (DOT_RADIUS + 1)))
                         {
-                            OsmGpsMapPoint* newpoint = malloc(sizeof(OsmGpsMapPoint));
-                            osm_gps_map_convert_screen_to_geographic(map, ptx, pty, newpoint);
-                            osm_gps_map_track_insert_point(track, newpoint, ctr);
+                            OsmGpsMapPoint newpoint;
+                            osm_gps_map_convert_screen_to_geographic(map, ptx, pty, &newpoint);
+                            osm_gps_map_track_insert_point(track, &newpoint, ctr);
                             osm_gps_map_map_redraw(map);
                             return FALSE;
                         }
@@ -2301,9 +2313,9 @@ osm_gps_map_button_press (GtkWidget *widget, GdkEventButton *event)
                         dist_sqrd = (event->x - ptx) * (event->x-ptx) + (event->y-pty) * (event->y-pty);
                         if(dist_sqrd <= ((DOT_RADIUS + 1) * (DOT_RADIUS + 1)))
                         {
-                            OsmGpsMapPoint* newpoint = malloc(sizeof(OsmGpsMapPoint));
-                            osm_gps_map_convert_screen_to_geographic(map, ptx, pty, newpoint);
-                            osm_gps_map_track_insert_point(track, newpoint, ctr);
+                            OsmGpsMapPoint newpoint;
+                            osm_gps_map_convert_screen_to_geographic(map, ptx, pty, &newpoint);
+                            osm_gps_map_track_insert_point(track, &newpoint, ctr);
                             osm_gps_map_map_redraw(map);
                             return FALSE;
                         }
@@ -2324,9 +2336,9 @@ osm_gps_map_button_press (GtkWidget *widget, GdkEventButton *event)
                 float dist_sqrd = (event->x - ptx) * (event->x-ptx) + (event->y-pty) * (event->y-pty);
                 if(dist_sqrd <= ((DOT_RADIUS + 1) * (DOT_RADIUS + 1)))
                 {
-                    OsmGpsMapPoint* newpoint = malloc(sizeof(OsmGpsMapPoint));
-                    osm_gps_map_convert_screen_to_geographic(map, ptx, pty, newpoint);
-                    osm_gps_map_track_insert_point(track, newpoint, ctr);
+                    OsmGpsMapPoint newpoint;
+                    osm_gps_map_convert_screen_to_geographic(map, ptx, pty, &newpoint);
+                    osm_gps_map_track_insert_point(track, &newpoint, ctr);
                     osm_gps_map_map_redraw(map);
                     return FALSE;
                 }


=====================================
tests/test.py
=====================================
@@ -1,13 +1,17 @@
 #!/usr/bin/env python3
+import os
+import tempfile
 import unittest
 import cairo
 import io
 
 import gi
 gi.require_version('OsmGpsMap', '1.0')
+gi.require_version('Soup', '3.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, Soup
 
 class TestOsmGpsMap(unittest.TestCase):
 	def setUp(self):
@@ -62,6 +66,54 @@ class TestOsmGpsMap(unittest.TestCase):
 		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
@@ -122,5 +174,120 @@ class TestOsmGpsMap(unittest.TestCase):
 		
 		self.osm.track_remove(track)
 
+	def test_insert_point(self):
+		# Issue #45: check insert_point does not double-free.
+		track = OsmGpsMap.MapTrack()
+		point = OsmGpsMap.MapPoint.new_degrees(self.lat, self.lon)
+		track.insert_point(point, 0)
+		self.assertEqual(track.n_points(), 1)
+
+	def test_pending_tile_download_survives_map_teardown(self):
+		server = Soup.Server()
+		def serve(_server, message, _path, _query):
+			message.pause()
+		server.add_handler(None, serve)
+		server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY)
+		self.addCleanup(server.disconnect)
+		uri = server.get_uris()[0].to_string()
+		osm = OsmGpsMap.Map(user_agent="test/0.1",
+				    repo_uri=uri + "#Z/#X/#Y.png",
+				    tile_cache="none://")
+		criticals = []
+		def on_critical(_domain, _level, message, _data):
+			criticals.append(message)
+		for domain in ("GLib", "GLib-GObject", "Gtk", "OsmGpsMap"):
+			hid = GLib.log_set_handler(domain, GLib.LogLevelFlags.LEVEL_CRITICAL,
+						   on_critical, None)
+			self.addCleanup(GLib.log_remove_handler, domain, hid)
+		window = Gtk.OffscreenWindow()
+		window.set_default_size(256, 256)
+		queued = []
+		osm.connect("notify::tiles-queued", lambda m, _p: queued.append(m.get_property("tiles-queued")))
+		window.add(osm)
+		window.show_all()
+		deadline = GLib.get_monotonic_time() + 1000000
+		while not queued and GLib.get_monotonic_time() < deadline:
+			Gtk.main_iteration_do(False)
+		self.assertTrue(queued)
+		released = []
+		osm.weak_ref(lambda: released.append(True))
+		window.destroy()
+		del osm
+		deadline = GLib.get_monotonic_time() + 1000000
+		while not released and GLib.get_monotonic_time() < deadline:
+			Gtk.main_iteration_do(False)
+		self.assertTrue(released, "Download callbacks did not release the map")
+		self.assertEqual(criticals, [])
+
+	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)
+		self.assertEqual(self.osm.get_property('zoom'),
+				 self.osm.get_property('max-zoom'))
+
+	def test_negative_map_origin_tiles_align_with_overlay(self):
+		# Issue #119: C / truncates toward 0. Negative map-x/map-y must floor so
+		# tiles stay locked to overlays. Zoom 1, 800x800 window, center 0,0
+		# makes both origins negative. Red cached tiles vs white outside the world.
+		cache_dir = tempfile.TemporaryDirectory(prefix="osm-gps-map-tiles-")
+		self.addCleanup(cache_dir.cleanup)
+		cache = cache_dir.name
+		tile = cairo.ImageSurface(cairo.FORMAT_RGB24, 256, 256)
+		cr = cairo.Context(tile)
+		cr.set_source_rgb(1, 0, 0)
+		cr.paint()
+		for x in (0, 1):
+			for y in (0, 1):
+				path = os.path.join(cache, "1", str(x), "%d.png" % y)
+				os.makedirs(os.path.dirname(path), exist_ok=True)
+				tile.write_to_png(path)
+
+		osm = OsmGpsMap.Map(user_agent="test/0.1",
+				    tile_cache=cache,
+				    auto_download=False)
+		window = Gtk.OffscreenWindow()
+		self.addCleanup(window.destroy)
+		window.set_default_size(800, 800)
+		window.add(osm)
+		self.addCleanup(window.remove, osm)
+		window.show_all()
+		osm.set_center_and_zoom(0.0, 0.0, 1)
+
+		deadline = GLib.get_monotonic_time() + 1000000
+		r = g = b = None
+		while GLib.get_monotonic_time() < deadline:
+			Gtk.main_iteration_do(False)
+			if osm.get_property("map-x") >= 0 or osm.get_property("map-y") >= 0:
+				continue
+			pixbuf = window.get_pixbuf()
+			if pixbuf is None or pixbuf.get_width() < 800 or pixbuf.get_height() < 800:
+				continue
+			pt = OsmGpsMap.MapPoint.new_degrees(0.0, 0.0)
+			sx, sy = osm.convert_geographic_to_screen(pt)
+			pixels = pixbuf.get_pixels()
+			nch = pixbuf.get_n_channels()
+			row = pixbuf.get_rowstride()
+			i = int(sy) * row + int(sx) * nch
+			r, g, b = pixels[i], pixels[i + 1], pixels[i + 2]
+			if r + g + b > 0:
+				break
+
+		self.assertLess(osm.get_property("map-x"), 0)
+		self.assertNotEqual(osm.get_property("map-x") % 256, 0)
+		self.assertLess(osm.get_property("map-y"), 0)
+		self.assertNotEqual(osm.get_property("map-y") % 256, 0)
+		self.assertIsNotNone(r)
+		self.assertGreater(r, 200)
+		self.assertLess(g, 50)
+		self.assertLess(b, 50)
+
 if __name__ == "__main__":
 	unittest.main()


=====================================
website/Gemfile
=====================================
@@ -0,0 +1,5 @@
+source "https://rubygems.org"
+
+gem "jekyll", "~> 4.4"
+gem "jekyll-theme-midnight", "~> 0.2"
+gem "jekyll-seo-tag", "~> 2.8"


=====================================
website/Gemfile.lock
=====================================
@@ -0,0 +1,170 @@
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.9.0)
+      public_suffix (>= 2.0.2, < 8.0)
+    base64 (0.3.0)
+    bigdecimal (4.1.2)
+    colorator (1.1.0)
+    concurrent-ruby (1.3.8)
+    csv (3.3.6)
+    em-websocket (0.5.3)
+      eventmachine (>= 0.12.9)
+      http_parser.rb (~> 0)
+    eventmachine (1.2.7)
+    ffi (1.17.4)
+    ffi (1.17.4-aarch64-linux-gnu)
+    ffi (1.17.4-aarch64-linux-musl)
+    ffi (1.17.4-arm-linux-gnu)
+    ffi (1.17.4-arm-linux-musl)
+    ffi (1.17.4-arm64-darwin)
+    ffi (1.17.4-x86-linux-gnu)
+    ffi (1.17.4-x86-linux-musl)
+    ffi (1.17.4-x86_64-darwin)
+    ffi (1.17.4-x86_64-linux-gnu)
+    ffi (1.17.4-x86_64-linux-musl)
+    forwardable-extended (2.6.0)
+    google-protobuf (4.36.1)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-aarch64-linux-gnu)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-aarch64-linux-musl)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-arm64-darwin)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-x86-linux-gnu)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-x86-linux-musl)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-x86_64-darwin)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-x86_64-linux-gnu)
+      bigdecimal
+      rake (~> 13.3)
+    google-protobuf (4.36.1-x86_64-linux-musl)
+      bigdecimal
+      rake (~> 13.3)
+    http_parser.rb (0.8.1)
+    i18n (1.15.2)
+      concurrent-ruby (~> 1.0)
+    jekyll (4.4.1)
+      addressable (~> 2.4)
+      base64 (~> 0.2)
+      colorator (~> 1.0)
+      csv (~> 3.0)
+      em-websocket (~> 0.5)
+      i18n (~> 1.0)
+      jekyll-sass-converter (>= 2.0, < 4.0)
+      jekyll-watch (~> 2.0)
+      json (~> 2.6)
+      kramdown (~> 2.3, >= 2.3.1)
+      kramdown-parser-gfm (~> 1.0)
+      liquid (~> 4.0)
+      mercenary (~> 0.3, >= 0.3.6)
+      pathutil (~> 0.9)
+      rouge (>= 3.0, < 5.0)
+      safe_yaml (~> 1.0)
+      terminal-table (>= 1.8, < 4.0)
+      webrick (~> 1.7)
+    jekyll-sass-converter (3.1.0)
+      sass-embedded (~> 1.75)
+    jekyll-seo-tag (2.9.0)
+      jekyll (>= 3.8, < 5.0)
+    jekyll-theme-midnight (0.2.0)
+      jekyll (> 3.5, < 5.0)
+      jekyll-seo-tag (~> 2.0)
+    jekyll-watch (2.2.1)
+      listen (~> 3.0)
+    json (2.21.2)
+    kramdown (2.5.2)
+      rexml (>= 3.4.4)
+    kramdown-parser-gfm (1.1.0)
+      kramdown (~> 2.0)
+    liquid (4.0.4)
+    listen (3.10.0)
+      logger
+      rb-fsevent (~> 0.10, >= 0.10.3)
+      rb-inotify (~> 0.9, >= 0.9.10)
+    logger (1.7.0)
+    mercenary (0.4.0)
+    pathutil (0.16.2)
+      forwardable-extended (~> 2.6)
+    public_suffix (7.0.5)
+    rake (13.4.2)
+    rb-fsevent (0.11.2)
+    rb-inotify (0.11.1)
+      ffi (~> 1.0)
+    rexml (3.4.4)
+    rouge (4.7.0)
+    safe_yaml (1.0.5)
+    sass-embedded (1.104.0)
+      google-protobuf (~> 4.31)
+      rake (>= 13)
+    sass-embedded (1.104.0-aarch64-linux-android)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-aarch64-linux-gnu)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-aarch64-linux-musl)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-arm-linux-androideabi)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-arm-linux-gnueabihf)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-arm-linux-musleabihf)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-arm64-darwin)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-riscv64-linux-android)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-riscv64-linux-gnu)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-riscv64-linux-musl)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-x86_64-darwin)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-x86_64-linux-android)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-x86_64-linux-gnu)
+      google-protobuf (~> 4.31)
+    sass-embedded (1.104.0-x86_64-linux-musl)
+      google-protobuf (~> 4.31)
+    terminal-table (3.0.2)
+      unicode-display_width (>= 1.1.1, < 3)
+    unicode-display_width (2.6.0)
+    webrick (1.9.2)
+
+PLATFORMS
+  aarch64-linux-android
+  aarch64-linux-gnu
+  aarch64-linux-musl
+  arm-linux-androideabi
+  arm-linux-gnu
+  arm-linux-gnueabihf
+  arm-linux-musl
+  arm-linux-musleabihf
+  arm64-darwin
+  riscv64-linux-android
+  riscv64-linux-gnu
+  riscv64-linux-musl
+  ruby
+  x86-linux-gnu
+  x86-linux-musl
+  x86_64-darwin
+  x86_64-linux-android
+  x86_64-linux-gnu
+  x86_64-linux-musl
+
+DEPENDENCIES
+  jekyll (~> 4.4)
+  jekyll-seo-tag (~> 2.8)
+  jekyll-theme-midnight (~> 0.2)
+
+BUNDLED WITH
+   2.6.9


=====================================
website/_config.yml
=====================================
@@ -0,0 +1,19 @@
+title: osm-gps-map
+tagline: A Gtk+ Widget for Displaying OpenStreetMap tiles.
+description: A Gtk+ widget (and Python bindings) for displaying OpenStreetMap tiles.
+theme: jekyll-theme-midnight
+url: https://nzjrs.github.io
+baseurl: /osm-gps-map
+show_downloads: true
+plugins:
+  - jekyll-seo-tag
+github:
+  repository_url: https://github.com/nzjrs/osm-gps-map
+  owner_url: https://github.com/nzjrs
+  owner_name: nzjrs
+  zip_url: https://github.com/nzjrs/osm-gps-map/archive/refs/heads/master.zip
+  tar_url: https://github.com/nzjrs/osm-gps-map/archive/refs/heads/master.tar.gz
+exclude:
+  - Gemfile
+  - Gemfile.lock
+  - vendor


=====================================
website/_data/releases.json
=====================================
@@ -0,0 +1,8 @@
+{
+  "repo": "nzjrs/osm-gps-map",
+  "clone_url": "https://github.com/nzjrs/osm-gps-map.git",
+  "master_tar": "https://github.com/nzjrs/osm-gps-map/archive/refs/heads/master.tar.gz",
+  "master_zip": "https://github.com/nzjrs/osm-gps-map/archive/refs/heads/master.zip",
+  "latest": null,
+  "archives": []
+}


=====================================
website/index.md
=====================================
@@ -0,0 +1,133 @@
+---
+layout: default
+permalink: /
+---
+
+{% if site.data.releases.latest %}
+<p>Download <a href="{{ site.data.releases.latest.archive_url }}">{{ site.data.releases.latest.tag }} archive</a>
+(<a href="{{ site.data.releases.latest.html_url }}">Release notes for {{ site.data.releases.latest.tag }}</a>).</p>
+{% else %}
+<p>No GitHub Release yet. Tag-only publishes gtk-doc; Download fills in when a Release exists.</p>
+{% endif %}
+
+A Gtk+ widget (and Python bindings) that when given GPS co-ordinates, draws a GPS track, and points of interest on a moving map display. Downloads map data from a number of websites, including openstreetmap.org. The library has excellent performance and is currently used in a number of Gtk+ applications.
+
+![osm-gps-map showing an OpenStreetMap track in Christchurch]({{ '/screenshot.png' | relative_url }})
+
+![osm-gps-map with on-screen display over satellite imagery]({{ '/screenshot-osd.png' | relative_url }})
+
+Currently supports a number of different mapping sources:
+
+- OpenStreetMap (default)
+- OpenTopoMap
+- OpenCycleMap
+- Maps-For-Free
+- Google Maps / Satellite and Bing Maps (Virtual Earth)
+
+It also has the following features:
+
+- Intelligent, flexible and customizable caching of maps, including the ability to request a specific area of the map to be cached ahead of time
+- Recording of points of interest on the map (and the ability to add arbitrary pixmaps at those points)
+- Automatically draws a GPS track (a line showing the history of past added points)
+- Automatic centering on new GPS points
+- Support for multiple other tracks of co-ordinate points
+- Adjustable Zoom
+- Built in support for keyboard navigation
+- Includes a [set of examples](https://github.com/{{ site.data.releases.repo }}/tree/master/examples/)
+- Simple, [flat API](https://github.com/{{ site.data.releases.repo }}/blob/master/src/osm-gps-map-widget.h)
+- Support for showing additional display layers rendered on top of the map
+- Optional on screen display (OSD)
+- Proper map copyright attribution and user-agent usage to comply with various map requirements
+
+## Documentation
+
+- [API Documentation]({{ '/docs/reference/html/index.html' | relative_url }})
+- Code examples
+  - [mapviewer.c](https://github.com/{{ site.data.releases.repo }}/blob/master/examples/mapviewer.c)
+  - [mapviewer.py](https://github.com/{{ site.data.releases.repo }}/blob/master/examples/mapviewer.py)
+  - [mapviewer.js](https://github.com/{{ site.data.releases.repo }}/blob/master/examples/mapviewer.js) (JavaScript example using GObject Introspection)
+  - [polygon.c](https://github.com/{{ site.data.releases.repo }}/blob/master/examples/polygon.c) (small polygon-on-map drawing example)
+  - [editable_track.c](https://github.com/{{ site.data.releases.repo }}/blob/master/examples/editable_track.c) (small example of an editable track feature)
+- Please e-mail the [mailing list](https://groups.google.com/group/osm-gps-map) for all questions.
+- Report issues using the canonical [issue tracker](https://github.com/nzjrs/osm-gps-map/issues)
+
+## Install Instructions
+
+osm-gps-map should be packaged by your distribution. On Debian, Ubuntu or similar, you can install the GTK+ library and its Python bindings like this:
+
+```
+sudo apt install libosmgpsmap-1.0-1 gir1.2-osmgpsmap-1.0 python3-gi
+```
+
+And if you want to build some application that depends on osm-gps-map, you can install the development files like this:
+
+```
+sudo apt install libosmgpsmap-1.0-dev
+```
+
+### Installing From Source
+
+To build from source on Debian or Ubuntu you will need:
+
+```
+sudo apt install build-essential autoconf automake libtool \
+  libgtk-3-dev libsoup-3.0-dev libgirepository1.0-dev
+```
+
+Then:
+
+```
+./autogen.sh && make && sudo make install
+```
+
+{% if site.data.releases.latest and site.data.releases.latest.changes.size > 0 %}
+## News
+
+<p>Changes in {{ site.data.releases.latest.tag }} ({{ site.data.releases.latest.date }})</p>
+<ul>
+{% for item in site.data.releases.latest.changes %}
+  <li>{{ item }}</li>
+{% endfor %}
+</ul>
+{% endif %}
+
+## Download
+
+You may download any of the following archives:
+
+{% if site.data.releases.archives.size > 0 %}
+<ul>
+{% for rel in site.data.releases.archives %}
+  <li><a href="{{ rel.archive_url }}">{{ rel.tag }} archive</a> (<a href="{{ rel.html_url }}">Release notes for {{ rel.tag }}</a>)</li>
+{% endfor %}
+</ul>
+{% else %}
+<ul><li>(no GitHub Releases yet)</li></ul>
+{% endif %}
+
+You can also clone the project with [Git](https://git-scm.com) by running:
+
+```
+$ git clone {{ site.data.releases.clone_url }}
+```
+
+A snapshot of master is also available as [tar]({{ site.data.releases.master_tar }}) or [zip]({{ site.data.releases.master_zip }}).
+
+## License
+
+- GPLv2+
+
+## Authors
+
+- John Stowers
+- Till Harbaum
+- Alberto Mardegan
+- Mark Cottrell
+- Originally based on tangoGPS by Marcus Bauer
+
+## Contact
+
+- osm-gps-map [mailing list](https://groups.google.com/group/osm-gps-map)
+- Canonical [issue tracker](https://github.com/nzjrs/osm-gps-map/issues)
+
+get the source code on GitHub : [{{ site.data.releases.repo }}](https://github.com/{{ site.data.releases.repo }})


=====================================
website/screenshot-osd.png
=====================================
Binary files /dev/null and b/website/screenshot-osd.png differ


=====================================
website/screenshot.png
=====================================
Binary files /dev/null and b/website/screenshot.png differ



View it on GitLab: https://salsa.debian.org/debian-gis-team/osm-gps-map/-/commit/04511e21b5faf9568ac3e143863526926ae51b50

-- 
View it on GitLab: https://salsa.debian.org/debian-gis-team/osm-gps-map/-/commit/04511e21b5faf9568ac3e143863526926ae51b50
You're receiving this email because of your account on salsa.debian.org. Manage all notifications: https://salsa.debian.org/-/profile/notifications | Help: https://salsa.debian.org/help


-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20261012/0b570c8a/attachment-0001.htm>


More information about the Pkg-grass-devel mailing list