[gnome-maps/wip/osm-edit: 1/2] osmEdit: Add OSM edit support
- From: Marcus Lundblad <mlundblad src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-maps/wip/osm-edit: 1/2] osmEdit: Add OSM edit support
- Date: Thu, 29 Oct 2015 21:54:41 +0000 (UTC)
commit 5a371be76d41a5a3366fb993a55e72d11122b908
Author: Marcus Lundblad <ml update uu se>
Date: Mon Oct 19 21:43:32 2015 +0200
osmEdit: Add OSM edit support
High-level JS implementation for communication with the OSM server.
Dialog for editing OSM data and utility functions used for editing.
https://bugzilla.gnome.org/show_bug.cgi?id=726628
data/org.gnome.Maps.data.gresource.xml | 1 +
data/ui/map-bubble.ui | 11 +
data/ui/osm-edit-dialog.ui | 226 +++++++++++++++++++
src/org.gnome.Maps.src.gresource.xml | 4 +
src/osmConnection.js | 292 ++++++++++++++++++++++++
src/osmEdit.js | 155 +++++++++++++
src/osmEditDialog.js | 387 ++++++++++++++++++++++++++++++++
src/osmUtils.js | 57 +++++
8 files changed, 1133 insertions(+), 0 deletions(-)
---
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml
index 6ba6040..eb68093 100644
--- a/data/org.gnome.Maps.data.gresource.xml
+++ b/data/org.gnome.Maps.data.gresource.xml
@@ -13,6 +13,7 @@
<file preprocess="xml-stripblanks">ui/main-window.ui</file>
<file preprocess="xml-stripblanks">ui/map-bubble.ui</file>
<file preprocess="xml-stripblanks">ui/notification.ui</file>
+ <file preprocess="xml-stripblanks">ui/osm-edit-dialog.ui</file>
<file preprocess="xml-stripblanks">ui/place-bubble.ui</file>
<file preprocess="xml-stripblanks">ui/place-entry.ui</file>
<file preprocess="xml-stripblanks">ui/place-list-row.ui</file>
diff --git a/data/ui/map-bubble.ui b/data/ui/map-bubble.ui
index e488412..17b8706 100644
--- a/data/ui/map-bubble.ui
+++ b/data/ui/map-bubble.ui
@@ -108,6 +108,17 @@
<property name="tooltip-text" translatable="yes">Check in here</property>
</object>
</child>
+ <child>
+ <object class="GtkButton" id="bubble-edit-button">
+ <property name="name">bubble-edit-button"</property>
+ <property name="label" translatable="yes">Edit</property>
+ <!-- TODO: this button should be invisible by default
+ when we handle OSM accounts -->
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ </object>
+ </child>
</object>
<packing>
<property name="left_attach">0</property>
diff --git a/data/ui/osm-edit-dialog.ui b/data/ui/osm-edit-dialog.ui
new file mode 100644
index 0000000..cf09a77
--- /dev/null
+++ b/data/ui/osm-edit-dialog.ui
@@ -0,0 +1,226 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.12"/>
+ <template class="Gjs_OSMEditDialog" parent="GtkDialog">
+ <property name="can_focus">False</property>
+ <property name="type">popup</property>
+ <property name="type_hint">dialog</property>
+ <property name="width_request">500</property>
+ <property name="height_request">500</property>
+ <child internal-child="vbox">
+ <object class="GtkBox" id="contentArea">
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="homogeneous">True</property>
+ <property name="transition_type">crossfade</property>
+ <child>
+ <object class="GtkGrid" id="loadingGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkSpinner" id="loadingSpinner">
+ <property name="height_request">32</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">loading</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="margin">20</property>
+ <child>
+ <object class="GtkGrid" id="editorGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row-spacing">12</property>
+ <property name="column-spacing">6</property>
+ <property name="margin-bottom">12</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">True</property>
+ <property name="valign">GTK_ALIGN_END</property>
+ <child>
+ <object class="GtkMenuButton" id="addFieldButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="popover">addFieldPopover</property>
+ <property name="direction">GTK_ARROW_UP</property>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row-spacing">5</property>
+ <property name="column-spacing">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label">Add Field</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon-name">go-up-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">editor</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="uploadGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">15</property>
+ <property name="margin_end">15</property>
+ <property name="margin_top">15</property>
+ <property name="margin_bottom">15</property>
+ <property name="row-spacing">5</property>
+ <child>
+ <object class="GtkLabel" id="commentLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="true">Comment</property>
+ <property name="halign">GTK_ALIGN_START</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkFrame">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkTextView" id="commentTextView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="vexpand">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="uploadInfoLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="true">Map changes will be visible on all maps that
use
+OpenStreetMap data.</property>
+ <property name="halign">GTK_ALIGN_START</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">upload</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <object class="GtkHeaderBar" id="headerBar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="show-close-button">False</property>
+ <property name="title" translatable="yes">Edit Point of Interest</property>
+ <child>
+ <object class="GtkButton" id="cancelButton">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="pack-type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="backButton">
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon-name">go-previous-symbolic</property>
+ <property name="pixel_size">16</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack-type">start</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="nextButton">
+ <property name="label" translatable="yes">Next</property>
+ <property name="visible">True</property>
+ <property name="sensitive">False</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <style>
+ <class name="default"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkPopover" id="addFieldPopover">
+ <property name="visible">False</property>
+ <child>
+ <object class="GtkGrid" id="addFieldPopoverGrid">
+ <property name="visible">True</property>
+ <property name="orientation">GTK_ORIENTATION_VERTICAL</property>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index 6a10a06..98fae43 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -28,6 +28,10 @@
<file>mapWalker.js</file>
<file>notification.js</file>
<file>notificationManager.js</file>
+ <file>osmConnection.js</file>
+ <file>osmEdit.js</file>
+ <file>osmEditDialog.js</file>
+ <file>osmUtils.js</file>
<file>overpass.js</file>
<file>place.js</file>
<file>placeBubble.js</file>
diff --git a/src/osmConnection.js b/src/osmConnection.js
new file mode 100644
index 0000000..7873e6c
--- /dev/null
+++ b/src/osmConnection.js
@@ -0,0 +1,292 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * 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: Marcus Lundblad <ml update uu se>
+ */
+
+const _ = imports.gettext.gettext;
+
+const Utils = imports.utils;
+
+const Lang = imports.lang;
+const GLib = imports.gi.GLib;
+const Maps = imports.gi.GnomeMaps;
+const Soup = imports.gi.Soup;
+
+const BASE_URL = 'https://api.openstreetmap.org/api';
+const TEST_BASE_URL = 'http://api06.dev.openstreetmap.org/api';
+const API_VERSION = '0.6';
+
+const OSMConnection = new Lang.Class({
+ Name: 'OSMConnection',
+
+ _init: function(params) {
+ this._session = new Soup.Session();
+
+ /* TODO: stopgap to supply username/password
+ to use with HTTP basic auth, should be
+ replaced with OAUTH and real settings */
+ this._username = GLib.getenv('OSM_USERNAME');
+ this._password = GLib.getenv('OSM_PASSWORD');
+
+ let useLiveApi = GLib.getenv('OSM_USE_LIVE_API');
+
+ /* for now use the test API unless explicitly
+ set to use the live one */
+ if (useLiveApi == 'yes')
+ this._useTestApi = false;
+ else
+ this._useTestApi = true;
+
+ this._session.connect('authenticate', this._authenticate.bind(this));
+
+ Maps.osm_init();
+ },
+
+ get useTestAPI() {
+ return this._useTestApi;
+ },
+
+ getOSMObject: function(type, id, callback, cancellable) {
+ let url = this._getQueryUrl(type, id);
+ let uri = new Soup.URI(url);
+ let request = new Soup.Message({ method: 'GET', uri: uri });
+
+ cancellable.connect((function() {
+ this._session.cancel_message(request, Soup.STATUS_CANCELLED);
+ }).bind(this));
+
+ this._session.queue_message(request, (function(obj, message) {
+ if (message.status_code !== Soup.Status.OK) {
+ callback(false, message.status_code, null);
+ return;
+ }
+
+ Utils.debug ('data received: ' + message.response_body.data);
+
+ /* override object type to use the mock object if using
+ the test API */
+ if (this._useTestApi)
+ type = GLib.getenv('OSM_MOCK_TYPE');
+
+ let object = this._parseXML(type, message.response_body);
+ if (object == null)
+ callback(false, message.status_code, null, type);
+ else
+ callback(true, message.status_code, object, type);
+ }).bind(this));
+ },
+
+ _getQueryUrl: function(type, id) {
+ if (this._useTestApi) {
+ /* override object type and ID from a mock object
+ since the object we get from Nominatim and Overpass
+ doesn't exist in the test OSM environment */
+ type = GLib.getenv('OSM_MOCK_TYPE');
+ id = GLib.getenv('OSM_MOCK_ID');
+ }
+
+ return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/' + id;
+ },
+
+ _getBaseUrl: function() {
+ return this._useTestApi ? TEST_BASE_URL : BASE_URL;
+ },
+
+ _parseXML: function(type, body) {
+ let object;
+
+ switch (type) {
+ case 'node':
+ object = Maps.osm_parse_node(body.data, body.length);
+ break;
+ case 'way':
+ object = Maps.osm_parse_way(body.data, body.length);
+ break;
+ case 'relation':
+ object = Maps.osm_parse_relation(body.data, body.length);
+ break;
+ default:
+ GLib.error('unknown OSM type: ' + type);
+ }
+
+ return object;
+ },
+
+ openChangeset: function(comment, callback) {
+ let changeset =
+ Maps.OSMChangeset.new(comment, 'gnome-maps ' + pkg.version);
+ let xml = changeset.serialize();
+
+ Utils.debug('about open changeset:\n' + xml + '\n');
+
+ let url = this._getOpenChangesetUrl();
+ let uri = new Soup.URI(url);
+ let msg = new Soup.Message({ method: 'PUT', uri: uri });
+ msg.set_request('text/xml', Soup.MemoryUse.COPY, xml, xml.length);
+
+ this._session.queue_message(msg, (function(obj, message) {
+ if (message.status_code !== Soup.Status.OK) {
+ callback(false, message.status_code, null);
+ return;
+ }
+
+ let changesetId =
+ GLib.ascii_strtoull (message.response_body.data, '', 10);
+ callback(true, message.status_code, changesetId);
+ }));
+ },
+
+ uploadObject: function(object, type, changeset, callback) {
+ object.changeset = changeset;
+
+ let xml = object.serialize();
+
+ Utils.debug('about to upload object:\n' + xml + '\n');
+
+ let url = this._getCreateOrUpdateUrl(object, type);
+ let uri = new Soup.URI(url);
+ let msg = new Soup.Message({ method: 'PUT', uri: uri });
+ msg.set_request('text/xml', Soup.MemoryUse.COPY, xml, xml.length);
+
+ this._session.queue_message(msg, (function(obj, message) {
+ if (message.status_code !== Soup.Status.OK) {
+ callback(false, message.status_code, null);
+ return;
+ }
+
+ callback(true, message.status_code, message.response_body.data);
+ }));
+ },
+
+ deleteObject: function(object, type, changeset, callback) {
+ object.changeset = changeset;
+ let xml = object.serialize();
+
+ Utils.debug('about to delete object:\n' + xml + '\n');
+
+ let url = this._getDeleteUrl(object, type);
+ let uri = new Soup.URI(url);
+ let msg = new Soup.Message({ method: 'DELETE', uri: uri });
+
+ Utils.debug('calling delete URL: ' + url);
+
+ msg.set_request('text/xml', Soup.MemoryUse.COPY, xml, xml.length);
+
+ this._session.queue_message(msg, (function(obj, message) {
+ if (message.status_code !== Soup.Status.OK) {
+ callback(false, message.status_code, null);
+ return;
+ }
+
+ callback(true, message.status_code, message.response_body.data);
+ }));
+ },
+
+ closeChangeset: function(changesetId, callback) {
+ let url = this._getCloseChangesetUrl(changesetId);
+ let uri = new Soup.URI(url);
+ let msg = new Soup.Message({ method: 'PUT', uri: uri });
+
+ this._session.queue_message(msg, (function(obj, message) {
+ if (message.status_code !== Soup.Status.OK) {
+ callback(false, message.status_code);
+ return;
+ }
+
+ callback(true, message.status_code);
+ }));
+ },
+
+ _getOpenChangesetUrl: function() {
+ return this._getBaseUrl() + '/' + API_VERSION + '/changeset/create';
+ },
+
+ _getCloseChangesetUrl: function(changesetId) {
+ return this._getBaseUrl() + '/' + API_VERSION + '/changeset/' +
+ changesetId + '/close';
+ },
+
+ _getCreateOrUpdateUrl: function(object, type) {
+ if (object.id) {
+ let id = object.id;
+ if (this._useTestApi) {
+ /* override object type and ID from a mock object
+ since the object we get from Nominatim and Overpass
+ doesn't exist in the test OSM environment */
+ type = GLib.getenv('OSM_MOCK_TYPE');
+ id = GLib.getenv('OSM_MOCK_ID');
+ }
+
+ return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/' + id;
+ } else {
+ return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/create';
+ }
+ },
+
+ _getDeleteUrl: function(object, type) {
+ let id = object.id;
+
+ if (this._useTestApi) {
+ /* override object type and ID from a mock object
+ since the object we get from Nominatim and Overpass
+ doesn't exist in the test OSM environment */
+ type = GLib.getenv('OSM_MOCK_TYPE');
+ id = GLib.getenv('OSM_MOCK_ID');
+ }
+
+ return this._getBaseUrl() + '/' + API_VERSION + '/' + type + '/' + id;
+ },
+
+ _authenticate: function(session, msg, auth, retrying, user_data) {
+ if (retrying)
+ session.cancel_message(msg, Soup.Status.UNAUTHORIZED);
+
+ auth.authenticate(this._username, this._password);
+ }
+});
+
+/*
+ * Gets a status message (usually for an error case)
+ * to show for a given OSM server response.
+ */
+function getStatusMessage(statusCode) {
+ switch (statusCode) {
+ case Soup.Status.IO_ERROR:
+ case Soup.Status.UNAUTHORIZED:
+ /* setting the status in session.cancel_message still seems
+ to always give status IO_ERROR */
+ return _("Incorrect user name or password");
+ case Soup.Status.OK:
+ return _("Success");
+ case Soup.Status.BAD_REQUEST:
+ return _("Bad request");
+ case Soup.Status.NOT_FOUND:
+ return _("Object not found");
+ case Soup.Status.CONFLICT:
+ return _("Conflict, someone else has just modified the object");
+ case Soup.Status.GONE:
+ return _("Object has been deleted");
+ case Soup.Status.PRECONDITION_FAILED:
+ return _("Way or relation refers to non-existing children");
+ default:
+ return null;
+ }
+}
+
diff --git a/src/osmEdit.js b/src/osmEdit.js
new file mode 100644
index 0000000..3900be9
--- /dev/null
+++ b/src/osmEdit.js
@@ -0,0 +1,155 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * 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: Marcus Lundblad <ml update uu se>
+ */
+
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+
+const OSMEditDialog = imports.osmEditDialog;
+const OSMConnection = imports.osmConnection;
+const Utils = imports.utils;
+
+const OSMEdit = new Lang.Class({
+ Name: 'OSMEdit',
+ Extends: GObject.Object,
+
+ _init: function() {
+ this._osmConnection = new OSMConnection.OSMConnection();
+ this._osmObject = null; // currently edited object
+ },
+
+ get useTestApi() {
+ return this._osmConnection.useTestApi;
+ },
+
+ get object() {
+ return this._osmObject;
+ },
+
+ showEditDialog: function(parentWindow, place) {
+ let dialog = new OSMEditDialog.OSMEditDialog({ transient_for: parentWindow,
+ place: place });
+ let response = dialog.run();
+ dialog.destroy();
+ return response;
+ },
+
+ fetchObject: function(place, callback, cancellable) {
+ let osmType = this._getOSMTypeName(place.osmType);
+
+ /* reset currenly edited object */
+ this._osmObject = null;
+ this._osmConnection.getOSMObject(osmType, place.osm_id,
+ (function(success, status, osmObject,
+ osmType) {
+ callback(success, status,
+ osmObject, osmType);
+ }), cancellable);
+
+ },
+
+ _getOSMTypeName: function(placeType) {
+ let osmType;
+
+ switch (placeType) {
+ case 1:
+ osmType = 'node';
+ break;
+ case 2:
+ osmType = 'relation';
+ break;
+ case 3:
+ osmType = 'way';
+ break;
+ default:
+ Utils.debug ('Unknown OSM type: ' + placeType);
+ break;
+ }
+
+ return osmType;
+ },
+
+ uploadObject: function(object, type, comment, callback) {
+ this._openChangeset(object, type, comment, this._uploadObject.bind(this),
+ callback);
+ },
+
+ _onChangesetOpened: function(success, status, changesetId, object, type,
+ action, callback) {
+ if (success) {
+ let osmType = this._getOSMTypeName(type);
+ action(object, osmType, changesetId, callback);
+ } else {
+ callback(false, status);
+ }
+ },
+
+ _openChangeset: function(object, type, comment, action, callback) {
+ this._osmConnection.openChangeset(comment,
+ (function(success, status, changesetId) {
+ this._onChangesetOpened(success, status, changesetId, object, type,
+ action, callback);
+ }).bind(this));
+ },
+
+ _onObjectUploaded: function(success, status, response, changesetId,
+ callback) {
+ if (success)
+ this._closeChangeset(changesetId, callback);
+ else
+ callback(false, status);
+ },
+
+ _uploadObject: function(object, type, changesetId, callback) {
+ this._osmObject = object;
+ this._osmConnection.uploadObject(object, type, changesetId,
+ (function(success, status, response) {
+ this._onObjectUploaded(success, status, response, changesetId,
+ callback);
+ }).bind(this));
+ },
+
+ deleteObject: function(object, type, comment, callback) {
+ this._openChangeset(object, type, comment, this._deleteObject.bind(this),
+ callback);
+ },
+
+ _onObjectDeleted: function(success, status, response, changesetId,
+ callback) {
+ if (success)
+ this._closeChangeset(changesetId, callback);
+ else
+ callback(false, status);
+ },
+
+ _deleteObject: function(object, type, changesetId, callback) {
+ this._osmObject = object;
+ this._osmConnection.deleteObject(object, type, changesetId,
+ (function(success, status, response) {
+ this._onObjectDeleted(success, status, response, changesetId,
+ callback);
+ }).bind(this));
+ },
+
+ _closeChangeset: function(changesetId, callback) {
+ this._osmConnection.closeChangeset(changesetId, callback);
+ }
+});
diff --git a/src/osmEditDialog.js b/src/osmEditDialog.js
new file mode 100644
index 0000000..047f726
--- /dev/null
+++ b/src/osmEditDialog.js
@@ -0,0 +1,387 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * 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: Marcus Lundblad <ml update uu se>
+ */
+
+const _ = imports.gettext.gettext;
+
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const Application = imports.application;
+const OSMConnection = imports.osmConnection;
+const OSMUtils = imports.osmUtils;
+
+const Response = {
+ UPLOADED: 0,
+ DELETED: 1,
+ CANCELLED: 2,
+ ERROR: 3
+};
+
+/*
+ * enumeration representing
+ * the different OSM editing
+ * field types
+ */
+const EditFieldType = {
+ // plain text
+ TEXT: 0,
+ // integer value (e.g. population)
+ INTEGER: 1,
+ // selection of yes|no|limited|designated
+ YES_NO_LIMITED_DESIGNATED: 2
+};
+
+let _osmWikipediaRewriteFunc = function(text) {
+ let wikipediaArticleFormatted =
+ OSMUtils.getWikipediaOSMArticleFormatFromUrl(text);
+
+ /* if the entered text is a Wikipedia link,
+ substitute it with the OSM-formatted Wikipedia article tag */
+ if (wikipediaArticleFormatted)
+ return wikipediaArticleFormatted;
+ else
+ return text;
+};
+
+/*
+ * specification of OSM edit fields
+ * name: the label for the edit field (translatable)
+ * tag: the OSM tag key value
+ * type: the field type (determines editing field type)
+ * rewriteFunc: a rewrite function taking a string argument
+ * (only used for TEXT fields)
+ */
+const OSM_FIELDS = [{name: _("Name"), tag: 'name', type: EditFieldType.TEXT},
+ {name: _("Wikipedia"), tag: 'wikipedia', type: EditFieldType.TEXT,
+ rewriteFunc: this._osmWikipediaRewriteFunc},
+ {name: _("Population"), tag: 'population',
+ type: EditFieldType.INTEGER},
+ {name: _("Wheelchair access"), tag: 'wheelchair',
+ type: EditFieldType.YES_NO_LIMITED_DESIGNATED}];
+
+
+const OSMEditDialog = new Lang.Class({
+ Name: 'OSMEditDialog',
+ Extends: Gtk.Dialog,
+ Template: 'resource:///org/gnome/Maps/ui/osm-edit-dialog.ui',
+ InternalChildren: [ 'cancelButton',
+ 'backButton',
+ 'nextButton',
+ 'stack',
+ 'editorGrid',
+ 'commentTextView',
+ 'addFieldPopoverGrid',
+ 'addFieldButton'],
+
+ _init: function(params) {
+ this._place = params.place;
+ delete params.place;
+
+ // This is a construct-only property and cannot be set by GtkBuilder
+ params.use_header_bar = true;
+
+ this.parent(params);
+
+ this._cancellable = new Gio.Cancellable();
+ this._cancellable.connect((function() {
+ this.response(Response.CANCELLED);
+ }).bind(this));
+
+ this.connect('delete-event', (function() {
+ this._cancellable.cancel();
+ }).bind(this));
+
+ this._isEditing = false;
+ this._nextButton.connect('clicked', this._onNextClicked.bind(this));
+ this._cancelButton.connect('clicked', this._onCancelClicked.bind(this));
+ this._backButton.connect('clicked', this._onBackClicked.bind(this));
+
+ Application.osmEdit.fetchObject(this._place,
+ this._fetchOSMObjectCB.bind(this),
+ this._cancellable);
+ },
+
+ _onNextClicked: function() {
+ if (this._isEditing) {
+ // switch to the upload view
+ this._switchToUpload();
+ } else {
+ // upload data to OSM
+ let comment = this._commentTextView.buffer.text;
+ Application.osmEdit.uploadObject(this._osmObject,
+ this._place.osmType,
+ comment,
+ this._uploadOSMObjectCB.bind(this));
+ }
+ },
+
+ _switchToUpload: function() {
+ this._stack.set_visible_child_name('upload');
+ this._nextButton.label = _("Done");
+ this._cancelButton.visible = false;
+ this._backButton.visible = true;
+ this._cancelButton
+ this._isEditing = false;
+ },
+
+ _onCancelClicked: function() {
+ this.response(Response.CANCELLED);
+ },
+
+ _onBackClicked: function() {
+ this._backButton.visible = false;
+ this._cancelButton.visible = true;
+ this._stack.set_visible_child_name('editor');
+ this._isEditing = true;
+ this._commentTextView.buffer.text = '';
+ },
+
+ _fetchOSMObjectCB: function(success, status, osmObject, osmType) {
+ if (success) {
+ this._isEditing = true;
+ this._loadOSMData(osmObject);
+ } else {
+ this._showError(status);
+ }
+ },
+
+ _uploadOSMObjectCB: function(success, status) {
+ if (success) {
+ this.response(Response.UPLOADED);
+ } else {
+ this._showError(status);
+ this.response(Response.ERROR);
+ }
+ },
+
+ _showError: function(status) {
+ let statusMessage = OSMConnection.getStatusMessage(status);
+ let messageDialog =
+ new Gtk.MessageDialog({ transient_for: this.get_toplevel(),
+ destroy_with_parent: true,
+ message_type: Gtk.MessageType.ERROR,
+ buttons: Gtk.ButtonsType.OK,
+ modal: true,
+ text: _("An error has occurred"),
+ secondary_text: statusMessage });
+
+ messageDialog.run();
+ messageDialog.destroy();
+ this.response(Response.ERROR);
+ },
+
+ /* GtkContainer.child_get_property doesn't seem to be usable from GJS */
+ _getRowOfDeleteButton: function(button) {
+ for (let row = 0;; row++) {
+ let label = this._editorGrid.get_child_at(0, row);
+ let deleteButton = this._editorGrid.get_child_at(2, row);
+
+ if (deleteButton === button)
+ return row;
+
+ /* if we reached the end of the table */
+ if (label == null)
+ return -1;
+ }
+ },
+
+ _addOSMEditDeleteButton: function(tag) {
+ let deleteButton = Gtk.Button.new_from_icon_name('user-trash-symbolic',
+ Gtk.IconSize.BUTTON);
+ let styleContext = deleteButton.get_style_context();
+
+ styleContext.add_class('flat');
+ this._editorGrid.attach(deleteButton, 2, this._nextRow, 1, 1);
+
+ deleteButton.connect('clicked', (function() {
+ this._osmObject.delete_tag(tag);
+
+ let row = this._getRowOfDeleteButton(deleteButton);
+ this._editorGrid.remove_row(row);
+ this._nextButton.sensitive = true;
+ this._nextRow--;
+ this._updateAddFieldMenu();
+ }).bind(this, tag));
+
+ deleteButton.show();
+ },
+
+ _addOSMEditLabel: function(text) {
+ let label = new Gtk.Label({label: text});
+ label.halign = Gtk.Align.END;
+ label.get_style_context().add_class('dim-label');
+ this._editorGrid.attach(label, 0, this._nextRow, 1, 1);
+ label.show();
+ },
+
+ _addOSMEditTextEntry: function(text, tag, value, rewriteFunc) {
+ this._addOSMEditLabel(text);
+
+ let entry = new Gtk.Entry();
+ entry.text = value;
+ entry.hexpand = true;
+
+ entry.connect('changed', (function() {
+ if (rewriteFunc)
+ entry.text = rewriteFunc(entry.text);
+ this._osmObject.set_tag(tag, entry.text);
+ this._nextButton.sensitive = true;
+ }).bind(this, tag, entry));
+
+ this._editorGrid.attach(entry, 1, this._nextRow, 1, 1);
+ entry.show();
+
+ /* TODO: should we allow deleting the name field? */
+ this._addOSMEditDeleteButton(tag);
+
+ this._nextRow++;
+ },
+
+ _addOSMEditIntegerEntry: function(text, tag, value) {
+ this._addOSMEditLabel(text);
+
+ let spinbutton = Gtk.SpinButton.new_with_range(0, 1e9, 1);
+ spinbutton.value = value;
+ spinbutton.numeric = true;
+ spinbutton.hexpand = true;
+ spinbutton.connect('changed', (function() {
+ this._osmObject.set_tag(tag, spinbutton.text);
+ this._nextButton.sensitive = true;
+ }).bind(this, tag, spinbutton));
+
+ this._editorGrid.attach(spinbutton, 1, this._nextRow, 1, 1);
+ spinbutton.show();
+
+ this._addOSMEditDeleteButton(tag);
+
+ this._nextRow++;
+ },
+
+ _addOSMEditYesNoLimitedDesignated: function(text, tag, value) {
+ this._addOSMEditLabel(text);
+
+ let combobox = new Gtk.ComboBoxText();
+
+ combobox.append('yes', _("Yes"));
+ combobox.append('no', _("No"));
+ combobox.append('limited', _("Limited"));
+ combobox.append('designated', _("Designated"));
+
+ combobox.active_id = value;
+ combobox.hexpand = true;
+ combobox.connect('changed', (function() {
+ this._osmObject.set_tag(tag, combobox.active_id);
+ this._nextButton.sensitive = true;
+ }).bind(this, tag, combobox));
+
+ this._editorGrid.attach(combobox, 1, this._nextRow, 1, 1);
+ combobox.show();
+
+ this._addOSMEditDeleteButton(tag);
+
+ this._nextRow++;
+ },
+
+ /* update visible items in the "Add Field" popover */
+ _updateAddFieldMenu: function() {
+ /* clear old items */
+ let children = this._addFieldPopoverGrid.get_children();
+ let hasAllFields = true;
+
+ for (let i = 0; i < children.length; i++) {
+ let button = children[i];
+ button.destroy();
+ }
+
+ for (let i = 0; i < OSM_FIELDS.length; i++) {
+ let fieldSpec = OSM_FIELDS[i];
+ let label = fieldSpec.name;
+ let tag = fieldSpec.tag;
+ let value = this._osmObject.get_tag(tag);
+ let type = fieldSpec.type;
+ let rewriteFunc = fieldSpec.rewriteFunc;
+
+ if (value == null) {
+ let button = new Gtk.Button({visible: true, sensitive: true,
+ label: label});
+ button.get_style_context().add_class('menuitem');
+ button.get_style_context().add_class('button');
+ button.get_style_context().add_class('flat');
+
+ button.connect('clicked', (function() {
+ this._addFieldButton.active = false;
+ this._addOSMField(label, tag, '', type, rewriteFunc);
+ /* add a "placeholder" empty OSM tag to keep the add field
+ menu updated, these tags will be filtered out if nothing
+ is entered */
+ this._osmObject.set_tag(tag, '');
+ this._updateAddFieldMenu();
+ }).bind(this, label, tag, type, rewriteFunc));
+
+ hasAllFields = false;
+ this._addFieldPopoverGrid.add(button);
+ }
+ }
+
+ /* update sensitiveness of the add details button, set it as
+ insensitive if all tags we support editing is already present */
+ this._addFieldButton.sensitive = !hasAllFields;
+ },
+
+ _addOSMField: function(label, tag, value, type, rewriteFunc) {
+ switch (type) {
+ case EditFieldType.TEXT:
+ this._addOSMEditTextEntry(label, tag, value, rewriteFunc);
+ break;
+ case EditFieldType.INTEGER:
+ this._addOSMEditIntegerEntry(label, tag, value);
+ break;
+ case EditFieldType.YES_NO_LIMITED_DESIGNATED:
+ this._addOSMEditYesNoLimitedDesignated(label, tag, value);
+ break;
+ }
+ },
+
+ _loadOSMData: function(osmObject, osmType) {
+ this._osmObject = osmObject;
+ this._osmType = osmType;
+
+ /* create edit widgets */
+ this._nextRow = 0;
+
+ for (let i = 0; i < OSM_FIELDS.length; i++) {
+ let fieldSpec = OSM_FIELDS[i];
+ let name = fieldSpec.name;
+ let tag = fieldSpec.tag;
+ let type = fieldSpec.type;
+ let rewriteFunc = fieldSpec.rewriteFunc;
+ let value = osmObject.get_tag(tag);
+
+ if (value != null)
+ this._addOSMField(name, tag, value, type, rewriteFunc);
+ }
+
+ this._updateAddFieldMenu();
+ this._stack.visible_child_name = 'editor';
+ }
+});
diff --git a/src/osmUtils.js b/src/osmUtils.js
new file mode 100644
index 0000000..2c5ed17
--- /dev/null
+++ b/src/osmUtils.js
@@ -0,0 +1,57 @@
+/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */
+/* vim: set et ts=4 sw=4: */
+/*
+ * Copyright (c) 2015 Marcus Lundblad
+ *
+ * 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: Marcus Lundblad <ml update uu se>
+ */
+
+const Soup = imports.gi.Soup;
+
+const Application = imports.application;
+
+/*
+ * Gets a Wikipedia article in OSM tag format (i.e. lang:Article title)
+ * given a URL or null if input doesn't match a Wikipedia URL
+ */
+function getWikipediaOSMArticleFormatFromUrl(url) {
+ let regex = /https?:\/\/(..)\.wikipedia\.org\/wiki\/(.+)/;
+ let match = url.match(regex);
+
+ if (match && match.length == 3) {
+ let lang = match[1];
+ let article = match[2];
+
+ return lang + ':' + Soup.uri_decode(article).replace(/_/g, ' ');
+ } else {
+ return null;
+ }
+}
+
+/**
+ * Updates a Place object according to an OSMObject.
+ * Will also update place in the place store.
+ */
+function updatePlaceFromOSMObject(place, object) {
+ place.name = object.get_tag('name');
+ place.population = object.get_tag('population');
+ place.wiki = object.get_tag('wikipedia');
+ place.openingHours = object.get_tag('opening_hours');
+ place.wheelchair = object.get_tag('wheelchair');
+
+ Application.placeStore.updatePlace(place);
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]