/* -*- Mode: JS2; indent-tabs-mode: nil; js2-basic-offset: 4 -*- */ /* vim: set et ts=4 sw=4: */ /* * 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 . * * Author: Jonas Danielsson */ const GLib = imports.gi.GLib; const GObject = imports.gi.GObject; const GdkPixbuf = imports.gi.GdkPixbuf; const Geocode = imports.gi.GeocodeGlib; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const ContactPlace = imports.contactPlace; const Place = imports.place; const StoredRoute = imports.storedRoute; const Utils = imports.utils; const _PLACES_STORE_FILE = 'maps-places.json'; const _ICON_SIZE = 20; const _ONE_DAY = 1000 * 60 * 60 * 24; // one day in ms const _STALE_THRESHOLD = 7; // mark the osm information as stale after a week const PlaceType = { ANY: -1, RECENT: 0, FAVORITE: 1, CONTACT: 2, RECENT_ROUTE: 3 }; const Columns = { PLACE_ICON: 0, PLACE: 1, NAME: 2, TYPE: 3, ADDED: 4 }; const PlaceStore = new Lang.Class({ Name: 'PlaceStore', Extends: Gtk.ListStore, _init: function(params) { this._recentPlacesLimit = params.recentPlacesLimit; delete params.recentPlacesLimit; this._recentRoutesLimit = params.recentRoutesLimit; delete params.recentRoutesLimit; this._numRecentPlaces = 0; this._numRecentRoutes = 0; this.filename = GLib.build_filenamev([GLib.get_user_data_dir(), _PLACES_STORE_FILE]); this._typeTable = {}; this.parent(); this.set_column_types([GdkPixbuf.Pixbuf, GObject.TYPE_OBJECT, GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_DOUBLE]); this.set_sort_column_id(Columns.ADDED, Gtk.SortType.ASCENDING); }, _addPlace: function(place, type) { this._setPlace(this.append(), place, type, new Date().getTime()); this._store(); }, _addContact: function(place) { if (this.exists(place, PlaceType.CONTACT)) { return; } this._addPlace(place, PlaceType.CONTACT); }, _addFavorite: function(place) { if (!place.store) return; if (this.exists(place, PlaceType.FAVORITE)) { return; } if (this.exists(place, PlaceType.RECENT)) { this._removeIf((function(model, iter) { let p = model.get_value(iter, Columns.PLACE); return p.uniqueID === place.uniqueID; }).bind(this), true); } this._addPlace(place, PlaceType.FAVORITE); }, _addRecent: function(place) { if (!place.store) return; if (this.exists(place, PlaceType.RECENT)) { this.updatePlace(place); return; } if (this._numRecentPlaces === this._recentPlacesLimit) { // Since we sort by added, the oldest recent will be // the first one we encounter. this._removeIf((function(model, iter) { let type = model.get_value(iter, Columns.TYPE); if (type === PlaceType.RECENT) { let place = model.get_value(iter, Columns.PLACE); this._typeTable[place.uniqueID] = null; this._numRecentPlaces--; return true; } return false; }).bind(this), true); } this._addPlace(place, PlaceType.RECENT); this._numRecentPlaces++; }, _addRecentRoute: function(stored) { if (this.exists(stored, PlaceType.RECENT_ROUTE)) return; if (stored.containsCurrentLocation) return; if (this._numRecentRoutes >= this._recentRoutesLimit) { this._removeIf((function(model, iter) { let type = model.get_value(iter, Columns.TYPE); if (type === PlaceType.RECENT_ROUTE) { let place = model.get_value(iter, Columns.PLACE); this._typeTable[place.uniqueID] = null; this._numRecentRoutes--; return true; } return false; }).bind(this), true); } this._addPlace(stored, PlaceType.RECENT_ROUTE); this._numRecentRoutes++; }, load: function() { if (!GLib.file_test(this.filename, GLib.FileTest.EXISTS)) return; let buffer = Utils.readFile(this.filename); if (buffer === null) return; try { let jsonArray = JSON.parse(buffer); jsonArray.forEach((function({ place, type, added }) { // We expect exception to be thrown in this line when parsing // gnome-maps 3.14 or below place stores since the "place" // key is not present. if (!place.id) return; let p; if (type === PlaceType.RECENT_ROUTE) { if (this._numRecentRoutes >= this._recentRoutesLimit) return; p = StoredRoute.StoredRoute.fromJSON(place); this._numRecentRoutes++; } else { p = Place.Place.fromJSON(place); if (type === PlaceType.RECENT) this._numRecentPlaces++; } this._setPlace(this.append(), p, type, added); }).bind(this)); } catch (e) { throw new Error('failed to parse places file'); } }, addPlace: function(place, type) { if (type === PlaceType.FAVORITE) this._addFavorite(place, type); else if (type === PlaceType.RECENT) this._addRecent(place, type); else if (type === PlaceType.CONTACT) this._addContact(place, type); else if (type === PlaceType.RECENT_ROUTE) this._addRecentRoute(place); }, removePlace: function(place, placeType) { if (!this.exists(place, placeType)) return; this._removeIf((function(model, iter) { let p = model.get_value(iter, Columns.PLACE); if (p.uniqueID === place.uniqueID) { this._typeTable[place.uniqueID] = null; return true; } return false; }).bind(this), true); this._store(); }, getModelForPlaceType: function(placeType) { let filter = new Gtk.TreeModelFilter({ child_model: this }); filter.set_visible_func(function(model, iter) { let type = model.get_value(iter, Columns.TYPE); return (type === placeType); }); return filter; }, _store: function() { let jsonArray = []; this.foreach(function(model, path, iter) { let place = model.get_value(iter, Columns.PLACE); let type = model.get_value(iter, Columns.TYPE); let added = model.get_value(iter, Columns.ADDED); if (!place || !place.store) return; jsonArray.push({ place: place.toJSON(), type: type, added: added }); }); let buffer = JSON.stringify(jsonArray); if (!Utils.writeFile(this.filename, buffer)) log('Failed to write places file!'); }, _setPlace: function(iter, place, type, added) { this.set(iter, [Columns.PLACE, Columns.NAME, Columns.TYPE, Columns.ADDED], [place, place.name, type, added]); if (place.icon !== null) { Utils.load_icon(place.icon, _ICON_SIZE, (function(pixbuf) { this.set(iter, [Columns.ICON], [pixbuf]); }).bind(this)); } this._typeTable[place.uniqueID] = type; }, get: function(place) { let storedPlace = null; this.foreach((function(model, path, iter) { let p = model.get_value(iter, Columns.PLACE); if (p.uniqueID === place.uniqueID) { storedPlace = p; return true; } return false; }).bind(this)); return storedPlace; }, isStale: function(place) { if (!this.exists(place, null)) return false; let added = null; this.foreach(function(model, path, iter) { let p = model.get_value(iter, Columns.PLACE); if (p.uniqueID === place.uniqueID) { let p_type = model.get_value(iter, Columns.TYPE); added = model.get_value(iter, Columns.ADDED); } }); let now = new Date().getTime(); let days = Math.abs(now - added) / _ONE_DAY; return (days >= _STALE_THRESHOLD); }, exists: function(place, type) { if (type !== undefined && type !== null) return this._typeTable[place.uniqueID] === type; else return this._typeTable[place.uniqueID] !== undefined; }, _removeIf: function(evalFunc, stop) { this.foreach((function(model, path, iter) { if (evalFunc(model, iter)) { this.remove(iter); if (stop) return true; } return false; }).bind(this)); }, updatePlace: function(place) { this.foreach((function(model, path, iter) { let p = model.get_value(iter, Columns.PLACE); if (p.uniqueID === place.uniqueID) { let type = model.get_value(iter, Columns.TYPE); this._setPlace(iter, place, type, new Date().getTime()); this._store(); return; } }).bind(this)); } });