[gnome-maps] Add MapMarker and MapBubble classes
- From: Jonas Danielsson <jonasdn src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-maps] Add MapMarker and MapBubble classes
- Date: Fri, 29 Aug 2014 17:50:19 +0000 (UTC)
commit f446cd87ecc65a81966f329d3d63087fb2f0b9c1
Author: Damián Nohales <damiannohales gmail com>
Date: Sun Jun 22 13:59:02 2014 -0300
Add MapMarker and MapBubble classes
Add base classes intended to create markers associated with
content rich GtkPopover based bubbles.
https://bugzilla.gnome.org/show_bug.cgi?id=722871
src/gnome-maps.js.gresource.xml | 3 +
src/mapBubble.js | 48 +++++++
src/mapMarker.js | 262 +++++++++++++++++++++++++++++++++++++++
src/mapView.js | 26 +---
src/mapWalker.js | 199 +++++++++++++++++++++++++++++
src/utils.js | 21 +++
6 files changed, 539 insertions(+), 20 deletions(-)
---
diff --git a/src/gnome-maps.js.gresource.xml b/src/gnome-maps.js.gresource.xml
index ca842c7..b37a5e8 100644
--- a/src/gnome-maps.js.gresource.xml
+++ b/src/gnome-maps.js.gresource.xml
@@ -11,8 +11,11 @@
<file>layersPopover.js</file>
<file>main.js</file>
<file>mainWindow.js</file>
+ <file>mapBubble.js</file>
<file>mapLocation.js</file>
+ <file>mapMarker.js</file>
<file>mapView.js</file>
+ <file>mapWalker.js</file>
<file>notification.js</file>
<file>notificationManager.js</file>
<file>path.js</file>
diff --git a/src/mapBubble.js b/src/mapBubble.js
new file mode 100644
index 0000000..f747327
--- /dev/null
+++ b/src/mapBubble.js
@@ -0,0 +1,48 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2014 Damián Nohales
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Author: Damián Nohales <damiannohales gmail com>
+ */
+
+const Gtk = imports.gi.Gtk;
+
+const Lang = imports.lang;
+
+const MapBubble = new Lang.Class({
+ Name: "MapBubble",
+ Extends: Gtk.Popover,
+ Abstract: true,
+
+ _init: function(params) {
+ this._place = params.place;
+ delete params.place;
+
+ this._mapView = params.mapView;
+ params.relative_to = params.mapView;
+ delete params.mapView;
+
+ params.modal = false;
+
+ this.parent(params);
+ },
+
+ get place() {
+ return this._place;
+ }
+});
\ No newline at end of file
diff --git a/src/mapMarker.js b/src/mapMarker.js
new file mode 100644
index 0000000..ea47ff3
--- /dev/null
+++ b/src/mapMarker.js
@@ -0,0 +1,262 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2014 Damián Nohales
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Author: Damián Nohales <damiannohales gmail com>
+ */
+
+const Cairo = imports.gi.cairo;
+const Champlain = imports.gi.Champlain;
+const Geocode = imports.gi.GeocodeGlib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+
+const MapWalker = imports.mapWalker;
+const Utils = imports.utils;
+
+const MapMarker = new Lang.Class({
+ Name: 'MapMarker',
+ Extends: Champlain.Marker,
+ Abstract: true,
+ Signals: {
+ 'gone-to': { }
+ },
+
+ _init: function(params) {
+ this._place = params.place;
+ delete params.place;
+
+ this._mapView = params.mapView;
+ delete params.mapView;
+
+ this._view = this._mapView.view;
+
+ params.latitude = this.place.location.latitude;
+ params.longitude = this.place.location.longitude;
+ params.selectable = true;
+
+ this.parent(params);
+
+ this.connect('notify::size', this._translateMarkerPosition.bind(this));
+ this.connect('notify::selected', this._onMarkerSelected.bind(this));
+
+ // Some markers are draggable, we want to sync the marker location and
+ // the location saved in the GeocodePlace
+ this.bind_property('latitude',
+ this.place.location, 'latitude',
+ GObject.BindingFlags.DEFAULT);
+
+ this.bind_property('longitude',
+ this.place.location, 'longitude',
+ GObject.BindingFlags.DEFAULT);
+ },
+
+ _translateMarkerPosition: function() {
+ this.set_translation(-this.anchor.x, -this.anchor.y, 0);
+ },
+
+ /**
+ * Returns: The anchor point for the marker icon, relative to the
+ * top left corner.
+ */
+ get anchor() {
+ return { x: 0, y: 0 };
+ },
+
+ get bubbleSpacing() {
+ return 0;
+ },
+
+ get place() {
+ return this._place;
+ },
+
+ get bubble() {
+ if (this._bubble === undefined)
+ this._bubble = this._createBubble();
+
+ return this._bubble;
+ },
+
+ _createBubble: function() {
+ // Markers has no associated bubble by default
+ return null;
+ },
+
+ _positionBubble: function(bubble) {
+ let [tx, ty, tz] = this.get_translation();
+ let x = this._view.longitude_to_x(this.longitude);
+ let y = this._view.latitude_to_y(this.latitude);
+ let mapSize = this._mapView.get_allocation();
+
+ let pos = new Cairo.RectangleInt({ x: x + tx - this.bubbleSpacing,
+ y: y + ty - this.bubbleSpacing,
+ width: this.width + this.bubbleSpacing * 2,
+ height: this.height + this.bubbleSpacing * 2 });
+ bubble.pointing_to = pos;
+ bubble.position = Gtk.PositionType.TOP;
+
+ // Gtk+ doesn't provide a widget allocation by calling get_allocation
+ // if it's not visible, the bubble positioning occurs when bubble
+ // is not visible yet
+ let bubbleSize = bubble.get_preferred_size()[1];
+
+ // Set bubble position left/right if it's close to a vertical map edge
+ if (pos.x + pos.width / 2 + bubbleSize.width / 2 >= mapSize.width)
+ bubble.position = Gtk.PositionType.LEFT;
+ else if (pos.x + pos.width / 2 - bubbleSize.width / 2 <= 0)
+ bubble.position = Gtk.PositionType.RIGHT;
+ // Avoid bubble to cover header bar if the marker is close to the top map edge
+ else if (pos.y - bubbleSize.height <= 0)
+ bubble.position = Gtk.PositionType.BOTTOM;
+ },
+
+ _hideBubbleOn: function(signal, duration) {
+ let sourceId = null;
+ let signalId = this._view.connect(signal, (function() {
+ if (sourceId)
+ Mainloop.source_remove(sourceId);
+ else
+ this.hideBubble();
+
+ let callback = (function() {
+ sourceId = null;
+ this.showBubble();
+ }).bind(this);
+
+ if (duration)
+ sourceId = Mainloop.timeout_add(duration, callback);
+ else
+ sourceId = Mainloop.idle_add(callback);
+ }).bind(this));
+
+ Utils.once(this.bubble, 'closed', (function() {
+ // We still listening for the signal to refresh
+ // the existent timeout
+ if (!sourceId)
+ this._view.disconnect(signalId);
+ }).bind(this));
+
+ Utils.once(this, 'notify::selected', (function() {
+ // When the marker gets deselected, we need to ensure
+ // that the timeout callback is not called anymore.
+ if (sourceId) {
+ Mainloop.source_remove(sourceId);
+ this._view.disconnect(signalId);
+ }
+ }).bind(this));
+ },
+
+ _initBubbleSignals: function() {
+ this._hideBubbleOn('notify::zoom-level', 500);
+ this._hideBubbleOn('notify::size');
+
+ // This is done to get just one marker selected at any time regardless
+ // of the layer to which it belongs so we can get only one visible bubble
+ // at any time. We do this for markers in different layers because for
+ // markers in the same layer, ChamplainMarkerLayer single selection mode
+ // does the job.
+ this._mapView.onSetMarkerSelected(this);
+
+ let markerSelectedSignalId = this._mapView.connect('marker-selected', (function(mapView,
selectedMarker) {
+ if (this.get_parent() != selectedMarker.get_parent())
+ this.selected = false;
+ }).bind(this));
+
+ let goingToSignalId = this._mapView.connect('going-to',
+ this.set_selected.bind(this, false));
+ let buttonPressSignalId = this._view.connect('button-press-event',
+ this.set_selected.bind(this, false));
+ // Destroy the bubble when the marker is destroyed o removed from a layer
+ let parentSetSignalId = this.connect('parent-set',
+ this.set_selected.bind(this, false));
+ let dragMotionSignalId = this.connect('drag-motion',
+ this.set_selected.bind(this, false));
+
+ Utils.once(this.bubble, 'closed', (function() {
+ this._mapView.disconnect(markerSelectedSignalId);
+ this._mapView.disconnect(goingToSignalId);
+ this._view.disconnect(buttonPressSignalId);
+ this.disconnect(parentSetSignalId);
+ this.disconnect(dragMotionSignalId);
+
+ this._bubble.destroy();
+ delete this._bubble;
+ }).bind(this));
+ },
+
+ _isInsideView: function() {
+ let [tx, ty, tz] = this.get_translation();
+ let x = this._view.longitude_to_x(this.longitude);
+ let y = this._view.latitude_to_y(this.latitude);
+ let mapSize = this._mapView.get_allocation();
+
+ return x + tx + this.width > 0 && x + tx < mapSize.width &&
+ y + ty + this.height > 0 && y + ty < mapSize.height;
+ },
+
+ showBubble: function() {
+ if (this.bubble && !this.bubble.visible && this._isInsideView()) {
+ this._initBubbleSignals();
+ this.bubble.show();
+ this._positionBubble(this.bubble);
+ }
+ },
+
+ hideBubble: function() {
+ if (this._bubble)
+ this._bubble.hide();
+ },
+
+ get walker() {
+ if (this._walker === undefined)
+ this._walker = new MapWalker.MapWalker(this.place, this._mapView);
+
+ return this._walker;
+ },
+
+ zoomToFit: function() {
+ this.walker.zoomToFit();
+ },
+
+ goTo: function(animate) {
+ Utils.once(this.walker, 'gone-to', (function() {
+ this.emit('gone-to');
+ }).bind(this));
+
+ this.walker.goTo(animate);
+ },
+
+ goToAndSelect: function(animate) {
+ Utils.once(this, 'gone-to', (function() {
+ this.selected = true;
+ }).bind(this));
+
+ this.goTo(animate);
+ },
+
+ _onMarkerSelected: function() {
+ if (this.selected)
+ this.showBubble();
+ else
+ this.hideBubble();
+ }
+});
diff --git a/src/mapView.js b/src/mapView.js
index 3ac6624..95153d8 100644
--- a/src/mapView.js
+++ b/src/mapView.js
@@ -37,6 +37,7 @@ const Application = imports.application;
const Utils = imports.utils;
const Path = imports.path;
const MapLocation = imports.mapLocation;
+const MapWalker = imports.mapWalker;
const UserLocation = imports.userLocation;
const _ = imports.gettext.gettext;
@@ -113,21 +114,6 @@ const MapView = new Lang.Class({
this.view.map_source = source;
},
- ensureLocationsVisible: function(locations) {
- let bbox = new Champlain.BoundingBox({ left: 180,
- right: -180,
- bottom: 90,
- top: -90 });
-
- locations.forEach(function(location) {
- bbox.left = Math.min(bbox.left, location.longitude);
- bbox.right = Math.max(bbox.right, location.longitude);
- bbox.bottom = Math.min(bbox.bottom, location.latitude);
- bbox.top = Math.max(bbox.top, location.latitude);
- });
- this.view.ensure_visible(bbox, true);
- },
-
gotoUserLocation: function(animate) {
this.emit('going-to-user-location');
this._userLocation.once("gone-to", (function() {
@@ -181,9 +167,6 @@ const MapView = new Lang.Class({
route.path.forEach(this._routeLayer.add_node.bind(this._routeLayer));
- // Animate to the center of the route bounding box
- // goto() is currently implemented on mapLocation, so we need to go
- // through some hoops here.
let [lat, lon] = route.bbox.get_center();
let place = new Geocode.Place({
location : new Geocode.Location({ latitude : lat,
@@ -193,13 +176,16 @@ const MapView = new Lang.Class({
left : route.bbox.left,
right : route.bbox.right })
});
- let mapLocation = new MapLocation.MapLocation(place, this);
- mapLocation.goTo(true);
+ new MapWalker.MapWalker(place, this).goTo(true);
},
_onViewMoved: function() {
this.emit('view-moved');
+ },
+
+ onSetMarkerSelected: function(selectedMarker) {
+ this.emit('marker-selected', selectedMarker);
}
});
Utils.addSignalMethods(MapView.prototype);
diff --git a/src/mapWalker.js b/src/mapWalker.js
new file mode 100644
index 0000000..ea6a14a
--- /dev/null
+++ b/src/mapWalker.js
@@ -0,0 +1,199 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2011, 2012, 2013 Red Hat, Inc.
+ * Copyright (c) 2014 Damián Nohales
+ *
+ * GNOME Maps is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * GNOME Maps is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with GNOME Maps; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Author: Zeeshan Ali (Khattak) <zeeshanak gnome org>
+ * Damián Nohales <damiannohales gmail com>
+ */
+
+const Clutter = imports.gi.Clutter;
+const Champlain = imports.gi.Champlain;
+const Geocode = imports.gi.GeocodeGlib;
+
+const Lang = imports.lang;
+
+const Utils = imports.utils;
+
+const _MAX_DISTANCE = 19850; // half of Earth's circumference (km)
+const _MIN_ANIMATION_DURATION = 2000; // msec
+const _MAX_ANIMATION_DURATION = 5000; // msec
+
+const MapWalker = new Lang.Class({
+ Name: 'MapWalker',
+
+ _init: function(place, mapView) {
+ this.place = place;
+ this._mapView = mapView;
+ this._view = mapView.view;
+ this._boundingBox = this._createBoundingBox(this.place);
+ },
+
+ _createBoundingBox: function(place) {
+ if (place.bounding_box !== null) {
+ return new Champlain.BoundingBox({ top: place.bounding_box.top,
+ bottom: place.bounding_box.bottom,
+ left: place.bounding_box.left,
+ right: place.bounding_box.right });
+ } else
+ return null;
+ },
+
+ // Zoom to the maximal zoom-level that fits the place type
+ zoomToFit: function() {
+ let zoom;
+
+ if (this._boundingBox !== null) {
+ let max = this._view.max_zoom_level;
+ let min = this._view.min_zoom_level;
+ for (let i = max; i >= min; i--) {
+ let zoomBox = this._view.get_bounding_box_for_zoom_level(i);
+ if (this._boxCovers(zoomBox)) {
+ zoom = i;
+ break;
+ }
+ }
+ } else {
+ switch (this.place.place_type) {
+ case Geocode.PlaceType.STREET:
+ zoom = 16;
+ break;
+
+ case Geocode.PlaceType.CITY:
+ zoom = 11;
+ break;
+
+ case Geocode.PlaceType.REGION:
+ zoom = 10;
+ break;
+
+ case Geocode.PlaceType.COUNTRY:
+ zoom = 6;
+ break;
+
+ default:
+ zoom = 11;
+ break;
+ }
+ }
+ this._view.zoom_level = zoom;
+ this._view.center_on(this.place.location.latitude,
+ this.place.location.longitude);
+ },
+
+ goTo: function(animate) {
+ Utils.debug('Going to ' + this.place.location.description);
+ this._mapView.emit('going-to');
+
+ if (!animate) {
+ this._view.center_on(this.place.location.latitude, this.place.location.longitude);
+ this._view.animate_zoom = false;
+ this.zoomToFit();
+ this._view.animate_zoom = true;
+ this.emit('gone-to');
+
+ return;
+ }
+
+ /* Lets first ensure that both current and destination location are visible
+ * before we start the animated journey towards destination itself. We do this
+ * to create the zoom-out-then-zoom-in effect that many map implementations
+ * do. This not only makes the go-to animation look a lot better visually but
+ * also give user a good idea of where the destination is compared to current
+ * location.
+ */
+
+ this._view.goto_animation_mode = Clutter.AnimationMode.EASE_IN_CUBIC;
+
+ let fromLocation = new Geocode.Location({ latitude: this._view.get_center_latitude(),
+ longitude: this._view.get_center_longitude() });
+ this._updateGoToDuration(fromLocation);
+
+ Utils.once(this._view, 'animation-completed', (function() {
+ Utils.once(this._view, 'animation-completed::go-to', (function() {
+ this.zoomToFit();
+ this._view.goto_animation_mode = Clutter.AnimationMode.EASE_IN_OUT_CUBIC;
+ this.emit('gone-to');
+ }).bind(this));
+
+ this._view.goto_animation_mode = Clutter.AnimationMode.EASE_OUT_CUBIC;
+ this._view.go_to(this.place.location.latitude, this.place.location.longitude);
+ }).bind(this));
+
+ this._ensureVisible(fromLocation);
+ },
+
+ _ensureVisible: function(fromLocation) {
+ let visibleBox = null;
+
+ if (this._boundingBox !== null && this._boundingBox.is_valid()) {
+ visibleBox = this._boundingBox.copy();
+
+ visibleBox.extend(fromLocation.latitude, fromLocation.longitude);
+ } else {
+ visibleBox = new Champlain.BoundingBox({ left: 180,
+ right: -180,
+ bottom: 90,
+ top: -90 });
+
+ [fromLocation, this.place.location].forEach(function(location) {
+ visibleBox.left = Math.min(visibleBox.left, location.longitude);
+ visibleBox.right = Math.max(visibleBox.right, location.longitude);
+ visibleBox.bottom = Math.min(visibleBox.bottom, location.latitude);
+ visibleBox.top = Math.max(visibleBox.top, location.latitude);
+ });
+ }
+
+ this._view.ensure_visible(visibleBox, true);
+ },
+
+ _boxCovers: function(coverBox) {
+ if (this._boundingBox === null)
+ return false;
+
+ if (coverBox.left > this._boundingBox.left)
+ return false;
+
+ if (coverBox.right < this._boundingBox.right)
+ return false;
+
+ if (coverBox.top < this._boundingBox.top)
+ return false;
+
+ if (coverBox.bottom > this._boundingBox.bottom)
+ return false;
+
+ return true;
+ },
+
+ _updateGoToDuration: function(fromLocation) {
+ let toLocation = this.place.location;
+
+ let distance = fromLocation.get_distance_from(toLocation);
+ let duration = (distance / _MAX_DISTANCE) * _MAX_ANIMATION_DURATION;
+
+ // Clamp duration
+ duration = Math.max(_MIN_ANIMATION_DURATION,
+ Math.min(duration, _MAX_ANIMATION_DURATION));
+
+ // We divide by two because Champlain treats both go_to and
+ // ensure_visible as 'goto' journeys with its own duration.
+ this._view.goto_animation_duration = duration / 2;
+ }
+});
+Utils.addSignalMethods(MapWalker.prototype);
diff --git a/src/utils.js b/src/utils.js
index 631ca12..9d4df43 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -159,6 +159,27 @@ function writeFile(filename, buffer) {
}
}
+function getAccuracyDescription(accuracy) {
+ switch(accuracy) {
+ case Geocode.LOCATION_ACCURACY_UNKNOWN:
+ /* Translators: Accuracy of user location information */
+ return _("Unknown");
+ case 0:
+ /* Translators: Accuracy of user location information */
+ return _("Exact");
+ default:
+ let area = Math.PI * Math.pow(accuracy / 1000, 2);
+
+ debug(accuracy + ' => ' + area);
+ if (area >= 1)
+ area = Math.floor(area);
+ else
+ area = Math.floor(area * 10) / 10;
+
+ return _("%f km²").format(area);
+ }
+}
+
function load_icon(icon, size, loadCompleteCallback) {
if (icon instanceof Gio.FileIcon) {
_load_file_icon(icon, loadCompleteCallback);
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]