mapbox-gl
Version:
A WebGL interactive maps library
185 lines (156 loc) • 5.8 kB
JavaScript
// @flow
import * as DOM from '../../util/dom.js';
import {extend, bindAll} from '../../util/util.js';
import type Map, {ControlPosition} from '../map.js';
type Unit = 'imperial' | 'metric' | 'nautical';
type Options = {
maxWidth?: number,
unit?: Unit;
};
const defaultOptions: Options = {
maxWidth: 100,
unit: 'metric'
};
const unitAbbr = {
kilometer: 'km',
meter: 'm',
mile: 'mi',
foot: 'ft',
'nautical-mile': 'nm',
};
/**
* A `ScaleControl` control displays the ratio of a distance on the map to the corresponding distance on the ground.
* Add this control to a map using {@link Map#addControl}.
*
* @implements {IControl}
* @param {Object} [options]
* @param {number} [options.maxWidth='100'] The maximum length of the scale control in pixels.
* @param {string} [options.unit='metric'] Unit of the distance (`'imperial'`, `'metric'` or `'nautical'`).
* @example
* const scale = new mapboxgl.ScaleControl({
* maxWidth: 80,
* unit: 'imperial'
* });
* map.addControl(scale);
*
* scale.setUnit('metric');
*/
class ScaleControl {
_map: Map;
_container: HTMLElement;
_language: ?string | ?string[];
_isNumberFormatSupported: boolean;
options: Options;
constructor(options: Options) {
this.options = extend({}, defaultOptions, options);
// Some old browsers (e.g., Safari < 14.1) don't support the "unit" style in NumberFormat.
// This is a workaround to display the scale without proper internationalization support.
this._isNumberFormatSupported = isNumberFormatSupported();
bindAll([
'_update',
'_setScale',
'setUnit'
], this);
}
getDefaultPosition(): ControlPosition {
return 'bottom-left';
}
_update() {
// A horizontal scale is imagined to be present at center of the map
// container with maximum length (Default) as 100px.
// Using spherical law of cosines approximation, the real distance is
// found between the two coordinates.
const maxWidth = this.options.maxWidth || 100;
const map = this._map;
const y = map._containerHeight / 2;
const x = (map._containerWidth / 2) - maxWidth / 2;
const left = map.unproject([x, y]);
const right = map.unproject([x + maxWidth, y]);
const maxMeters = left.distanceTo(right);
// The real distance corresponding to 100px scale length is rounded off to
// near pretty number and the scale length for the same is found out.
// Default unit of the scale is based on User's locale.
if (this.options.unit === 'imperial') {
const maxFeet = 3.2808 * maxMeters;
if (maxFeet > 5280) {
const maxMiles = maxFeet / 5280;
this._setScale(maxWidth, maxMiles, 'mile');
} else {
this._setScale(maxWidth, maxFeet, 'foot');
}
} else if (this.options.unit === 'nautical') {
const maxNauticals = maxMeters / 1852;
this._setScale(maxWidth, maxNauticals, 'nautical-mile');
} else if (maxMeters >= 1000) {
this._setScale(maxWidth, maxMeters / 1000, 'kilometer');
} else {
this._setScale(maxWidth, maxMeters, 'meter');
}
}
_setScale(maxWidth: number, maxDistance: number, unit: string) {
this._map._requestDomTask(() => {
const distance = getRoundNum(maxDistance);
const ratio = distance / maxDistance;
if (this._isNumberFormatSupported && unit !== 'nautical-mile') {
// $FlowFixMe[incompatible-call] — flow v0.190.1 doesn't support optional `locales` argument and `unit` style option
this._container.innerHTML = new Intl.NumberFormat(this._language, {style: 'unit', unitDisplay: 'short', unit}).format(distance);
} else {
this._container.innerHTML = `${distance} ${unitAbbr[unit]}`;
}
this._container.style.width = `${maxWidth * ratio}px`;
});
}
onAdd(map: Map): HTMLElement {
this._map = map;
this._language = map.getLanguage();
this._container = DOM.create('div', 'mapboxgl-ctrl mapboxgl-ctrl-scale', map.getContainer());
this._container.dir = 'auto';
// $FlowFixMe[method-unbinding]
this._map.on('move', this._update);
this._update();
return this._container;
}
onRemove() {
this._container.remove();
// $FlowFixMe[method-unbinding]
this._map.off('move', this._update);
this._map = (undefined: any);
}
_setLanguage(language: string) {
this._language = language;
this._update();
}
/**
* Set the scale's unit of the distance.
*
* @param {'imperial' | 'metric' | 'nautical'} unit Unit of the distance (`'imperial'`, `'metric'` or `'nautical'`).
*/
setUnit(unit: Unit) {
this.options.unit = unit;
this._update();
}
}
export default ScaleControl;
function isNumberFormatSupported() {
try {
// $FlowIgnore
new Intl.NumberFormat('en', {style: 'unit', unitDisplay: 'short', unit: 'meter'});
return true;
} catch (_) {
return false;
}
}
function getDecimalRoundNum(d: number) {
const multiplier = Math.pow(10, Math.ceil(-Math.log(d) / Math.LN10));
return Math.round(d * multiplier) / multiplier;
}
function getRoundNum(num: number) {
const pow10 = Math.pow(10, (`${Math.floor(num)}`).length - 1);
let d = num / pow10;
d = d >= 10 ? 10 :
d >= 5 ? 5 :
d >= 3 ? 3 :
d >= 2 ? 2 :
d >= 1 ? 1 : getDecimalRoundNum(d);
return pow10 * d;
}