summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcus Lundblad <ml@update.uu.se>2016-01-31 15:01:27 +0100
committerMarcus Lundblad <ml@update.uu.se>2016-02-12 08:34:54 +0100
commitc4c25a8ed6553e081c3b723d2bbc8cd729a6dce8 (patch)
treed65b18b5ce711a861accbc399958d58614341bd4
parent00aad3df22d846ca862caf36a6e39446cfa5ce6f (diff)
downloadgnome-maps-c4c25a8ed6553e081c3b723d2bbc8cd729a6dce8.tar.gz
osmEdit: Add support for creating new locations in the edit dialog
Adds the ability to edit newly created locations in the dialog. Also adds a module to handle OSM tag key/value mapping to translated POI types and a list of recently used POI types. Also enables editing the POI type of existing objects in more simple cases (known tag values with no combined tags). https://bugzilla.gnome.org/show_bug.cgi?id=761327
-rw-r--r--data/org.gnome.Maps.data.gresource.xml3
-rw-r--r--data/ui/osm-edit-dialog.ui111
-rw-r--r--data/ui/osm-type-list-row.ui29
-rw-r--r--data/ui/osm-type-popover.ui16
-rw-r--r--data/ui/osm-type-search-entry.ui10
-rw-r--r--src/application.js8
-rw-r--r--src/org.gnome.Maps.src.gresource.xml4
-rw-r--r--src/osmEditDialog.js217
-rw-r--r--src/osmTypeListRow.js51
-rw-r--r--src/osmTypePopover.js67
-rw-r--r--src/osmTypeSearchEntry.js76
-rw-r--r--src/osmTypes.js168
12 files changed, 738 insertions, 22 deletions
diff --git a/data/org.gnome.Maps.data.gresource.xml b/data/org.gnome.Maps.data.gresource.xml
index 81dc771a..7483d3d4 100644
--- a/data/org.gnome.Maps.data.gresource.xml
+++ b/data/org.gnome.Maps.data.gresource.xml
@@ -18,6 +18,9 @@
<file preprocess="xml-stripblanks">ui/notification.ui</file>
<file preprocess="xml-stripblanks">ui/osm-account-dialog.ui</file>
<file preprocess="xml-stripblanks">ui/osm-edit-dialog.ui</file>
+ <file preprocess="xml-stripblanks">ui/osm-type-list-row.ui</file>
+ <file preprocess="xml-stripblanks">ui/osm-type-search-entry.ui</file>
+ <file preprocess="xml-stripblanks">ui/osm-type-popover.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/osm-edit-dialog.ui b/data/ui/osm-edit-dialog.ui
index 3f2623d9..b3336066 100644
--- a/data/ui/osm-edit-dialog.ui
+++ b/data/ui/osm-edit-dialog.ui
@@ -51,6 +51,56 @@
<property name="row-spacing">12</property>
<property name="column-spacing">6</property>
<property name="margin-bottom">12</property>
+ <child>
+ <object class="GtkLabel" id="typeLabel">
+ <property name="visible">False</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="true">Type</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="typeButton">
+ <property name="visible">False</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</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" id="typeValueLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">None</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">GTK_ALIGN_END</property>
+ <property name="hexpand">True</property>
+ <property name="icon-name">go-next-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
</object>
</child>
<child>
@@ -159,6 +209,65 @@
<property name="name">upload</property>
</packing>
</child>
+ <child>
+ <object class="GtkGrid" id="typeSearchGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_start">60</property>
+ <property name="margin_end">60</property>
+ <property name="margin_top">15</property>
+ <property name="margin_bottom">30</property>
+ <property name="row-spacing">5</property>
+ <!--
+ <child>
+ <object class="Gjs_OSMTypeSearchEntry" id="typeSearchEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">True</property>
+ <property name="margin_start">10</property>
+ <property name="margin_end">10</property>
+ <property name="margin_bottom">10</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ -->
+ <child>
+ <object class="GtkLabel" id="recentTypesLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Recently Used</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="GtkListBox" id="recentTypesListBox">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="frame"/>
+ </style>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">select-type</property>
+ </packing>
+ </child>
</object>
</child>
</object>
@@ -168,7 +277,7 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">False</property>
- <property name="title" translatable="yes">Edit Location</property>
+ <property name="title" translatable="yes">Edit on OpenStreetMap</property>
<child>
<object class="GtkButton" id="cancelButton">
<property name="label" translatable="yes">Cancel</property>
diff --git a/data/ui/osm-type-list-row.ui b/data/ui/osm-type-list-row.ui
new file mode 100644
index 00000000..4dda2381
--- /dev/null
+++ b/data/ui/osm-type-list-row.ui
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.12"/>
+ <template class="Gjs_OSMTypeListRow" parent="GtkListBoxRow">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkGrid" id="grid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row-homogeneous">True</property>
+ <property name="margin">5</property>
+ <child>
+ <object class="GtkLabel" id="name">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="valign">end</property>
+ <property name="hexpand">True</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/ui/osm-type-popover.ui b/data/ui/osm-type-popover.ui
new file mode 100644
index 00000000..4ae9e7fb
--- /dev/null
+++ b/data/ui/osm-type-popover.ui
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.12"/>
+ <template class="Gjs_OSMTypePopover" parent="Gjs_SearchPopover">
+ <property name="position">GTK_POS_BOTTOM</property>
+ <property name="modal">False</property>
+ <child>
+ <object class="GtkListBox" id="list">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="activate_on_single_click">True</property>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/data/ui/osm-type-search-entry.ui b/data/ui/osm-type-search-entry.ui
new file mode 100644
index 00000000..db292a76
--- /dev/null
+++ b/data/ui/osm-type-search-entry.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.12"/>
+ <template class="Gjs_OSMTypeSearchEntry" parent="GtkSearchEntry">
+ <property name="hexpand">True</property>
+ <property name="margin_start">10</property>
+ <property name="margin_end">10</property>
+ <property name="margin_bottom">10</property>
+ </template>
+</interface>
diff --git a/src/application.js b/src/application.js
index aad5fd09..74b6fc89 100644
--- a/src/application.js
+++ b/src/application.js
@@ -38,6 +38,7 @@ const MainWindow = imports.mainWindow;
const Maps = imports.gi.GnomeMaps;
const NotificationManager = imports.notificationManager;
const OSMEdit = imports.osmEdit;
+const OSMTypeSearchEntry = imports.osmTypeSearchEntry;
const PlaceStore = imports.placeStore;
const RouteService = imports.routeService;
const Settings = imports.settings;
@@ -57,6 +58,9 @@ let contactStore = null;
let osmEdit = null;
let normalStartup = true;
+const _ensuredTypes = [WebKit2.WebView,
+ OSMTypeSearchEntry.OSMTypeSearchEntry];
+
const Application = new Lang.Class({
Name: 'Application',
Extends: Gtk.Application,
@@ -83,7 +87,9 @@ const Application = new Lang.Class({
GLib.set_prgname('gnome-maps');
/* Needed to be able to use in UI files */
- GObject.type_ensure(WebKit2.WebView);
+ _ensuredTypes.forEach(function(type) {
+ GObject.type_ensure(type);
+ });
this.parent({ application_id: 'org.gnome.Maps',
flags: Gio.ApplicationFlags.HANDLES_OPEN });
diff --git a/src/org.gnome.Maps.src.gresource.xml b/src/org.gnome.Maps.src.gresource.xml
index ba3150cf..ec88b7fa 100644
--- a/src/org.gnome.Maps.src.gresource.xml
+++ b/src/org.gnome.Maps.src.gresource.xml
@@ -37,6 +37,10 @@
<file>osmConnection.js</file>
<file>osmEdit.js</file>
<file>osmEditDialog.js</file>
+ <file>osmTypeSearchEntry.js</file>
+ <file>osmTypeListRow.js</file>
+ <file>osmTypePopover.js</file>
+ <file>osmTypes.js</file>
<file>osmUtils.js</file>
<file>overpass.js</file>
<file>place.js</file>
diff --git a/src/osmEditDialog.js b/src/osmEditDialog.js
index f488ca42..2bace3ad 100644
--- a/src/osmEditDialog.js
+++ b/src/osmEditDialog.js
@@ -28,8 +28,12 @@ const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Application = imports.application;
+const Maps = imports.gi.GnomeMaps;
const OSMConnection = imports.osmConnection;
+const OSMTypes = imports.osmTypes;
+const OSMTypeSearchEntry = imports.osmTypeSearchEntry;
const OSMUtils = imports.osmUtils;
+const Utils = imports.utils;
const Response = {
UPLOADED: 0,
@@ -54,7 +58,7 @@ 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 */
+ * substitute it with the OSM-formatted Wikipedia article tag */
if (wikipediaArticleFormatted)
return wikipediaArticleFormatted;
else
@@ -106,17 +110,44 @@ const OSMEditDialog = new Lang.Class({
'editorGrid',
'commentTextView',
'addFieldPopoverGrid',
- 'addFieldButton'],
+ 'addFieldButton',
+ 'typeSearchGrid',
+ 'typeLabel',
+ 'typeButton',
+ 'typeValueLabel',
+ 'recentTypesLabel',
+ 'recentTypesListBox',
+ 'headerBar'],
_init: function(params) {
this._place = params.place;
delete params.place;
+ this._addLocation = params.addLocation;
+ delete params.addLocation;
+
+ this._latitude = params.latitude;
+ delete params.latitude;
+
+ this._longitude = params.longitude;
+ delete params.longitude;
+
/* This is a construct-only property and cannot be set by GtkBuilder */
params.use_header_bar = true;
this.parent(params);
+ /* I could not get this widget working from within the widget template
+ * this results in a segfault. The widget definition is left in-place,
+ * but commented-out in the template file */
+ this._typeSearch = new OSMTypeSearchEntry.OSMTypeSearchEntry();
+ this._typeSearchGrid.attach(this._typeSearch, 0, 0, 1, 1);
+ this._typeSearch.visible = true;
+ this._typeSearch.can_focus = true;
+
+ let typeSearchPopover = this._typeSearch.popover;
+ typeSearchPopover.connect('selected', this._onTypeSelected.bind(this));
+
this._cancellable = new Gio.Cancellable();
this._cancellable.connect((function() {
this.response(Response.CANCELLED);
@@ -130,29 +161,176 @@ const OSMEditDialog = new Lang.Class({
this._nextButton.connect('clicked', this._onNextClicked.bind(this));
this._cancelButton.connect('clicked', this._onCancelClicked.bind(this));
this._backButton.connect('clicked', this._onBackClicked.bind(this));
+ this._typeButton.connect('clicked', this._onTypeClicked.bind(this));
+
+ if (this._addLocation) {
+ this._headerBar.title = _("Add to OpenStreetMap");
+ this._typeLabel.visible = true;
+ this._typeButton.visible = true;
+
+ /* the OSMObject ID, version, and changeset ID is unknown for now */
+ let newNode =
+ Maps.OSMNode.new(0, 0, 0, this._longitude, this._latitude);
+ /* set a placeholder name tag to always get a name entry for new
+ * locations */
+ newNode.set_tag('name', '');
+ this._loadOSMData(newNode);
+ this._isEditing = true;
+ } else {
+ this._osmType = this._place.osmType;
+ Application.osmEdit.fetchObject(this._place,
+ this._onObjectFetched.bind(this),
+ this._cancellable);
+ }
+
+ /* store original title of the dialog to be able to restore it when
+ * coming back from type selection */
+ this._originalTitle = this._headerBar.title;
+ this._updateRecentTypesList();
- Application.osmEdit.fetchObject(this._place,
- this._onObjectFetched.bind(this),
- this._cancellable);
+ this._recentTypesListBox.set_header_func(function (row, previous) {
+ row.set_header(new Gtk.Separator());
+ });
+
+ this._recentTypesListBox.connect('row-activated', (function(listbox, row) {
+ this._onTypeSelected(null, row._key, row._value, row._title);
+ }).bind(this));
},
_onNextClicked: function() {
if (this._isEditing) {
- // switch to the upload view
this._switchToUpload();
} else {
- // turn on spinner
this._stack.visible_child_name = 'loading';
this._nextButton.sensitive = false;
this._backButton.sensitive = false;
- // upload data to OSM
+
let comment = this._commentTextView.buffer.text;
Application.osmEdit.uploadObject(this._osmObject,
- this._place.osmType, comment,
+ this._osmType, comment,
this._onObjectUploaded.bind(this));
}
},
+ _onTypeClicked: function() {
+ this._cancelButton.visible = false;
+ this._backButton.visible = true;
+ this._headerBar.title = _("Select Type");
+ this._stack.visible_child_name = 'select-type';
+ },
+
+ _onTypeSelected: function(popover, key, value, title) {
+ this._typeValueLabel.label = title;
+ this._updateType(key, value);
+
+ if (popover)
+ popover.hide();
+
+ /* clear out type search entry */
+ this._typeSearch.text = '';
+
+ /* go back to the editing stack page */
+ this._backButton.visible = false;
+ this._cancelButton.visible = true;
+ this._stack.visible_child_name = 'editor';
+ this._headerBar.title = this._originalTitle;
+
+ /* update recent types store */
+ OSMTypes.recentTypesStore.pushType(key, value);
+
+ /* enable the Next button, so that it's possible to just change the type
+ * of an object without changing anything else */
+ this._nextButton.sensitive = true;
+
+ this._updateRecentTypesList();
+ },
+
+ _updateType: function(key, value) {
+ /* clear out any previous type-related OSM tags */
+ OSMTypes.OSM_TYPE_TAGS.forEach((function (tag) {
+ this._osmObject.delete_tag(tag);
+ }).bind(this));
+
+ this._osmObject.set_tag(key, value);
+ },
+
+ /* update visibility and enable the type selection button if the object has
+ * a well-known type (based on a known set of tags) */
+ _updateTypeButton: function() {
+ let numTypeTags = 0;
+ let lastTypeTag = null;
+
+ for (let i = 0; i < OSMTypes.OSM_TYPE_TAGS.length; i++) {
+ let key = OSMTypes.OSM_TYPE_TAGS[i];
+ let value = this._osmObject.get_tag(key);
+
+ if (value != null) {
+ numTypeTags++;
+ lastTypeTag = key;
+ }
+ }
+
+ /* if the object has none of tags set, enable the button and keep the
+ * pre-set "None" label */
+ if (numTypeTags === 0) {
+ this._typeLabel.visible = true;
+ this._typeButton.visible = true;
+ } else if (numTypeTags === 1) {
+ let value = this._osmObject.get_tag(lastTypeTag);
+ let typeTitle = OSMTypes.lookupType(lastTypeTag, value);
+
+ /* if the type tag has a value we know of, and possible has
+ * translations for */
+ if (typeTitle != null) {
+ this._typeValueLabel.label = typeTitle;
+ this._typeLabel.visible = true;
+ this._typeButton.visible = true;
+ }
+ }
+ },
+
+ _updateRecentTypesList: function() {
+ let recentTypes = OSMTypes.recentTypesStore.recentTypes;
+
+ if (recentTypes.length > 0) {
+ let children = this._recentTypesListBox.get_children();
+
+ for (let i = 0; i < children.length; i++) {
+ children[i].destroy();
+ }
+
+ this._recentTypesLabel.visible = true;
+ this._recentTypesListBox.visible = true;
+
+ for (let i = 0; i < recentTypes.length; i++) {
+ let key = recentTypes[i].key;
+ let value = recentTypes[i].value;
+ let title = OSMTypes.lookupType(key, value);
+
+ let row = new Gtk.ListBoxRow({visible: true, hexpand: true});
+ let grid = new Gtk.Grid({visible: true,
+ margin_top: 6, margin_bottom: 6,
+ margin_start: 12, margin_end: 12});
+ let label = new Gtk.Label({visible: true, halign: Gtk.Align.START,
+ label: title});
+
+ label.get_style_context().add_class('dim-label');
+
+ row._title = title;
+ row._key = key;
+ row._value = value;
+
+ row.add(grid);
+ grid.add(label);
+
+ this._recentTypesListBox.add(row);
+ }
+ } else {
+ this._recentTypesLabel.visible = false;
+ this._recentTypesListBox.visible = false;
+ }
+ },
+
_switchToUpload: function() {
this._stack.set_visible_child_name('upload');
this._nextButton.label = _("Done");
@@ -173,6 +351,8 @@ const OSMEditDialog = new Lang.Class({
this._stack.set_visible_child_name('editor');
this._isEditing = true;
this._commentTextView.buffer.text = '';
+ this._typeSearch.text = '';
+ this._headerBar.title = this._originalTitle;
},
_onObjectFetched: function(success, status, osmObject, osmType, error) {
@@ -195,7 +375,7 @@ const OSMEditDialog = new Lang.Class({
_showError: function(status, error) {
/* set error message from specific error if available, otherwise use
- a generic error message for the HTTP status code */
+ * a generic error message for the HTTP status code */
let statusMessage =
error ? error.message : OSMConnection.getStatusMessage(status);
let messageDialog =
@@ -214,7 +394,7 @@ const OSMEditDialog = new Lang.Class({
/* GtkContainer.child_get_property doesn't seem to be usable from GJS */
_getRowOfDeleteButton: function(button) {
- for (let row = 0;; row++) {
+ for (let row = 1;; row++) {
let label = this._editorGrid.get_child_at(0, row);
let deleteButton = this._editorGrid.get_child_at(2, row);
@@ -357,8 +537,8 @@ const OSMEditDialog = new Lang.Class({
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 */
+ * 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));
@@ -368,8 +548,6 @@ const OSMEditDialog = new Lang.Class({
}
}
- /* update sensitiveness of the add details button, set it as
- insensitive if all tags we support editing is already present */
this._addFieldButton.sensitive = !hasAllFields;
},
@@ -387,15 +565,13 @@ const OSMEditDialog = new Lang.Class({
}
},
- _loadOSMData: function(osmObject, osmType) {
+ _loadOSMData: function(osmObject) {
this._osmObject = osmObject;
- this._osmType = osmType;
/* keeps track of the current insertion row in the grid for editing
- widgets */
- this._currentRow = 0;
+ * widgets */
+ this._currentRow = 1;
- /* create edit widgets */
for (let i = 0; i < OSM_FIELDS.length; i++) {
let fieldSpec = OSM_FIELDS[i];
let name = fieldSpec.name;
@@ -409,6 +585,7 @@ const OSMEditDialog = new Lang.Class({
}
this._updateAddFieldMenu();
+ this._updateTypeButton();
this._stack.visible_child_name = 'editor';
}
});
diff --git a/src/osmTypeListRow.js b/src/osmTypeListRow.js
new file mode 100644
index 00000000..ae5e17c4
--- /dev/null
+++ b/src/osmTypeListRow.js
@@ -0,0 +1,51 @@
+/* -*- 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const OSMTypeListRow = new Lang.Class({
+ Name: 'OSMTypeListRow',
+ Extends: Gtk.ListBoxRow,
+ Template: 'resource:///org/gnome/Maps/ui/osm-type-list-row.ui',
+ InternalChildren: [ 'name' ],
+
+ _init: function(props) {
+ this._type = props.type;
+ delete props.type;
+
+ this.parent(props);
+
+ this._name.label = this._type.title;
+ },
+
+ get key() {
+ return this._type.key;
+ },
+
+ get value() {
+ return this._type.value;
+ },
+
+ get title() {
+ return this._type.title;
+ }
+});
diff --git a/src/osmTypePopover.js b/src/osmTypePopover.js
new file mode 100644
index 00000000..8881a028
--- /dev/null
+++ b/src/osmTypePopover.js
@@ -0,0 +1,67 @@
+/* -*- 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const OSMTypeListRow = imports.osmTypeListRow;
+const SearchPopover = imports.searchPopover;
+
+const OSMTypePopover = new Lang.Class({
+ Name: 'OSMTypePopover',
+ Extends: SearchPopover.SearchPopover,
+ InternalChildren: ['list'],
+ Template: 'resource:///org/gnome/Maps/ui/osm-type-popover.ui',
+ Signals : {
+ /* signal emitted when selecting a type, indicates OSM key and value
+ * and display title */
+ 'selected' : { param_types: [ GObject.TYPE_STRING,
+ GObject.TYPE_STRING,
+ GObject.TYPE_STRING ] }
+ },
+
+ _init: function(props) {
+ this.parent(props);
+
+ this._list.connect('row-activated', (function(list, row) {
+ if (row)
+ this.emit('selected', row.key, row.value, row.title);
+ }).bind(this));
+ },
+
+ showMatches: function(matches) {
+ this._list.forall(function(row) {
+ row.destroy();
+ });
+
+ matches.forEach((function(type) {
+ this._addRow(type);
+ }).bind(this));
+ this.show();
+ },
+
+ _addRow: function(type) {
+ let row = new OSMTypeListRow.OSMTypeListRow({ type: type,
+ can_focus: true });
+ this._list.add(row);
+ }
+});
diff --git a/src/osmTypeSearchEntry.js b/src/osmTypeSearchEntry.js
new file mode 100644
index 00000000..2bbebbec
--- /dev/null
+++ b/src/osmTypeSearchEntry.js
@@ -0,0 +1,76 @@
+/* -*- 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+
+const OSMTypePopover = imports.osmTypePopover;
+const OSMTypes = imports.osmTypes;
+const Utils = imports.utils;
+
+const MAX_MATCHES = 10;
+
+const OSMTypeSearchEntry = new Lang.Class({
+ Name: 'OSMTypeSearchEntry',
+ Extends: Gtk.SearchEntry,
+ Template: 'resource:///org/gnome/Maps/ui/osm-type-search-entry.ui',
+
+ _init: function(props) {
+ this.parent(props);
+
+ this._popover =
+ new OSMTypePopover.OSMTypePopover({relative_to: this});
+
+ this.connect('size-allocate', (function(widget, allocation) {
+ /* Magic number to make the alignment pixel perfect. */
+ let width_request = allocation.width + 20;
+ this._popover.width_request = width_request;
+ }).bind(this));
+
+ this.connect('search-changed', this._onSearchChanged.bind(this));
+ this.connect('activate', this._onSearchChanged.bind(this));
+ },
+
+ get popover() {
+ return this._popover;
+ },
+
+ _onSearchChanged: function() {
+ if (this.text.length === 0) {
+ this._popover.hide();
+ return;
+ }
+
+ /* Note: Not sure if searching already on one character might be a bit
+ * too much, but unsure about languages such as Chinese and Japanese
+ * using ideographs. */
+ if (this.text.length >= 1) {
+ let matches = OSMTypes.findMatches(this.text, MAX_MATCHES);
+
+ if (matches.length > 0) {
+ /* show search results */
+ this._popover.showMatches(matches);
+ } else {
+ this._popover.hide();
+ }
+ }
+ }
+});
diff --git a/src/osmTypes.js b/src/osmTypes.js
new file mode 100644
index 00000000..c464a743
--- /dev/null
+++ b/src/osmTypes.js
@@ -0,0 +1,168 @@
+/* -*- 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, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Marcus Lundblad <ml@update.uu.se>
+ */
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+
+const Utils = imports.utils;
+
+const _RECENT_TYPES_STORE_FILE = 'maps-recent-types.json';
+const _NUM_RECENT_TYPES = 10;
+
+/* Lists the OSM tags we base our notion of location types on */
+const OSM_TYPE_TAGS = ['amenity', 'leisure', 'office', 'place', 'shop', 'tourism' ];
+
+const _file = Gio.file_new_for_uri('resource://org/gnome/Maps/osm-types.json');
+const [_status, _buffer] = _file.load_contents(null);
+const OSM_TYPE_MAP = JSON.parse(_buffer);
+
+/* Sort function comparing two type values accoring to the locale-specific
+ * comparison of the type title */
+function _sortType(t1, t2) {
+ return t1.title.toLocaleLowerCase().localeCompare(t2.title.toLocaleLowerCase());
+}
+
+/* find the localized display title and a normalized locale-specific lower case
+ * value for search purposes, given a type mapping value,
+ * also cache the title to avoid re-iterating the language map every time,
+ * and store the lower-case normalized title in the current locale */
+function _lookupTitle(item) {
+ let langs = GLib.get_language_names();
+ let title = item.cachedTitle;
+
+ if (title)
+ return [title, item.normalizedTitle];
+
+ for (let i = 0; i < langs.length; i++) {
+ title = item.title[langs[i].replace('_', '-')];
+
+ if (title) {
+ let normalizedTitle = title.toLocaleLowerCase();
+
+ item.cachedTitle = title;
+ item.normalizedTitle = normalizedTitle;
+ return [title, normalizedTitle];
+ }
+ }
+
+ return null;
+}
+
+function findMatches(prefix, maxMatches) {
+ let numMatches = 0;
+ let prefixLength = prefix.length;
+ let normalized = prefix.toLocaleLowerCase();
+ let matches = [];
+
+ for (let type in OSM_TYPE_MAP) {
+ let item = OSM_TYPE_MAP[type];
+ let [title, normalizedTitle] = _lookupTitle(item);
+ let parts = type.split('/');
+
+ /* if the (locale-case-normalized) title matches parts of the search
+ * string, or as a convenience for expert mappers, if the search string
+ * is prefix of the raw OSM tag value */
+ if (normalizedTitle.indexOf(normalized) != -1
+ || (prefixLength >= 3 && parts[1].startsWith(prefix))) {
+ numMatches++;
+ matches.push({key: parts[0], value: parts[1], title: title});
+ }
+
+ if (numMatches === maxMatches)
+ break;
+ }
+
+ return matches.sort(_sortType);
+}
+
+/* return the title of a type with a given key/value if it is known by us */
+function lookupType(key, value) {
+ let item = OSM_TYPE_MAP[key + '/' + value];
+
+ if (item) {
+ let [title, _] = _lookupTitle(item);
+ return title;
+ } else
+ return null;
+}
+
+const RecentTypesStore = new Lang.Class({
+ Name: 'RecentTypesStore',
+
+ _init: function() {
+ this.parent();
+ this._filename = GLib.build_filenamev([GLib.get_user_data_dir(),
+ _RECENT_TYPES_STORE_FILE]);
+ this._load();
+ },
+
+ get recentTypes() {
+ return this._recentTypes;
+ },
+
+ _load: function() {
+ if (!GLib.file_test(this._filename, GLib.FileTest.EXISTS)) {
+ this._recentTypes = [];
+ return;
+ }
+
+ let buffer = Utils.readFile(this._filename);
+ if (buffer === null) {
+ this._recentTypes = [];
+ return;
+ }
+
+ this._recentTypes = JSON.parse(buffer);
+ },
+
+ _save: function() {
+ let buffer = JSON.stringify(this._recentTypes);
+ if (!Utils.writeFile(this._filename, buffer))
+ log('Failed to write recent types file!');
+ },
+
+ /* push a type key/value as the most recently used type */
+ pushType: function(key, value) {
+ /* find out if the type is already stored */
+ let pos = -1;
+ for (let i = 0; i < this._recentTypes.length; i++) {
+ if (this._recentTypes[i].key === key &&
+ this._recentTypes[i].value === value) {
+ pos = i;
+ break;
+ }
+ }
+
+ /* remove the type if it was already found in the list */
+ if (pos != -1)
+ this._recentTypes.splice(pos, 1);
+
+ this._recentTypes.unshift({key: key, value: value});
+
+ /* prune the list */
+ this._recentTypes.splice(_NUM_RECENT_TYPES);
+
+ this._save();
+ }
+});
+
+const recentTypesStore = new RecentTypesStore();