/* -*- 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, see . * * Author: Zeeshan Ali (Khattak) * Damián Nohales */ import Clutter from 'gi://Clutter'; import GObject from 'gi://GObject'; import {BoundingBox} from './boundingBox.js'; import {Location} from './location.js'; import * as PlaceZoom from './placeZoom.js'; import * as Utils from './utils.js'; const _MAX_DISTANCE = 19850; // half of Earth's circumference (km) const _MIN_ANIMATION_DURATION = 2000; // msec const _MAX_ANIMATION_DURATION = 5000; // msec export class MapWalker extends GObject.Object { constructor(place, mapView) { super(); this.place = place; this._mapView = mapView; this._view = mapView.view; this._boundingBox = this._createBoundingBox(this.place); } _createBoundingBox(place) { if (place.bounding_box !== null) { return new 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() { let zoom; if (this.place.initialZoom) { zoom = this.place.initialZoom; } else { zoom = PlaceZoom.getZoomLevelForPlace(this.place) ?? this._view.max_zoom_level; /* If the place has a bounding box, use the lower of the default * zoom level based on the place's type and the zoom level needed * fit the bounding box. This way we ensure the bounding box will * be all visible and we also have an appropriate amount * of context for the place */ if (this._boundingBox !== null && this._boundingBox.isValid()) { let bboxZoom = this._mapView.getZoomLevelFittingBBox(this._boundingBox); zoom = Math.min(zoom, bboxZoom); } } this._view.zoom_level = zoom; this._view.center_on(this.place.location.latitude, this.place.location.longitude); } goTo(animate, linear) { Utils.debug('Going to ' + [this.place.name, this.place.location.latitude, this.place.location.longitude].join(' ')); this._mapView.emit('going-to'); if (!animate) { this._view.center_on(this.place.location.latitude, this.place.location.longitude); this.emit('gone-to'); return; } let fromLocation = new Location({ latitude: this._view.get_center_latitude(), longitude: this._view.get_center_longitude() }); this._updateGoToDuration(fromLocation); if (linear) { this._view.goto_animation_mode = Clutter.AnimationMode.LINEAR; Utils.once(this._view, 'animation-completed', this.zoomToFit.bind(this)); this._view.go_to(this.place.location.latitude, this.place.location.longitude); } else { /* 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; this._ensureVisible(fromLocation); Utils.once(this._view, 'animation-completed', () => { this._view.goto_animation_mode = Clutter.AnimationMode.EASE_OUT_CUBIC; this._view.go_to(this.place.location.latitude, this.place.location.longitude); Utils.once(this._view, 'animation-completed::go-to', () => { this.zoomToFit(); this._view.goto_animation_mode = Clutter.AnimationMode.EASE_IN_OUT_CUBIC; this.emit('gone-to'); }); }); } } _ensureVisible(fromLocation) { let visibleBox = null; if (this._boundingBox !== null && this._boundingBox.isValid()) { visibleBox = this._boundingBox.copy(); visibleBox.extend(fromLocation.latitude, fromLocation.longitude); } else { visibleBox = new BoundingBox({ left: 180, right: -180, bottom: 90, top: -90 }); [fromLocation, this.place.location].forEach((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); }); } let [lon, lat] = visibleBox.getCenter(); this._view.zoom_level = this._mapView.getZoomLevelFittingBBox(visibleBox); this._view.go_to(lat, lon); } _boxCovers(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(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; } } GObject.registerClass({ Signals: { 'gone-to': { } } }, MapWalker);