itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
152 lines (138 loc) • 9.29 kB
JavaScript
import { Coordinates } from '@itowns/geographic';
import { MAIN_LOOP_EVENTS, PlanarView, CAMERA_TYPE } from 'itowns';
import Widget from "./Widget.js";
const DEFAULT_OPTIONS = {
minScale: 1 / 500000,
maxScale: 1 / 5E8,
zoomRatio: 1 / 30,
width: 150,
height: 150,
position: 'bottom-left'
};
/**
* A widget for minimap
*
* To use it, you need to link the widgets' stylesheet to your html webpage. This stylesheet is included in
* [itowns bundles](https://github.com/iTowns/itowns/releases) if you downloaded them, or it can be found in
* `node_modules/itowns/examples/css` if you installed iTowns with npm. Otherwise, it can be found at
* [this link](https://raw.githubusercontent.com/iTowns/itowns/master/examples/css/widgets.css). See
* [this example](http://www.itowns-project.org/itowns/examples/#widgets_minimap) for more details.
*
* @extends Widget
*
* @property {HTMLElement} domElement An html div containing the minimap.
* @property {HTMLElement} parentElement The parent HTML container of `this.domElement`.
*/
class Minimap extends Widget {
/**
* @param {GlobeView} view The iTowns view the minimap should be
* linked to. Only {@link GlobeView} is
* supported at the moment.
* @param {ColorLayer} layer The {@link ColorLayer} that should be
* displayed on the minimap.
* @param {Object} [options] The minimap optional configuration.
* @param {HTMLElement} [options.parentElement=view.domElement] The parent HTML container of the div
* which contains minimap widgets.
* @param {number} [options.size] The size of the minimap. It is a number
* that describes both width and height
* in pixels of the minimap.
* @param {number} [options.width=150] The width in pixels of the minimap.
* @param {number} [options.height=150] The height in pixels of the minimap.
* @param {string} [options.position='bottom-left'] Defines which position within the
* `parentElement` the minimap should be
* displayed to. Possible values are
* `top`, `bottom`, `left`, `right`,
* `top-left`, `top-right`, `bottom-left`
* and `bottom-right`. If the input value
* does not match one of these, it will
* be defaulted to `bottom-left`.
* @param {Object} [options.translate] An optional translation of the minimap.
* @param {number} [options.translate.x=0] The minimap translation along the page
* x-axis.
* @param {number} [options.translate.y=0] The minimap translation along the page
* y-axis.
* @param {HTMLElement|string} [options.cursor] An html element or an HTML string
* describing a cursor showing minimap
* view camera target position at the
* center of the minimap.
* @param {number} [options.minScale=1/2000] The minimal scale the minimap can reach.
* @param {number} [options.maxScale=1/1_250_000] The maximal scale the minimap can reach.
* @param {number} [options.zoomRatio=1/30] The ratio between minimap camera zoom
* and view camera zoom.
* @param {number} [options.pitch=0.28] The screen pixel pitch, used to compute
* view and minimap scale.
*/
constructor(view, layer) {
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
// ---------- BUILD PROPERTIES ACCORDING TO DEFAULT OPTIONS AND OPTIONS PASSED IN PARAMETERS : ----------
if (!view.isGlobeView) {
throw new Error('\'Minimap\' plugin only supports \'GlobeView\'. Therefore, the \'view\' parameter must be a ' + '\'GlobeView\'.');
}
if (!layer.isColorLayer) {
throw new Error('\'layer\' parameter form \'Minimap\' constructor should be a \'ColorLayer\'.');
}
super(view, options, DEFAULT_OPTIONS);
this.minScale = options.minScale || DEFAULT_OPTIONS.minScale;
this.maxScale = options.maxScale || DEFAULT_OPTIONS.maxScale;
// TODO : it could be interesting to be able to specify a method as zoomRatio parameter. This method could
// return a zoom ratio from the scale of the minimap.
this.zoomRatio = options.zoomRatio || DEFAULT_OPTIONS.zoomRatio;
// ---------- this.domElement SETTINGS SPECIFIC TO MINIMAP : ----------
this.domElement.id = 'widgets-minimap';
// Display a cursor at the center of the minimap, if requested.
if (options.cursor) {
// Wrap cursor domElement inside a div to center it in minimap.
const cursorWrapper = document.createElement('div');
cursorWrapper.id = 'cursor-wrapper';
this.domElement.appendChild(cursorWrapper);
// Add specified cursor to its wrapper.
if (typeof options.cursor === 'string') {
cursorWrapper.innerHTML = options.cursor;
} else if (options.cursor instanceof HTMLElement) {
cursorWrapper.appendChild(options.cursor);
}
}
// ---------- CREATE A MINIMAP View AND DISPLAY DATA PASSED IN Layer PARAMETER : ----------
this.view = new PlanarView(this.domElement, layer.source.extent, {
camera: {
type: CAMERA_TYPE.ORTHOGRAPHIC
},
placement: layer.source.extent,
// TODO : the default placement should be the view extent for ortho camera
noControls: true,
maxSubdivisionLevel: view.tileLayer.maxSubdivisionLevel,
disableFocusOnStart: true
});
this.view.addLayer(layer); // TODO : should this promise be returned by constructor so that user can use it ?
// Prevent the minimap domElement to get focus when clicked, and prevent click event to be propagated to the
// main view controls.
this.domElement.addEventListener('pointerdown', event => {
event.preventDefault();
});
// ---------- UPDATE MINIMAP VIEW WHEN UPDATING THE MAIN VIEW : ----------
// The minimal and maximal value the minimap camera3D zoom can reach in order to stay in the scale limits.
const initialScale = this.view.getScale(options.pitch);
const minZoom = this.view.camera3D.zoom * this.maxScale / initialScale;
const maxZoom = this.view.camera3D.zoom * this.minScale / initialScale;
// Coordinates used to transform position vectors from the main view CRS to the minimap view CRS.
const mainViewCoordinates = new Coordinates(view.referenceCrs);
const viewCoordinates = new Coordinates(this.view.referenceCrs);
const targetPosition = view.controls.getCameraTargetPosition();
view.addFrameRequester(MAIN_LOOP_EVENTS.AFTER_RENDER, () => {
// Update minimap camera zoom
const distance = view.camera3D.position.distanceTo(targetPosition);
const scale = view.getScaleFromDistance(options.pitch, distance);
this.view.camera3D.zoom = this.zoomRatio * maxZoom * scale / this.minScale;
this.view.camera3D.zoom = Math.min(Math.max(this.view.camera3D.zoom, minZoom), maxZoom);
this.view.camera3D.updateProjectionMatrix();
// Update minimap camera position.
mainViewCoordinates.setFromVector3(view.controls.getCameraTargetPosition());
mainViewCoordinates.as(this.view.referenceCrs, viewCoordinates);
this.view.camera3D.position.x = viewCoordinates.x;
this.view.camera3D.position.y = viewCoordinates.y;
this.view.camera3D.updateMatrixWorld(true);
this.view.notifyChange(this.view.camera3D);
});
}
}
export default Minimap;