[libshumate] Add support for rotation



commit c54ed6c66b72a2d8016319c5c991a4c6a506109c
Author: James Westman <james jwestman net>
Date:   Wed May 26 19:57:27 2021 -0500

    Add support for rotation
    
    Add support for rotating the map using touchscreen/trackpad gestures.
    This involved changing the gesture tracking code to support zooming and
    rotating at the same time. Also, some of the viewport methods had to be
    changed since the calculations now depend on both coordinates.

 shumate/shumate-map-layer.c    |  35 ++++++-
 shumate/shumate-map.c          | 207 ++++++++++++++++++++++++---------------
 shumate/shumate-marker-layer.c |  21 +++-
 shumate/shumate-path-layer.c   |  19 +++-
 shumate/shumate-viewport.c     | 213 +++++++++++++++++++++++++----------------
 shumate/shumate-viewport.h     |  28 +++---
 6 files changed, 336 insertions(+), 187 deletions(-)
---
diff --git a/shumate/shumate-map-layer.c b/shumate/shumate-map-layer.c
index 06ba5bf..a7cd28b 100644
--- a/shumate/shumate-map-layer.c
+++ b/shumate/shumate-map-layer.c
@@ -209,12 +209,23 @@ recompute_grid (ShumateMapLayer *self)
   int source_rows = shumate_map_source_get_row_count (self->map_source, zoom_level);
   int source_columns = shumate_map_source_get_column_count (self->map_source, zoom_level);
 
+  double rotation = shumate_viewport_get_rotation (viewport);
+
+  int size_x = MAX (
+    abs (cos (rotation) *  width/2.0 - sin (rotation) * height/2.0),
+    abs (cos (rotation) * -width/2.0 - sin (rotation) * height/2.0)
+  );
+  int size_y = MAX (
+    abs (sin (rotation) *  width/2.0 + cos (rotation) * height/2.0),
+    abs (sin (rotation) * -width/2.0 + cos (rotation) * height/2.0)
+  );
+
   // This is the (column, row) of the top left tile
-  int tile_initial_column = floor ((longitude_x - width/2) / (double) tile_size);
-  int tile_initial_row = floor ((latitude_y - height/2) / (double) tile_size);
+  int tile_initial_column = floor ((longitude_x - size_x) / (double) tile_size);
+  int tile_initial_row = floor ((latitude_y - size_y) / (double) tile_size);
 
-  int required_columns = (width / tile_size) + 2;
-  int required_rows = (height / tile_size) + 2;
+  int required_columns = (size_x * 2 / tile_size) + 2;
+  int required_rows = (size_y * 2 / tile_size) + 2;
 
   gboolean all_filled = TRUE;
 
@@ -342,6 +353,16 @@ on_view_zoom_level_changed (ShumateMapLayer *self,
   gtk_widget_queue_allocate (GTK_WIDGET (self));
 }
 
+static void
+on_view_rotation_changed (ShumateMapLayer *self,
+                          GParamSpec      *pspec,
+                          ShumateViewport *view)
+{
+  g_assert (SHUMATE_IS_MAP_LAYER (self));
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
 static void
 shumate_map_layer_set_property (GObject      *object,
                                 guint         property_id,
@@ -414,6 +435,8 @@ shumate_map_layer_constructed (GObject *object)
   g_signal_connect_swapped (viewport, "notify::longitude", G_CALLBACK (on_view_longitude_changed), self);
   g_signal_connect_swapped (viewport, "notify::latitude", G_CALLBACK (on_view_latitude_changed), self);
   g_signal_connect_swapped (viewport, "notify::zoom-level", G_CALLBACK (on_view_zoom_level_changed), self);
+  g_signal_connect_swapped (viewport, "notify::rotation", G_CALLBACK (on_view_rotation_changed), self);
+
 }
 
 static void
@@ -502,10 +525,12 @@ shumate_map_layer_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
   double extra_zoom = fmod (zoom_level, 1.0) + 1.0;
   int width = gtk_widget_get_width (GTK_WIDGET (self));
   int height = gtk_widget_get_height (GTK_WIDGET (self));
+  double rotation = shumate_viewport_get_rotation (viewport);
 
-  /* Scale around the center of the view */
+  /* Scale and rotate around the center of the view */
   gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (width / 2.0, height / 2.0));
   gtk_snapshot_scale (snapshot, extra_zoom, extra_zoom);
+  gtk_snapshot_rotate (snapshot, rotation * 180 / G_PI);
   gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-width / 2.0, -height / 2.0));
 
   GTK_WIDGET_CLASS (shumate_map_layer_parent_class)->snapshot (widget, snapshot);
diff --git a/shumate/shumate-map.c b/shumate/shumate-map.c
index 4571a32..d39729e 100644
--- a/shumate/shumate-map.c
+++ b/shumate/shumate-map.c
@@ -154,18 +154,70 @@ typedef struct
   double current_y;
 
   double zoom_level_begin;
-  double zoom_center_x;
-  double zoom_center_y;
+  double rotate_begin;
 
   double focus_lat;
   double focus_lon;
   double accumulated_scroll_dy;
-  double drag_begin_lat;
-  double drag_begin_lon;
+  double gesture_begin_lat;
+  double gesture_begin_lon;
+  double drag_begin_x;
+  double drag_begin_y;
 } ShumateMapPrivate;
 
 G_DEFINE_TYPE_WITH_PRIVATE (ShumateMap, shumate_map, GTK_TYPE_WIDGET);
 
+static double
+positive_mod (double i, double n)
+{
+  return fmod (fmod (i, n) + n, n);
+}
+
+static void
+move_location_to_coords (ShumateMap *self,
+                         double lat,
+                         double lon,
+                         double x,
+                         double y)
+{
+  ShumateMapPrivate *priv = shumate_map_get_instance_private (self);
+  ShumateMapSource *map_source = shumate_viewport_get_reference_map_source (priv->viewport);
+  double zoom_level = shumate_viewport_get_zoom_level (priv->viewport);
+  double tile_size, map_width, map_height;
+  double map_x, map_y;
+  double target_lat, target_lon;
+  double target_map_x, target_map_y;
+  double current_lat, current_lon;
+  double current_map_x, current_map_y;
+  double new_map_x, new_map_y;
+
+  if (map_source == NULL)
+    return;
+
+  tile_size = shumate_map_source_get_tile_size (map_source) * (fmod (zoom_level, 1.0) + 1.0);
+  map_width = tile_size * shumate_map_source_get_column_count (map_source, zoom_level);
+  map_height = tile_size * shumate_map_source_get_row_count (map_source, zoom_level);
+
+  map_x = shumate_map_source_get_x (map_source, zoom_level, lon);
+  map_y = shumate_map_source_get_y (map_source, zoom_level, lat);
+
+  current_lat = shumate_location_get_latitude (SHUMATE_LOCATION (priv->viewport));
+  current_lon = shumate_location_get_longitude (SHUMATE_LOCATION (priv->viewport));
+  current_map_x = shumate_map_source_get_x (map_source, zoom_level, current_lon);
+  current_map_y = shumate_map_source_get_y (map_source, zoom_level, current_lat);
+
+  shumate_viewport_widget_coords_to_location (priv->viewport, GTK_WIDGET (self), x, y, &target_lat, 
&target_lon);
+  target_map_x = shumate_map_source_get_x (map_source, zoom_level, target_lon);
+  target_map_y = shumate_map_source_get_y (map_source, zoom_level, target_lat);
+
+  new_map_x = positive_mod (current_map_x - (target_map_x - map_x), map_width);
+  new_map_y = positive_mod (current_map_y - (target_map_y - map_y), map_height);
+
+  shumate_location_set_location (SHUMATE_LOCATION (priv->viewport),
+                                 shumate_map_source_get_latitude (map_source, zoom_level, new_map_y),
+                                 shumate_map_source_get_longitude (map_source, zoom_level, new_map_x));
+}
+
 static void
 move_viewport_from_pixel_offset (ShumateMap *self,
                                  double      latitude,
@@ -177,8 +229,6 @@ move_viewport_from_pixel_offset (ShumateMap *self,
   ShumateMapSource *map_source;
   double x, y;
   double lat, lon;
-  double zoom_level;
-  double tile_size, max_x, max_y;
 
   g_assert (SHUMATE_IS_MAP (self));
 
@@ -186,24 +236,15 @@ move_viewport_from_pixel_offset (ShumateMap *self,
   if (!map_source)
     return;
 
-  zoom_level = shumate_viewport_get_zoom_level (priv->viewport);
-  x = shumate_map_source_get_x (map_source, zoom_level, longitude) - offset_x;
-  y = shumate_map_source_get_y (map_source, zoom_level, latitude) - offset_y;
-
-  tile_size = shumate_map_source_get_tile_size (map_source) * (fmod (zoom_level, 1.0) + 1.0);
-  max_x = shumate_map_source_get_column_count (map_source, zoom_level) * tile_size;
-  max_y = shumate_map_source_get_row_count (map_source, zoom_level) * tile_size;
+  shumate_viewport_location_to_widget_coords (priv->viewport, GTK_WIDGET (self), latitude, longitude, &x, 
&y);
 
-  x = fmod (x, max_x);
-  if (x < 0)
-    x += max_x;
+  x -= offset_x;
+  y -= offset_y;
 
-  y = fmod (y, max_y);
-  if (y < 0)
-    y += max_y;
+  shumate_viewport_widget_coords_to_location (priv->viewport, GTK_WIDGET (self), x, y, &lat, &lon);
 
-  lat = shumate_map_source_get_latitude (map_source, zoom_level, y);
-  lon = shumate_map_source_get_longitude (map_source, zoom_level, x);
+  lat = fmod (lat + 90, 180) - 90;
+  lon = fmod (lon + 180, 360) - 180;
 
   shumate_location_set_location (SHUMATE_LOCATION (priv->viewport), lat, lon);
 }
@@ -389,8 +430,13 @@ on_drag_gesture_drag_begin (ShumateMap     *self,
 
   cancel_deceleration (self);
 
-  priv->drag_begin_lon = shumate_location_get_longitude (SHUMATE_LOCATION (priv->viewport));
-  priv->drag_begin_lat = shumate_location_get_latitude (SHUMATE_LOCATION (priv->viewport));
+  priv->drag_begin_x = start_x;
+  priv->drag_begin_y = start_y;
+
+  shumate_viewport_widget_coords_to_location (priv->viewport, GTK_WIDGET (self),
+                                              start_x, start_y,
+                                              &priv->gesture_begin_lat,
+                                              &priv->gesture_begin_lon);
 
   gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grabbing");
 }
@@ -403,11 +449,11 @@ on_drag_gesture_drag_update (ShumateMap     *self,
 {
   ShumateMapPrivate *priv = shumate_map_get_instance_private (self);
 
-  move_viewport_from_pixel_offset (self,
-                                   priv->drag_begin_lat,
-                                   priv->drag_begin_lon,
-                                   offset_x,
-                                   offset_y);
+  move_location_to_coords (self,
+                           priv->gesture_begin_lat,
+                           priv->gesture_begin_lon,
+                           priv->drag_begin_x + offset_x,
+                           priv->drag_begin_y + offset_y);
 }
 
 static void
@@ -422,14 +468,8 @@ on_drag_gesture_drag_end (ShumateMap     *self,
 
   gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "grab");
 
-  move_viewport_from_pixel_offset (self,
-                                   priv->drag_begin_lat,
-                                   priv->drag_begin_lon,
-                                   offset_x,
-                                   offset_y);
-
-  priv->drag_begin_lon = 0;
-  priv->drag_begin_lat = 0;
+  priv->gesture_begin_lon = 0;
+  priv->gesture_begin_lat = 0;
 }
 
 static void
@@ -447,41 +487,22 @@ set_zoom_level (ShumateMap *self,
 {
   ShumateMapPrivate *priv = shumate_map_get_instance_private (self);
   ShumateMapSource *map_source;
-  double scroll_latitude, scroll_longitude;
-  double view_lon, view_lat;
+  double lat, lon;
 
   g_object_freeze_notify (G_OBJECT (priv->viewport));
-  view_lon = shumate_location_get_longitude (SHUMATE_LOCATION (priv->viewport));
-  view_lat = shumate_location_get_latitude (SHUMATE_LOCATION (priv->viewport));
 
   map_source = shumate_viewport_get_reference_map_source (priv->viewport);
   if (map_source)
-    {
-      scroll_longitude = shumate_viewport_widget_x_to_longitude (priv->viewport, GTK_WIDGET (self), 
priv->current_x);
-      scroll_latitude = shumate_viewport_widget_y_to_latitude (priv->viewport, GTK_WIDGET (self), 
priv->current_y);
-    }
+    shumate_viewport_widget_coords_to_location (priv->viewport,
+                                                GTK_WIDGET (self),
+                                                priv->current_x, priv->current_y,
+                                                &lat, &lon);
 
   shumate_viewport_set_zoom_level (priv->viewport, zoom_level);
 
   if (map_source)
-    {
-      double scroll_map_x, scroll_map_y;
-      double view_center_x, view_center_y;
-      double x_offset, y_offset;
-      double zoom_level;
-
-      scroll_map_x = shumate_viewport_longitude_to_widget_x (priv->viewport, GTK_WIDGET (self), 
scroll_longitude);
-      scroll_map_y = shumate_viewport_latitude_to_widget_y (priv->viewport, GTK_WIDGET (self), 
scroll_latitude);
-
-      zoom_level = shumate_viewport_get_zoom_level (priv->viewport);
-      view_center_x = shumate_map_source_get_x (map_source, zoom_level, view_lon);
-      view_center_y = shumate_map_source_get_y (map_source, zoom_level, view_lat);
-      x_offset = scroll_map_x - priv->current_x;
-      y_offset = scroll_map_y - priv->current_y;
-      shumate_location_set_location (SHUMATE_LOCATION (priv->viewport),
-                                     shumate_map_source_get_latitude (map_source, zoom_level, view_center_y 
+ y_offset),
-                                     shumate_map_source_get_longitude (map_source, zoom_level, view_center_x 
+ x_offset));
-    }
+    move_location_to_coords (self, lat, lon, priv->current_x, priv->current_y);
+
   g_object_thaw_notify (G_OBJECT (priv->viewport));
 }
 
@@ -512,15 +533,18 @@ on_zoom_gesture_begin (ShumateMap       *self,
 {
   ShumateMapPrivate *priv = shumate_map_get_instance_private (self);
   double zoom_level = shumate_viewport_get_zoom_level (priv->viewport);
+  double x, y;
 
   gtk_gesture_set_state (GTK_GESTURE (zoom), GTK_EVENT_SEQUENCE_CLAIMED);
   cancel_deceleration (self);
 
   priv->zoom_level_begin = zoom_level;
 
-  gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &priv->zoom_center_x, &priv->zoom_center_y);
-  priv->drag_begin_lon = shumate_location_get_longitude (SHUMATE_LOCATION (priv->viewport));
-  priv->drag_begin_lat = shumate_location_get_latitude (SHUMATE_LOCATION (priv->viewport));
+  gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &x, &y);
+  shumate_viewport_widget_coords_to_location (priv->viewport, GTK_WIDGET (self),
+                                              x, y,
+                                              &priv->gesture_begin_lat,
+                                              &priv->gesture_begin_lon);
 }
 
 static void
@@ -533,21 +557,42 @@ on_zoom_gesture_update (ShumateMap       *self,
   double scale = gtk_gesture_zoom_get_scale_delta (zoom);
 
   gtk_gesture_get_bounding_box_center (GTK_GESTURE (zoom), &x, &y);
+  shumate_viewport_set_zoom_level (priv->viewport, priv->zoom_level_begin + log (scale) / G_LN2);
+  move_location_to_coords (self, priv->gesture_begin_lat, priv->gesture_begin_lon, x, y);
+}
 
-  move_viewport_from_pixel_offset (self,
-                                   priv->drag_begin_lat,
-                                   priv->drag_begin_lon,
-                                   x - priv->zoom_center_x,
-                                   y - priv->zoom_center_y);
+static void
+on_rotate_gesture_begin (ShumateMap *self,
+                         GdkEventSequence *seq,
+                         GtkGestureRotate *rotate)
+{
+  ShumateMapPrivate *priv = shumate_map_get_instance_private (self);
+  double rotation = shumate_viewport_get_rotation (priv->viewport);
 
-  priv->current_x = x;
-  priv->current_y = y;
-  set_zoom_level (self, priv->zoom_level_begin + log (scale) / G_LN2);
+  gtk_gesture_set_state (GTK_GESTURE (rotate), GTK_EVENT_SEQUENCE_CLAIMED);
+  cancel_deceleration (self);
+
+  priv->rotate_begin = rotation;
+}
+
+static void
+on_rotate_gesture_update (ShumateMap *self,
+                          GdkEventSequence *seq,
+                          GtkGestureRotate *rotate)
+{
+  ShumateMapPrivate *priv = shumate_map_get_instance_private (self);
+  double rotation;
+  double x, y;
+
+  rotation = gtk_gesture_rotate_get_angle_delta (rotate) + priv->rotate_begin;
 
-  priv->drag_begin_lon = shumate_location_get_longitude (SHUMATE_LOCATION (priv->viewport));
-  priv->drag_begin_lat = shumate_location_get_latitude (SHUMATE_LOCATION (priv->viewport));
-  priv->zoom_center_x = x;
-  priv->zoom_center_y = y;
+  /* snap to due north */
+  if (fabs (fmod (rotation - 0.25, G_PI * 2)) < 0.5)
+    rotation = 0.0;
+
+  shumate_viewport_set_rotation (priv->viewport, rotation);
+  gtk_gesture_get_bounding_box_center (GTK_GESTURE (rotate), &x, &y);
+  move_location_to_coords (self, priv->gesture_begin_lat, priv->gesture_begin_lon, x, y);
 }
 
 static void
@@ -775,6 +820,7 @@ shumate_map_init (ShumateMap *self)
   GtkEventController *motion_controller;
   GtkGesture *swipe_gesture;
   GtkGesture *zoom_gesture;
+  GtkGesture *rotate_gesture;
 
   priv->viewport = shumate_viewport_new ();
   priv->zoom_on_double_click = TRUE;
@@ -818,6 +864,13 @@ shumate_map_init (ShumateMap *self)
   g_signal_connect_swapped (motion_controller, "motion", G_CALLBACK (on_motion_controller_motion), self);
   gtk_widget_add_controller (GTK_WIDGET (self), motion_controller);
 
+  rotate_gesture = gtk_gesture_rotate_new ();
+  g_signal_connect_swapped (rotate_gesture, "begin", G_CALLBACK (on_rotate_gesture_begin), self);
+  g_signal_connect_swapped (rotate_gesture, "update", G_CALLBACK (on_rotate_gesture_update), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (rotate_gesture));
+
+  gtk_gesture_group (zoom_gesture, rotate_gesture);
+
   gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
 }
 
diff --git a/shumate/shumate-marker-layer.c b/shumate/shumate-marker-layer.c
index d76b243..fe97e06 100644
--- a/shumate/shumate-marker-layer.c
+++ b/shumate/shumate-marker-layer.c
@@ -123,8 +123,9 @@ update_marker_visibility (ShumateMarkerLayer *layer,
   gtk_widget_measure (GTK_WIDGET (marker), GTK_ORIENTATION_HORIZONTAL, -1, 0, &marker_width, NULL, NULL);
   gtk_widget_measure (GTK_WIDGET (marker), GTK_ORIENTATION_VERTICAL, -1, 0, &marker_height, NULL, NULL);
 
-  x = floorf (shumate_viewport_longitude_to_widget_x (viewport, GTK_WIDGET (layer), lon) - marker_width/2.f);
-  y = floorf (shumate_viewport_latitude_to_widget_y (viewport, GTK_WIDGET (layer), lat) - marker_height/2.f);
+  shumate_viewport_location_to_widget_coords (viewport, GTK_WIDGET (layer), lat, lon, &x, &y);
+  x = floorf (x - marker_width/2.f);
+  y = floorf (y - marker_height/2.f);
 
   within_viewport = x > -marker_width && x <= width &&
                     y > -marker_height && y <= height &&
@@ -186,6 +187,16 @@ on_view_zoom_level_changed (ShumateMarkerLayer *self,
   shumate_marker_layer_reposition_markers (self);
 }
 
+static void
+on_view_rotation_changed (ShumateMarkerLayer *self,
+                          GParamSpec      *pspec,
+                          ShumateViewport *view)
+{
+  g_assert (SHUMATE_IS_MARKER_LAYER (self));
+
+  shumate_marker_layer_reposition_markers (self);
+}
+
 static void
 shumate_marker_layer_size_allocate (GtkWidget *widget,
                                     int        width,
@@ -217,8 +228,9 @@ shumate_marker_layer_size_allocate (GtkWidget *widget,
       gtk_widget_measure (child, GTK_ORIENTATION_HORIZONTAL, -1, 0, &marker_width, NULL, NULL);
       gtk_widget_measure (child, GTK_ORIENTATION_VERTICAL, -1, 0, &marker_height, NULL, NULL);
 
-      x = floorf (shumate_viewport_longitude_to_widget_x (viewport, widget, lon) - marker_width/2.f);
-      y = floorf (shumate_viewport_latitude_to_widget_y (viewport, widget, lat) - marker_height/2.f);
+      shumate_viewport_location_to_widget_coords (viewport, widget, lat, lon, &x, &y);
+      x = floorf (x - marker_width/2.f);
+      y = floorf (y - marker_height/2.f);
 
       allocation.x = x;
       allocation.y = y;
@@ -312,6 +324,7 @@ shumate_marker_layer_constructed (GObject *object)
   g_signal_connect_swapped (viewport, "notify::longitude", G_CALLBACK (on_view_longitude_changed), self);
   g_signal_connect_swapped (viewport, "notify::latitude", G_CALLBACK (on_view_latitude_changed), self);
   g_signal_connect_swapped (viewport, "notify::zoom-level", G_CALLBACK (on_view_zoom_level_changed), self);
+  g_signal_connect_swapped (viewport, "notify::rotation", G_CALLBACK (on_view_rotation_changed), self);
 
 }
 
diff --git a/shumate/shumate-path-layer.c b/shumate/shumate-path-layer.c
index e6319e6..d61cc2f 100644
--- a/shumate/shumate-path-layer.c
+++ b/shumate/shumate-path-layer.c
@@ -107,6 +107,16 @@ on_view_zoom_level_changed (ShumatePathLayer *self,
   gtk_widget_queue_draw (GTK_WIDGET (self));
 }
 
+static void
+on_view_rotation_changed (ShumatePathLayer *self,
+                          GParamSpec       *pspec,
+                          ShumateViewport  *view)
+{
+  g_assert (SHUMATE_IS_PATH_LAYER (self));
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
 
 static void
 shumate_path_layer_get_property (GObject *object,
@@ -235,7 +245,7 @@ shumate_path_layer_constructed (GObject *object)
   g_signal_connect_swapped (viewport, "notify::longitude", G_CALLBACK (on_view_longitude_changed), self);
   g_signal_connect_swapped (viewport, "notify::latitude", G_CALLBACK (on_view_latitude_changed), self);
   g_signal_connect_swapped (viewport, "notify::zoom-level", G_CALLBACK (on_view_zoom_level_changed), self);
-
+  g_signal_connect_swapped (viewport, "notify::rotation", G_CALLBACK (on_view_rotation_changed), self);
 }
 
 
@@ -278,10 +288,11 @@ shumate_path_layer_snapshot (GtkWidget   *widget,
   for (elem = priv->nodes; elem != NULL; elem = elem->next)
     {
       ShumateLocation *location = SHUMATE_LOCATION (elem->data);
-      double x, y;
+      double x, y, lat, lon;
 
-      x = shumate_viewport_longitude_to_widget_x (viewport, widget, shumate_location_get_longitude 
(location));
-      y = shumate_viewport_latitude_to_widget_y (viewport, widget, shumate_location_get_latitude (location));
+      lat = shumate_location_get_latitude (location);
+      lon = shumate_location_get_longitude (location);
+      shumate_viewport_location_to_widget_coords (viewport, widget, lat, lon, &x, &y);
 
       cairo_line_to (cr, x, y);
     }
diff --git a/shumate/shumate-viewport.c b/shumate/shumate-viewport.c
index bbfea60..9774c14 100644
--- a/shumate/shumate-viewport.c
+++ b/shumate/shumate-viewport.c
@@ -43,6 +43,7 @@ struct _ShumateViewport
   double zoom_level;
   guint min_zoom_level;
   guint max_zoom_level;
+  double rotation;
 
   ShumateMapSource *ref_map_source;
 };
@@ -58,6 +59,7 @@ enum
   PROP_MIN_ZOOM_LEVEL,
   PROP_MAX_ZOOM_LEVEL,
   PROP_REFERENCE_MAP_SOURCE,
+  PROP_ROTATION,
   N_PROPERTIES,
 
   PROP_LONGITUDE,
@@ -127,6 +129,10 @@ shumate_viewport_get_property (GObject    *object,
       g_value_set_object (value, self->ref_map_source);
       break;
 
+    case PROP_ROTATION:
+      g_value_set_double (value, self->rotation);
+      break;
+
     case PROP_LONGITUDE:
       g_value_set_double (value, self->lon);
       break;
@@ -167,6 +173,10 @@ shumate_viewport_set_property (GObject      *object,
       shumate_viewport_set_reference_map_source (self, g_value_get_object (value));
       break;
 
+    case PROP_ROTATION:
+      shumate_viewport_set_rotation (self, g_value_get_double (value));
+      break;
+
     case PROP_LONGITUDE:
       self->lon = CLAMP (g_value_get_double (value), SHUMATE_MIN_LONGITUDE, SHUMATE_MAX_LONGITUDE);
       g_object_notify (object, "longitude");
@@ -249,6 +259,18 @@ shumate_viewport_class_init (ShumateViewportClass *klass)
                          "The reference map source being displayed",
                          SHUMATE_TYPE_MAP_SOURCE,
                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * ShumateViewport:rotation:
+   *
+   * The rotation of the map view, in radians clockwise from up being due north
+   */
+  obj_properties[PROP_ROTATION] =
+    g_param_spec_double ("rotation",
+                         "Rotation",
+                         "The rotation of the map view in radians",
+                         0, G_PI * 2.0, 0,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
   
   g_object_class_install_properties (object_class,
                                      N_PROPERTIES,
@@ -480,139 +502,160 @@ shumate_viewport_get_reference_map_source (ShumateViewport *self)
 }
 
 /**
- * shumate_viewport_widget_x_to_longitude:
+ * shumate_viewport_set_rotation:
  * @self: a #ShumateViewport
- * @widget: a #GtkWidget that uses @self as viewport
- * @x: the x coordinate
+ * @rotation: the rotation
  *
- * Get the longitude from an x coordinate of a widget.
- * The widget is assumed to be using the viewport.
- * 
- * Returns: the longitude
+ * Sets the rotation
  */
-double
-shumate_viewport_widget_x_to_longitude (ShumateViewport *self,
-                                        GtkWidget       *widget,
-                                        double           x)
+void
+shumate_viewport_set_rotation (ShumateViewport *self,
+                               double           rotation)
 {
-  double center_x;
-  int width;
+  g_return_if_fail (SHUMATE_IS_VIEWPORT (self));
 
-  g_return_val_if_fail (SHUMATE_IS_VIEWPORT (self), 0.0);
-  g_return_val_if_fail (GTK_IS_WIDGET (widget), 0.0);
+  rotation = fmod (rotation, G_PI * 2.0);
 
-  if (!self->ref_map_source)
-    {
-      g_critical ("A reference map source is required to compute the longitude from X.");
-      return 0.0;
-    }
+  if (self->rotation == rotation)
+    return;
 
-  width = gtk_widget_get_width (widget);
-  center_x = shumate_map_source_get_x (self->ref_map_source, self->zoom_level, self->lon);
-  return shumate_map_source_get_longitude (self->ref_map_source, self->zoom_level, center_x - width/2 + x);
+  self->rotation = rotation;
+  g_object_notify_by_pspec (G_OBJECT (self), obj_properties[PROP_ROTATION]);
 }
 
 /**
- * shumate_viewport_widget_y_to_latitude:
+ * shumate_viewport_get_rotation:
  * @self: a #ShumateViewport
- * @widget: a #GtkWidget that uses @self as viewport
- * @y: the y coordinate
  *
- * Get the latitude from an y coordinate of a widget.
- * The widget is assumed to be using the viewport.
- * 
- * Returns: the latitude
+ * Gets the current rotation
+ *
+ * Returns: the current rotation
  */
 double
-shumate_viewport_widget_y_to_latitude (ShumateViewport *self,
-                                       GtkWidget       *widget,
-                                       double           y)
+shumate_viewport_get_rotation (ShumateViewport *self)
 {
-  double center_y;
-  int height;
+  g_return_val_if_fail (SHUMATE_IS_VIEWPORT (self), 0);
 
-  g_return_val_if_fail (SHUMATE_IS_VIEWPORT (self), 0.0);
-  g_return_val_if_fail (GTK_IS_WIDGET (widget), 0.0);
+  return self->rotation;
+}
 
-  if (!self->ref_map_source)
-    {
-      g_critical ("A reference map source is required to compute Y from the latitude.");
-      return 0.0;
-    }
+static void
+rotate_around_center (double *x, double *y, double width, double height, double angle)
+{
+  /* Rotate (x, y) around (width / 2, height / 2) */
 
-  height = gtk_widget_get_height (widget);
-  center_y = shumate_map_source_get_y (self->ref_map_source, self->zoom_level, self->lat);
-  return shumate_map_source_get_latitude (self->ref_map_source, self->zoom_level, center_y - height/2 + y);
+  double old_x = *x;
+  double old_y = *y;
+  double center_x = width / 2.0;
+  double center_y = height / 2.0;
+
+  *x = cos(angle) * (old_x - center_x) - sin(angle) * (old_y - center_y) + center_x;
+  *y = sin(angle) * (old_x - center_x) + cos(angle) * (old_y - center_y) + center_y;
+}
+
+static double
+positive_mod (double i, double n)
+{
+  return fmod (fmod (i, n) + n, n);
 }
 
 /**
- * shumate_viewport_longitude_to_widget_x:
+ * shumate_viewport_widget_coords_to_location:
  * @self: a #ShumateViewport
  * @widget: a #GtkWidget that uses @self as viewport
- * @longitude: the longitude
+ * @x: the x coordinate
+ * @y: the y coordinate
+ * @latitude: (out): return location for the latitude
+ * @longitude: (out): return location for the longitude
  *
- * Get an x coordinate of a widget from the longitude.
- * The widget is assumed to be using the viewport.
- * 
- * Returns: the x coordinate
+ * Gets the latitude and longitude corresponding to a position on @widget.
  */
-double
-shumate_viewport_longitude_to_widget_x (ShumateViewport *self,
-                                        GtkWidget       *widget,
-                                        double           longitude)
+void
+shumate_viewport_widget_coords_to_location (ShumateViewport *self,
+                                            GtkWidget       *widget,
+                                            double           x,
+                                            double           y,
+                                            double          *latitude,
+                                            double          *longitude)
 {
-  double center_longitude;
-  double left_x, x;
-  int width;
+  double center_x, center_y;
+  double width, height;
+  double tile_size;
+  double map_width, map_height;
 
-  g_return_val_if_fail (SHUMATE_IS_VIEWPORT (self), 0.0);
-  g_return_val_if_fail (GTK_IS_WIDGET (widget), 0.0);
+  g_return_if_fail (SHUMATE_IS_VIEWPORT (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+  g_return_if_fail (latitude != NULL);
+  g_return_if_fail (longitude != NULL);
 
   if (!self->ref_map_source)
     {
-      g_critical ("A reference map source is required to compute X from the longitude.");
-      return 0.0;
+      g_critical ("A reference map source is required.");
+      return;
     }
 
   width = gtk_widget_get_width (widget);
-  center_longitude = shumate_location_get_longitude (SHUMATE_LOCATION (self));
-  left_x = shumate_map_source_get_x (self->ref_map_source, self->zoom_level, center_longitude) - width/2;
-  x = shumate_map_source_get_x (self->ref_map_source, self->zoom_level, longitude);
-  return x - left_x;
+  height = gtk_widget_get_height (widget);
+  rotate_around_center (&x, &y, width, height, -self->rotation);
+
+  tile_size = shumate_map_source_get_tile_size (self->ref_map_source) * (fmod (self->zoom_level, 1.0) + 1);
+  map_width = tile_size * shumate_map_source_get_column_count (self->ref_map_source, self->zoom_level);
+  map_height = tile_size * shumate_map_source_get_row_count (self->ref_map_source, self->zoom_level);
+
+  center_x = shumate_map_source_get_x (self->ref_map_source, self->zoom_level, self->lon) - width/2 + x;
+  center_y = shumate_map_source_get_y (self->ref_map_source, self->zoom_level, self->lat) - height/2 + y;
+
+  center_x = positive_mod (center_x, map_width);
+  center_y = positive_mod (center_y, map_height);
+
+  *latitude = shumate_map_source_get_latitude (self->ref_map_source, self->zoom_level, center_y);
+  *longitude = shumate_map_source_get_longitude (self->ref_map_source, self->zoom_level, center_x);
 }
 
 /**
- * shumate_viewport_latitude_to_widget_y:
+ * shumate_viewport_location_to_widget_coords:
  * @self: a #ShumateViewport
  * @widget: a #GtkWidget that uses @self as viewport
  * @latitude: the latitude
+ * @longitude: the longitude
+ * @x: (out): return value for the x coordinate
+ * @y: (out): return value for the y coordinate
  *
- * Get an y coordinate of a widget from the latitude.
- * The widget is assumed to be using the viewport.
- * 
- * Returns: the y coordinate
+ * Gets the position on @widget that correspond to the given latitude and
+ * longitude.
  */
-double
-shumate_viewport_latitude_to_widget_y (ShumateViewport *self,
-                                       GtkWidget       *widget,
-                                       double           latitude)
+void
+shumate_viewport_location_to_widget_coords (ShumateViewport *self,
+                                            GtkWidget       *widget,
+                                            double           latitude,
+                                            double           longitude,
+                                            double          *x,
+                                            double          *y)
 {
-  double center_latitude;
-  double top_y, y;
-  int height;
+  double center_latitude, center_longitude;
+  double width, height;
 
-  g_return_val_if_fail (SHUMATE_IS_VIEWPORT (self), 0.0);
-  g_return_val_if_fail (GTK_IS_WIDGET (widget), 0.0);
+  g_return_if_fail (SHUMATE_IS_VIEWPORT (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+  g_return_if_fail (x != NULL);
+  g_return_if_fail (y != NULL);
 
   if (!self->ref_map_source)
     {
-      g_critical ("A reference map source is required to compute the latitude from Y.");
-      return 0.0;
+      g_critical ("A reference map source is required.");
+      return;
     }
 
+  width = gtk_widget_get_width (widget);
   height = gtk_widget_get_height (widget);
+
+  *x = shumate_map_source_get_x (self->ref_map_source, self->zoom_level, longitude);
+  *y = shumate_map_source_get_y (self->ref_map_source, self->zoom_level, latitude);
+
   center_latitude = shumate_location_get_latitude (SHUMATE_LOCATION (self));
-  top_y = shumate_map_source_get_y (self->ref_map_source, self->zoom_level, center_latitude) - height/2;
-  y = shumate_map_source_get_y (self->ref_map_source, self->zoom_level, latitude);
-  return y - top_y;
+  center_longitude = shumate_location_get_longitude (SHUMATE_LOCATION (self));
+  *x -= shumate_map_source_get_x (self->ref_map_source, self->zoom_level, center_longitude) - width/2;
+  *y -= shumate_map_source_get_y (self->ref_map_source, self->zoom_level, center_latitude) - height/2;
+
+  rotate_around_center (x, y, width, height, self->rotation);
 }
diff --git a/shumate/shumate-viewport.h b/shumate/shumate-viewport.h
index dcb96a5..90cc0e2 100644
--- a/shumate/shumate-viewport.h
+++ b/shumate/shumate-viewport.h
@@ -56,18 +56,22 @@ void shumate_viewport_set_reference_map_source (ShumateViewport  *self,
                                                 ShumateMapSource *map_source);
 ShumateMapSource *shumate_viewport_get_reference_map_source (ShumateViewport  *self);
 
-double shumate_viewport_widget_x_to_longitude (ShumateViewport *self,
-                                               GtkWidget       *widget,
-                                               double           x);
-double shumate_viewport_widget_y_to_latitude (ShumateViewport *self,
-                                              GtkWidget       *widget,
-                                              double           y);
-double shumate_viewport_longitude_to_widget_x (ShumateViewport *self,
-                                               GtkWidget       *widget,
-                                               double           longitude);
-double shumate_viewport_latitude_to_widget_y (ShumateViewport *self,
-                                              GtkWidget       *widget,
-                                              double           latitude);
+void shumate_viewport_set_rotation (ShumateViewport *self,
+                                    double           rotation);
+double shumate_viewport_get_rotation (ShumateViewport *self);
+
+void shumate_viewport_widget_coords_to_location (ShumateViewport *self,
+                                                 GtkWidget       *widget,
+                                                 double           x,
+                                                 double           y,
+                                                 double          *latitude,
+                                                 double          *longitude);
+void shumate_viewport_location_to_widget_coords (ShumateViewport *self,
+                                                 GtkWidget       *widget,
+                                                 double           latitude,
+                                                 double           longitude,
+                                                 double          *x,
+                                                 double          *y);
 
 G_END_DECLS
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]