All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.control.ScaleLine.js Maven / Gradle / Ivy

The newest version!
/**
 * @module ol/control/ScaleLine
 */
import Control from './Control.js';
import {CLASS_UNSELECTABLE} from '../css.js';
import {METERS_PER_UNIT, getPointResolution} from '../proj.js';

/**
 * @type {string}
 */
const UNITS_PROP = 'units';

/**
 * @typedef {'degrees' | 'imperial' | 'nautical' | 'metric' | 'us'} Units
 * Units for the scale line.
 */

/**
 * @const
 * @type {Array}
 */
const LEADING_DIGITS = [1, 2, 5];

/**
 * @const
 * @type {number}
 */
const DEFAULT_DPI = 25.4 / 0.28;

/***
 * @template Return
 * @typedef {import("../Observable").OnSignature &
 *   import("../Observable").OnSignature &
 *   import("../Observable").CombinedOnSignature} ScaleLineOnSignature
 */

/**
 * @typedef {Object} Options
 * @property {string} [className] CSS class name. The default is `ol-scale-bar` when configured with
 * `bar: true`. Otherwise the default is `ol-scale-line`.
 * @property {number} [minWidth=64] Minimum width in pixels at the OGC default dpi. The width will be
 * adjusted to match the dpi used.
 * @property {number} [maxWidth] Maximum width in pixels at the OGC default dpi. The width will be
 * adjusted to match the dpi used.
 * @property {function(import("../MapEvent.js").default):void} [render] Function called when the control
 * should be re-rendered. This is called in a `requestAnimationFrame` callback.
 * @property {HTMLElement|string} [target] Specify a target if you want the control
 * to be rendered outside of the map's viewport.
 * @property {Units} [units='metric'] Units.
 * @property {boolean} [bar=false] Render scalebars instead of a line.
 * @property {number} [steps=4] Number of steps the scalebar should use. Use even numbers
 * for best results. Only applies when `bar` is `true`.
 * @property {boolean} [text=false] Render the text scale above of the scalebar. Only applies
 * when `bar` is `true`.
 * @property {number|undefined} [dpi=undefined] dpi of output device such as printer. Only applies
 * when `bar` is `true`. If undefined the OGC default screen pixel size of 0.28mm will be assumed.
 */

/**
 * @classdesc
 * A control displaying rough y-axis distances, calculated for the center of the
 * viewport. For conformal projections (e.g. EPSG:3857, the default view
 * projection in OpenLayers), the scale is valid for all directions.
 * No scale line will be shown when the y-axis distance of a pixel at the
 * viewport center cannot be calculated in the view projection.
 * By default the scale line will show in the bottom left portion of the map,
 * but this can be changed by using the css selector `.ol-scale-line`.
 * When specifying `bar` as `true`, a scalebar will be rendered instead
 * of a scaleline.
 * For cartesian measurements of the scaleline, you need to set the
 * `getPointResolution` method of your projection to simply return the input
 * value, e.g. `projection.setGetPointResolution(r => r);`
 *
 * @api
 */
class ScaleLine extends Control {
  /**
   * @param {Options} [options] Scale line options.
   */
  constructor(options) {
    options = options ? options : {};

    const element = document.createElement('div');
    element.style.pointerEvents = 'none';

    super({
      element: element,
      render: options.render,
      target: options.target,
    });

    /***
     * @type {ScaleLineOnSignature}
     */
    this.on;

    /***
     * @type {ScaleLineOnSignature}
     */
    this.once;

    /***
     * @type {ScaleLineOnSignature}
     */
    this.un;

    const className =
      options.className !== undefined
        ? options.className
        : options.bar
          ? 'ol-scale-bar'
          : 'ol-scale-line';

    /**
     * @private
     * @type {HTMLElement}
     */
    this.innerElement_ = document.createElement('div');
    this.innerElement_.className = className + '-inner';

    this.element.className = className + ' ' + CLASS_UNSELECTABLE;
    this.element.appendChild(this.innerElement_);

    /**
     * @private
     * @type {?import("../View.js").State}
     */
    this.viewState_ = null;

    /**
     * @private
     * @type {number}
     */
    this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64;

    /**
     * @private
     * @type {number|undefined}
     */
    this.maxWidth_ = options.maxWidth;

    /**
     * @private
     * @type {boolean}
     */
    this.renderedVisible_ = false;

    /**
     * @private
     * @type {number|undefined}
     */
    this.renderedWidth_ = undefined;

    /**
     * @private
     * @type {string}
     */
    this.renderedHTML_ = '';

    this.addChangeListener(UNITS_PROP, this.handleUnitsChanged_);

    this.setUnits(options.units || 'metric');

    /**
     * @private
     * @type {boolean}
     */
    this.scaleBar_ = options.bar || false;

    /**
     * @private
     * @type {number}
     */
    this.scaleBarSteps_ = options.steps || 4;

    /**
     * @private
     * @type {boolean}
     */
    this.scaleBarText_ = options.text || false;

    /**
     * @private
     * @type {number|undefined}
     */
    this.dpi_ = options.dpi || undefined;
  }

  /**
   * Return the units to use in the scale line.
   * @return {Units} The units
   * to use in the scale line.
   * @observable
   * @api
   */
  getUnits() {
    return this.get(UNITS_PROP);
  }

  /**
   * @private
   */
  handleUnitsChanged_() {
    this.updateElement_();
  }

  /**
   * Set the units to use in the scale line.
   * @param {Units} units The units to use in the scale line.
   * @observable
   * @api
   */
  setUnits(units) {
    this.set(UNITS_PROP, units);
  }

  /**
   * Specify the dpi of output device such as printer.
   * @param {number|undefined} dpi The dpi of output device.
   * @api
   */
  setDpi(dpi) {
    this.dpi_ = dpi;
  }

  /**
   * @private
   */
  updateElement_() {
    const viewState = this.viewState_;

    if (!viewState) {
      if (this.renderedVisible_) {
        this.element.style.display = 'none';
        this.renderedVisible_ = false;
      }
      return;
    }

    const center = viewState.center;
    const projection = viewState.projection;
    const units = this.getUnits();
    const pointResolutionUnits = units == 'degrees' ? 'degrees' : 'm';
    let pointResolution = getPointResolution(
      projection,
      viewState.resolution,
      center,
      pointResolutionUnits,
    );

    const minWidth =
      (this.minWidth_ * (this.dpi_ || DEFAULT_DPI)) / DEFAULT_DPI;

    const maxWidth =
      this.maxWidth_ !== undefined
        ? (this.maxWidth_ * (this.dpi_ || DEFAULT_DPI)) / DEFAULT_DPI
        : undefined;

    let nominalCount = minWidth * pointResolution;
    let suffix = '';
    if (units == 'degrees') {
      const metersPerDegree = METERS_PER_UNIT.degrees;
      nominalCount *= metersPerDegree;
      if (nominalCount < metersPerDegree / 60) {
        suffix = '\u2033'; // seconds
        pointResolution *= 3600;
      } else if (nominalCount < metersPerDegree) {
        suffix = '\u2032'; // minutes
        pointResolution *= 60;
      } else {
        suffix = '\u00b0'; // degrees
      }
    } else if (units == 'imperial') {
      if (nominalCount < 0.9144) {
        suffix = 'in';
        pointResolution /= 0.0254;
      } else if (nominalCount < 1609.344) {
        suffix = 'ft';
        pointResolution /= 0.3048;
      } else {
        suffix = 'mi';
        pointResolution /= 1609.344;
      }
    } else if (units == 'nautical') {
      pointResolution /= 1852;
      suffix = 'NM';
    } else if (units == 'metric') {
      if (nominalCount < 1e-6) {
        suffix = 'nm';
        pointResolution *= 1e9;
      } else if (nominalCount < 0.001) {
        suffix = 'μm';
        pointResolution *= 1000000;
      } else if (nominalCount < 1) {
        suffix = 'mm';
        pointResolution *= 1000;
      } else if (nominalCount < 1000) {
        suffix = 'm';
      } else {
        suffix = 'km';
        pointResolution /= 1000;
      }
    } else if (units == 'us') {
      if (nominalCount < 0.9144) {
        suffix = 'in';
        pointResolution *= 39.37;
      } else if (nominalCount < 1609.344) {
        suffix = 'ft';
        pointResolution /= 0.30480061;
      } else {
        suffix = 'mi';
        pointResolution /= 1609.3472;
      }
    } else {
      throw new Error('Invalid units');
    }

    let i = 3 * Math.floor(Math.log(minWidth * pointResolution) / Math.log(10));
    let count, width, decimalCount;
    let previousCount, previousWidth, previousDecimalCount;
    while (true) {
      decimalCount = Math.floor(i / 3);
      const decimal = Math.pow(10, decimalCount);
      count = LEADING_DIGITS[((i % 3) + 3) % 3] * decimal;
      width = Math.round(count / pointResolution);
      if (isNaN(width)) {
        this.element.style.display = 'none';
        this.renderedVisible_ = false;
        return;
      }
      if (maxWidth !== undefined && width >= maxWidth) {
        count = previousCount;
        width = previousWidth;
        decimalCount = previousDecimalCount;
        break;
      } else if (width >= minWidth) {
        break;
      }
      previousCount = count;
      previousWidth = width;
      previousDecimalCount = decimalCount;
      ++i;
    }
    const html = this.scaleBar_
      ? this.createScaleBar(width, count, suffix)
      : count.toFixed(decimalCount < 0 ? -decimalCount : 0) + ' ' + suffix;

    if (this.renderedHTML_ != html) {
      this.innerElement_.innerHTML = html;
      this.renderedHTML_ = html;
    }

    if (this.renderedWidth_ != width) {
      this.innerElement_.style.width = width + 'px';
      this.renderedWidth_ = width;
    }

    if (!this.renderedVisible_) {
      this.element.style.display = '';
      this.renderedVisible_ = true;
    }
  }

  /**
   * @private
   * @param {number} width The current width of the scalebar.
   * @param {number} scale The current scale.
   * @param {string} suffix The suffix to append to the scale text.
   * @return {string} The stringified HTML of the scalebar.
   */
  createScaleBar(width, scale, suffix) {
    const resolutionScale = this.getScaleForResolution();
    const mapScale =
      resolutionScale < 1
        ? Math.round(1 / resolutionScale).toLocaleString() + ' : 1'
        : '1 : ' + Math.round(resolutionScale).toLocaleString();
    const steps = this.scaleBarSteps_;
    const stepWidth = width / steps;
    const scaleSteps = [this.createMarker('absolute')];
    for (let i = 0; i < steps; ++i) {
      const cls =
        i % 2 === 0 ? 'ol-scale-singlebar-odd' : 'ol-scale-singlebar-even';
      scaleSteps.push(
        '
' + '
' + '
' + this.createMarker('relative') + // render text every second step, except when only 2 steps (i % 2 === 0 || steps === 2 ? this.createStepText(i, width, false, scale, suffix) : '') + '
', ); } // render text at the end scaleSteps.push(this.createStepText(steps, width, true, scale, suffix)); const scaleBarText = this.scaleBarText_ ? `
` + mapScale + '
' : ''; return scaleBarText + scaleSteps.join(''); } /** * Creates a marker at given position * @param {'absolute'|'relative'} position The position, absolute or relative * @return {string} The stringified div containing the marker */ createMarker(position) { const top = position === 'absolute' ? 3 : -10; return ( '
' ); } /** * Creates the label for a marker marker at given position * @param {number} i The iterator * @param {number} width The width the scalebar will currently use * @param {boolean} isLast Flag indicating if we add the last step text * @param {number} scale The current scale for the whole scalebar * @param {string} suffix The suffix for the scale * @return {string} The stringified div containing the step text */ createStepText(i, width, isLast, scale, suffix) { const length = i === 0 ? 0 : Math.round((scale / this.scaleBarSteps_) * i * 100) / 100; const lengthString = length + (i === 0 ? '' : ' ' + suffix); const margin = i === 0 ? -3 : (width / this.scaleBarSteps_) * -1; const minWidth = i === 0 ? 0 : (width / this.scaleBarSteps_) * 2; return ( '
' + lengthString + '
' ); } /** * Returns the appropriate scale for the given resolution and units. * @return {number} The appropriate scale. */ getScaleForResolution() { const resolution = getPointResolution( this.viewState_.projection, this.viewState_.resolution, this.viewState_.center, 'm', ); const dpi = this.dpi_ || DEFAULT_DPI; const inchesPerMeter = 1000 / 25.4; return resolution * inchesPerMeter * dpi; } /** * Update the scale line element. * @param {import("../MapEvent.js").default} mapEvent Map event. * @override */ render(mapEvent) { const frameState = mapEvent.frameState; if (!frameState) { this.viewState_ = null; } else { this.viewState_ = frameState.viewState; } this.updateElement_(); } } export default ScaleLine;




© 2015 - 2024 Weber Informatics LLC | Privacy Policy