diff options
author | James Westman <james@flyingpimonster.net> | 2020-12-28 16:01:49 -0600 |
---|---|---|
committer | Marcus Lundblad <ml@update.uu.se> | 2021-02-09 20:48:40 +0000 |
commit | 39cf3f63dd9a84e80a2be3867dd4aab8232151ef (patch) | |
tree | ff2ebdc9077e51c6e834bca5a686a5aa2c5a4560 /src/placeView.js | |
parent | e3368acc19676628454ef87cb0a60954e13b18ad (diff) | |
download | gnome-maps-39cf3f63dd9a84e80a2be3867dd4aab8232151ef.tar.gz |
Rename placeBubble -> placeView
It's no longer a subclass of MapBubble, so PlaceView is a more fitting name.
Also rename PlaceBubbleImage -> PlaceViewImage.
Diffstat (limited to 'src/placeView.js')
-rw-r--r-- | src/placeView.js | 550 |
1 files changed, 550 insertions, 0 deletions
diff --git a/src/placeView.js b/src/placeView.js new file mode 100644 index 00000000..628dee03 --- /dev/null +++ b/src/placeView.js @@ -0,0 +1,550 @@ +/* -*- 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, see <http://www.gnu.org/licenses/>. + * + * Author: Damián Nohales <damiannohales@gmail.com> + */ + +const Geocode = imports.gi.GeocodeGlib; +const GLib = imports.gi.GLib; +const GObject = imports.gi.GObject; +const Gtk = imports.gi.Gtk; +const Pango = imports.gi.Pango; + +const Format = imports.format; + +const Application = imports.application; +const ContactPlace = imports.contactPlace; +const Overpass = imports.overpass; +const Place = imports.place; +const PlaceViewImage = imports.placeViewImage; +const PlaceButtons = imports.placeButtons; +const PlaceFormatter = imports.placeFormatter; +const PlaceStore = imports.placeStore; +const Translations = imports.translations; +const Utils = imports.utils; +const Wikipedia = imports.wikipedia; + +// maximum dimension of thumbnails to fetch from Wikipedia +const THUMBNAIL_FETCH_SIZE = 360; + +var PlaceView = GObject.registerClass({ + Properties: { + 'overpass-place': GObject.ParamSpec.object('overpass-place', + 'Overpass Place', + 'The place as filled in by Overpass', + GObject.ParamFlags.READABLE | + GObject.ParamFlags.WRITABLE, + Geocode.Place) + } +}, class PlaceView extends Gtk.Box { + + _init(params) { + this._place = params.place; + delete params.place; + + let mapView = params.mapView; + delete params.mapView; + + super._init(params); + + let ui = Utils.getUIObject('place-view', [ 'bubble-main-box', + 'bubble-spinner', + 'bubble-thumbnail', + 'thumbnail-separator', + 'label-title', + 'contact-avatar', + 'address-label', + 'bubble-main-stack', + 'bubble-content-area', + 'place-buttons', + 'send-to-button-alt', + 'title-box' ]); + this._title = ui.labelTitle; + this._thumbnail = ui.bubbleThumbnail; + this._thumbnailSeparator = ui.thumbnailSeparator; + this._content = ui.bubbleContentArea; + this._mainStack = ui.bubbleMainStack; + this._spinner = ui.bubbleSpinner; + this._mainBox = ui.bubbleMainBox; + this._contactAvatar = ui.contactAvatar; + this._addressLabel = ui.addressLabel; + + this.add(this._mainStack); + + let placeButtons = new PlaceButtons.PlaceButtons({ place: this._place, + mapView: mapView }); + ui.placeButtons.add(placeButtons); + + if (this.place.isCurrentLocation) { + /* Current Location bubbles have a slightly different layout, to + avoid awkward whitespace */ + + /* hide the normal button area */ + ui.placeButtons.visible = false; + + /* show the top-end-corner share button instead */ + ui.sendToButtonAlt.visible = true; + placeButtons.initSendToButton(ui.sendToButtonAlt); + + /* adjust some margins */ + ui.titleBox.margin = 12; + ui.titleBox.marginStart = 18; + ui.titleBox.spacing = 18; + } + + /* Set up contact avatar */ + if (this.place instanceof ContactPlace.ContactPlace) { + this._contactAvatar.visible = true; + Utils.load_icon(this.place.icon, 32, (pixbuf) => { + this._contactAvatar.set_image_load_func((size) => Utils.loadAvatar(pixbuf, size)); + }); + } + + this.loading = true; + + this._placeDetails = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, + visible: true}); + this.content.add(this._placeDetails); + + if (this.place.isCurrentLocation) { + this._populate(this.place); + } else { + let overpass = new Overpass.Overpass(); + + /* use a property binding from the Overpass instance to avoid + * accessing this object after the underlying GObject has + * been finalized */ + overpass.bind_property('place', this, 'overpass-place', + GObject.BindingFlags.DEFAULT); + this.connect('notify::overpass-place', () => this._onInfoAdded()); + + if (Application.placeStore.exists(this.place, null)) { + + // If the place is stale, update from Overpass. + if (Application.placeStore.isStale(this.place)) { + overpass.addInfo(this.place); + } else { + this._place = Application.placeStore.get(this.place); + this._populate(this.place); + } + } else if (this.place.store && !this.place.prefilled) { + overpass.addInfo(this.place); + } else { + this._populate(this.place); + } + } + + this.updatePlaceDetails(); + } + + updateLocation() { + /* Called by the UserLocationMarker when its location changes */ + this._populate(this.place); + } + + get place() { + return this._place; + } + + get content() { + return this._content; + } + + get thumbnail() { + return this._thumbnail.pixbuf; + } + + set thumbnail(val) { + if (val) { + this._thumbnail.pixbuf = val; + this._thumbnail.visible = true; + this._thumbnailSeparator.visible = true; + } + } + + get loading() { + return this._spinner.active; + } + set loading(val) { + this._mainStack.set_visible_child(val ? this._spinner : this._mainBox); + this._spinner.active = val; + } + + updatePlaceDetails() { + let place = this.place; + let formatter = new PlaceFormatter.PlaceFormatter(place); + + let address = formatter.rows.map((row) => { + row = row.map(function(prop) { + return GLib.markup_escape_text(place[prop], -1); + }); + return row.join(', '); + }); + if (address.length > 0) { + this._addressLabel.label = address.join('\n'); + this._addressLabel.show(); + } else { + this._addressLabel.hide(); + } + + this._title.label = formatter.title; + this._contactAvatar.text = formatter.title; + } + + _onInfoAdded() { + this._populate(this.place); + if (Application.placeStore.exists(this.place, null)) + Application.placeStore.updatePlace(this.place); + else + Application.placeStore.addPlace(this.place, PlaceStore.PlaceType.RECENT); + } + + _formatWikiLink(wiki) { + let lang = Wikipedia.getLanguage(wiki); + let article = Wikipedia.getArticle(wiki); + + return Format.vprintf('https://%s.wikipedia.org/wiki/%s', [ lang, article ]); + } + + _createContent(place) { + let content = []; + + if (place.isCurrentLocation) { + let coordinates = place.location.latitude.toFixed(5) + + ', ' + + place.location.longitude.toFixed(5); + let accuracyDescription = Utils.getAccuracyDescription(this.place.location.accuracy); + + content.push({ label: _("Coordinates"), + icon: 'map-marker-symbolic', + info: coordinates }); + + content.push({ label: _("Accuracy"), + icon: 'find-location-symbolic', + /* Translators: %s can be "Unknown", "Exact" or "%f km" (or ft/mi/m) */ + info: _("Accuracy: %s").format(accuracyDescription) }); + } + + if (place.website) { + if (Utils.isValidWebsite(place.website)) { + content.push({ label: _("Website"), + icon: 'web-browser-symbolic', + info: GLib.markup_escape_text(place.website, -1), + linkUrl: place.website }); + } + } + + if (place.phone) { + let phone = { label: _("Phone number"), + icon: 'phone-oldschool-symbolic', + info: GLib.markup_escape_text(place.phone, -1) }; + + if (Utils.uriSchemeSupported('tel')) { + /* RFC3966 only allows "-", '.", "(", and ")" as visual + * separator characters in a global phone number, no space */ + phone.linkUrl = 'tel:%s'.format(place.phone.replace(/\s+/g, '')); + } + + content.push(phone); + } + + if (place.openingHours) { + content.push({ label: _("Opening hours"), + icon: 'emoji-recent-symbolic', + grid: Translations.translateOpeningHours(place.openingHours) }); + } + + switch(place.internetAccess) { + case 'yes': + /* Translators: + * There is public internet access but the particular kind is unknown. + */ + content.push({ info: _("Public internet access"), + icon: 'network-wireless-signal-excellent-symbolic' }); + break; + + case 'no': + /* Translators: + * no internet access is offered in a place where + * someone might expect it. + */ + content.push({ info: _("No internet access"), + icon: 'network-wireless-offline-symbolic' }); + break; + + case 'wlan': + /* Translators: + * This means a WLAN Hotspot, also known as wireless, wifi or Wi-Fi. + */ + content.push({ info: _("Public Wi-Fi"), + icon: 'network-wireless-signal-excellent-symbolic' }); + break; + + case 'wired': + /* Translators: + * This means a a place where you can plug in your laptop with ethernet. + */ + content.push({ info: _("Wired internet access"), + icon: 'network-wired-symbolic' }); + break; + + case 'terminal': + /* Translators: + * Like internet cafe or library where the computer is given. + */ + content.push({ info: _("Computers available for use"), + icon: 'computer-symbolic' }); + break; + + case 'service': + /* Translators: + * This means there is personnel which helps you in case of problems. + */ + content.push({ info: _("Internet assistance available"), + icon: 'computer-symbolic' }); + break; + } + + if (place.toilets === 'no') { + content.push({ info: _("No toilets available"), + icon: 'no-toilets-symbolic' }); + } else if (place.toilets === 'yes') { + content.push({ info: _("Toilets available"), + icon: 'toilets-symbolic' }); + } + + switch(place.wheelchair) { + case 'yes': + /* Translators: + * This means wheelchairs have full unrestricted access. + */ + content.push({ info: _("Wheelchair accessible"), + icon: 'wheelchair-symbolic' }); + break; + + case 'limited': + /* Translators: + * This means wheelchairs have partial access (e.g some areas + * can be accessed and others not, areas requiring assistance + * by someone pushing up a steep gradient). + */ + content.push({ info: _("Limited wheelchair accessibility"), + icon: 'wheelchair-limited-symbolic' }); + break; + + case 'no': + /* Translators: + * This means wheelchairs have no unrestricted access + * (e.g. stair only access). + */ + content.push({ info: _("Not wheelchair accessible"), + icon: 'no-wheelchair-symbolic' }); + break; + + case 'designated': + /* Translators: + * This means that the way or area is designated or purpose built + * for wheelchairs (e.g. elevators designed for wheelchair access + * only). This is rarely used. + */ + content.push({ info: _("Designated for wheelchair users"), + icon: 'wheelchair-symbolic' }); + break; + } + + if (place.population) { + /* TODO: this is a bit of a work-around to re-interpret the population, + * stored as a string into an integer to convert back to a locale- + * formatted string. Ideally it should be kept as an integer value + * in the Place class. But this will also need to be handled by the + * PlaceStore, possible in a backwards-compatible way + */ + content.push({ label: _("Population"), + icon: 'system-users-symbolic', + info: parseInt(place.population).toLocaleString() }); + } + + /* The default value for a place's altitude is -G_MAXDOUBLE, so we can + * compare to an impossibly low altitude to see if one is set */ + if (place.location.altitude > -1000000000.0) { + let alt = place.location.altitude; + content.push({ label: _("Altitude"), + icon: 'mountain-symbolic', + info: Utils.prettyDistance(alt, true) }); + } + + if (place.religion) { + content.push({ label: _("Religion:"), + info: Translations.translateReligion(place.religion) }); + } + + if (place.wiki) { + content.push({ type: 'wikipedia', info: '' }); + } + + return content; + } + + _attachContent(content) { + content.forEach(({ type, label, icon, linkUrl, info, grid }) => { + let separator = new Gtk.Separator({ visible: true }); + separator.get_style_context().add_class('no-margin-separator'); + this._placeDetails.add(separator); + + let box = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, + visible: true, + marginStart: 18, + marginEnd: 18, + marginTop: 6, + marginBottom: 6, + spacing: 12 }); + + if (icon) { + let widget = new Gtk.Image({ icon_name: icon, + visible: true, + xalign: 1, + valign: Gtk.Align.START, + halign: Gtk.Align.END }); + + if (label) { + widget.tooltip_markup = label; + } + + box.add(widget); + } else if (label) { + let widget = new Gtk.Label({ label: label.italics(), + visible: true, + use_markup: true, + yalign: 0, + halign: Gtk.Align.END }); + box.add(widget); + } + + if (linkUrl) { + let uri = GLib.markup_escape_text(linkUrl, -1); + /* double-escape the tooltip text, as GTK treats it as markup */ + let tooltipText = GLib.markup_escape_text(uri, -1); + info = '<a href="%s" title="%s">%s</a>'.format(uri, + tooltipText, + info); + } + + let widget; + + if (grid) { + widget = new Gtk.Grid({ visible: true, + column_spacing: 8 }); + + for (let i = 0; i < grid.length; i++) { + let row = grid[i]; + + for (let j = 0; j < row.length; j++) { + let label = new Gtk.Label({ label: row[j], + visible: true, + xalign: 0, + hexpand: false, + halign: Gtk.Align.FILL }); + + if (j === 1) { + /* set tabular digits for the second column to get + * aligned times + */ + let attrList = Pango.AttrList.new(); + let tnum = Pango.AttrFontFeatures.new('tnum'); + + attrList.insert(tnum); + label.set_attributes(attrList); + } + + widget.attach(label, j, i, 1, 1); + } + } + } else { + widget = new Gtk.Label({ label: info, + visible: true, + use_markup: true, + max_width_chars: 30, + wrap: true, + xalign: 0, + hexpand: true, + halign: Gtk.Align.FILL }); + + if (type === 'wikipedia') { + box.marginTop = 14; + box.marginBottom = 18; + this._wikipediaLabel = widget; + } + } + + box.add(widget); + this._placeDetails.add(box); + }); + } + + _populate(place) { + // refresh place view + this._clearView(); + + let content = this._createContent(place); + this._attachContent(content); + + if (place.wiki) { + this._requestWikipedia(place.wiki); + } + + this.updatePlaceDetails(); + this.loading = false; + } + + _requestWikipedia(wiki) { + Wikipedia.fetchArticleInfo(wiki, + THUMBNAIL_FETCH_SIZE, + this._onWikiMetadataComplete.bind(this), + this._onThumbnailComplete.bind(this)); + } + + _onThumbnailComplete(thumbnail) { + this.thumbnail = thumbnail; + } + + _onWikiMetadataComplete(wiki, metadata) { + if (metadata.extract) { + let text = GLib.markup_escape_text(metadata.extract, -1); + let link = this._formatWikiLink(wiki); + + /* If the text goes past some number of characters (see + * wikipedia.js), it is ellipsized with '...' + * GNOME HIG says to use U+2026 HORIZONTAL ELLIPSIS instead. + * Also, trim whitespace. */ + text = text.replace(/\s*\.\.\.\s*$/, '…'); + + let uri = GLib.markup_escape_text(link, -1); + /* double-escape the tooltip text, as GTK treats it as markup */ + let tooltipText = GLib.markup_escape_text(uri, -1); + + /* Translators: This is the text for the "Wikipedia" link at the end + of summaries */ + this._wikipediaLabel.label = `${text} <a href="${link}" title="${tooltipText}">${ _("Wikipedia") }</a>`; + } + } + + // clear the view widgets to be able to re-populate an updated place + _clearView() { + this._placeDetails.get_children().forEach((child) => child.destroy()); + } +}); |