Source: src/component/FeatureRenderer.js

/* Copyright (c) 2015-2017 The Open Source Geospatial Foundation
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/**
 * A component that renders a `ol.style.Style` with an optional `ol.Feature`.
 *
 *     @example preview
 *     var poly = Ext.create('GeoExt.component.FeatureRenderer', {
 *         symbolizers: new ol.style.Style({
 *             fill: new ol.style.Fill({color: 'red'})
 *         })
 *     });
 *     var line = Ext.create('GeoExt.component.FeatureRenderer', {
 *         symbolizers: new ol.style.Style({
 *             stroke: new ol.style.Stroke({color: 'orange', width: 3}),
 *         }),
 *         symbolType: 'Line'
 *     });
 *     var point = Ext.create('GeoExt.component.FeatureRenderer', {
 *         symbolizers: new ol.style.Style({
 *             image: new ol.style.Circle({
 *                 radius: 7,
 *                 fill: new ol.style.Fill({color: 'gray'}),
 *                 stroke: new ol.style.Stroke({color: 'black', width: 3}),
 *             })
 *         }),
 *         symbolType: 'Point'
 *     });
 *     var star = Ext.create('GeoExt.component.FeatureRenderer', {
 *         symbolizers: new ol.style.Style({
 *             image: new ol.style.RegularShape({
 *                 fill: new ol.style.Fill({color: 'blue'}),
 *                 stroke: new ol.style.Stroke({color: 'green', width: 3}),
 *                 points: 7,
 *                 radius: 15,
 *                 radius2: 7,
 *                 angle: 0
 *             })
 *         }),
 *         minWidth: 40,
 *         minHeight: 40,
 *         symbolType: 'Point'
 *     });
 *     Ext.create('Ext.panel.Panel', {
 *         title: 'Rendering of ol.Features in a panel',
 *         items: [poly, line, point, star],
 *         border: false,
 *         renderTo: Ext.getBody()
 *     });
 *
 * @class GeoExt.component.FeatureRenderer
 */
Ext.define('GeoExt.component.FeatureRenderer', {
  extend: 'Ext.Component',
  alias: 'widget.gx_renderer',
  requires: ['GeoExt.util.Version'],
  mixins: ['GeoExt.mixin.SymbolCheck'],

  // <debug>
  symbols: [
    'ol.extent.getCenter',
    'ol.extent.getWidth',
    'ol.extent.getHeight',
    'ol.Feature',
    'ol.Feature#getGeometry',
    'ol.Feature#setStyle',
    'ol.geom.Geometry#getExtent',
    'ol.geom.Point',
    'ol.geom.LineString',
    'ol.geom.Polygon',
    'ol.layer.Vector',
    'ol.layer.Vector#getSource',
    'ol.Map#getSize',
    'ol.Map#getView',
    'ol.Map#setView',
    'ol.Map#updateSize',
    'ol.proj.Projection',
    'ol.source.Vector',
    'ol.source.Vector#addFeature',
    'ol.View',
    'ol.View#fit',
  ],
  // </debug>

  /**
   * Fires when the feature is clicked on.
   *
   * @event click
   * @param {GeoExt.component.FeatureRenderer} renderer The feature renderer.
   */

  config: {
    /**
     * Optional class to set on the feature renderer div.
     *
     * @cfg {string}
     */
    imgCls: '',

    /**
     * The minimum width.
     *
     * @cfg {number}
     */
    minWidth: 20,

    /**
     * The minimum height.
     *
     * @cfg {number}
     */
    minHeight: 20,

    /**
     * The resolution for the renderer.
     *
     * @cfg {number}
     */
    resolution: 1,

    /**
     * Optional vector to be drawn.
     *
     * @cfg {ol.Feature}
     */
    feature: undefined,

    /**
     * Feature to use for point swatches. Optional.
     *
     * @cfg {ol.Feature}
     */
    pointFeature: undefined,

    /**
     * Feature to use for line swatches. Optional.
     *
     * @cfg {ol.Feature}
     */
    lineFeature: undefined,

    /**
     * Feature to use for polygon swatches. Optional.
     *
     * @cfg {ol.Feature}
     */
    polygonFeature: undefined,

    /**
     * Feature to use for text label swatches. Optional.
     *
     * @cfg {ol.Feature}
     */
    textFeature: undefined,

    /**
     * An `ol.style.Style` instance or an array of `ol.style.Style`
     * instances for rendering a  feature.  If no symbolizers are
     * provided, the default style from OpenLayers will be used.
     *
     * @cfg {Array<ol.style.Style> | ol.style.Style}
     */
    symbolizers: undefined,

    /**
     * One of `"Point"`, `"Line"`, `"Polygon"` or `"Text"`.  Only relevant
     * if `feature` is not provided.
     *
     * @cfg {string}
     */
    symbolType: 'Polygon',
  },

  inheritableStatics: {
    /**
     * Determines the style for the given feature record.
     *
     * @param {GeoExt.data.model.Feature} record A feature record to get the
     *     styler for.
     * @return {Array<ol.style.Style> | ol.style.Style} The style(s) applied to the
     *     given feature record.
     */
    determineStyle: function (record) {
      const feature = record.getFeature();
      return (
        feature.getStyle() ||
        feature.getStyleFunction() ||
        (record.store ? record.store.layer.getStyle() : null)
      );
    },
  },

  /**
   * Initialize the GeoExt.component.FeatureRenderer.
   */
  initComponent: function () {
    const me = this;
    const id = me.getId();
    me.autoEl = {
      id: id,
      tag: 'div',
      class: this.getImgCls(),
    };
    if (!me.getLineFeature()) {
      me.setLineFeature(
        new ol.Feature({
          geometry: new ol.geom.LineString([
            [-8, -3],
            [-3, 3],
            [3, -3],
            [8, 3],
          ]),
        }),
      );
    }
    if (!me.getPointFeature()) {
      me.setPointFeature(
        new ol.Feature({
          geometry: new ol.geom.Point([0, 0]),
        }),
      );
    }
    if (!me.getPolygonFeature()) {
      me.setPolygonFeature(
        new ol.Feature({
          geometry: new ol.geom.Polygon([
            [
              [-8, -4],
              [-6, -6],
              [6, -6],
              [8, -4],
              [8, 4],
              [6, 6],
              [-6, 6],
              [-8, 4],
            ],
          ]),
        }),
      );
    }
    if (!me.getTextFeature()) {
      me.setTextFeature(
        new ol.Feature({
          geometry: new ol.geom.Point([0, 0]),
        }),
      );
    }
    me.map = new ol.Map({
      controls: [],
      interactions: [],
      layers: [
        new ol.layer.Vector({
          source: new ol.source.Vector(),
        }),
      ],
    });
    const feature = me.getFeature();
    if (!feature) {
      me.setFeature(me['get' + me.getSymbolType() + 'Feature']());
    } else {
      me.applyFeature(feature);
    }
    me.callParent();
  },
  /**
   * Draw the feature when we are rendered.
   *
   * @private
   */
  onRender: function () {
    this.callParent(arguments);
    this.drawFeature();
  },
  /**
   * After rendering we setup our own custom events using #initCustomEvents.
   *
   * @private
   */
  afterRender: function () {
    this.callParent(arguments);
    this.initCustomEvents();
  },
  /**
   * (Re-)Initializes our custom event listeners, mainly #onClick.
   *
   * @private
   */
  initCustomEvents: function () {
    const me = this;
    me.clearCustomEvents();
    me.el.on('click', me.onClick, me);
  },
  /**
   * Unbinds previously bound listeners on #el.
   *
   * @private
   */
  clearCustomEvents: function () {
    const el = this.el;
    if (el && el.clearListeners) {
      el.clearListeners();
    }
  },
  /**
   * Bound to the click event on the #el, this fires the click event.
   *
   * @private
   */
  onClick: function () {
    this.fireEvent('click', this);
  },
  /**
   * Private method called during the destroy sequence.
   *
   * @private
   */
  beforeDestroy: function () {
    const me = this;
    me.clearCustomEvents();
    if (me.map) {
      me.map.setTarget(null);
    }
  },
  /**
   * When resizing has happened, we might need to re-set the renderer's
   * dimensions via #setRendererDimensions.
   *
   * @private
   */
  onResize: function () {
    this.setRendererDimensions();
    this.callParent(arguments);
  },
  /**
   * Draw the feature in the map.
   *
   * @private
   */
  drawFeature: function () {
    const me = this;
    me.map.setTarget(me.el.id); // TODO why not me.el?
    me.setRendererDimensions();
  },
  /**
   * Set the dimension of our renderer, i.e. map and view.
   *
   * @private
   */
  setRendererDimensions: function () {
    const me = this;
    const gb = me.feature.getGeometry().getExtent();
    const gw = ol.extent.getWidth(gb);
    const gh = ol.extent.getHeight(gb);
    /*
     * Determine resolution based on the following rules:
     * 1) always use value specified in config
     * 2) if not specified, use max res based on width or height of element
     * 3) if no width or height, assume a resolution of 1
     */
    let resolution = me.initialConfig.resolution;
    if (!resolution) {
      resolution = Math.max(gw / me.width || 0, gh / me.height || 0) || 1;
    }
    me.map.setView(
      new ol.View({
        minResolution: resolution,
        maxResolution: resolution,
        projection: new ol.proj.Projection({
          code: '',
          units: 'pixels',
        }),
      }),
    );
    // determine height and width of element
    const width = Math.max(me.width || me.getMinWidth(), gw / resolution);
    const height = Math.max(me.height || me.getMinHeight(), gh / resolution);
    // determine bounds of renderer
    const center = ol.extent.getCenter(gb);
    const bhalfw = (width * resolution) / 2;
    const bhalfh = (height * resolution) / 2;
    const bounds = [
      center[0] - bhalfw,
      center[1] - bhalfh,
      center[0] + bhalfw,
      center[1] + bhalfh,
    ];
    me.el.setSize(Math.round(width), Math.round(height));
    me.map.updateSize();
    // Check for backwards compatibility
    if (GeoExt.util.Version.isOl3()) {
      me.map.getView().fit(bounds, me.map.getSize());
    } else {
      me.map.getView().fit(bounds);
    }
  },
  /**
   * We're setting the symbolizers on the feature.
   *
   * @param {Array<ol.style.Style> | ol.style.Style} symbolizers The style (or
   *     array of styles) that have been set.
   * @return {Array<ol.style.Style> | ol.style.Style} The style (or
   *     array of styles) that have been set.
   * @private
   */
  applySymbolizers: function (symbolizers) {
    const feature = this.getFeature();
    if (feature && symbolizers) {
      feature.setStyle(symbolizers);
    }
    return symbolizers;
  },

  /**
   * We're setting the feature and add it to the source.
   *
   * @param {ol.Feature} feature The feature that has been set.
   * @return {ol.Feature} feature The feature that has been set.
   * @private
   */
  applyFeature: function (feature) {
    const symbolizers = this.getSymbolizers();
    if (feature && symbolizers) {
      feature.setStyle(symbolizers);
    }
    if (this.map) {
      const source = this.map.getLayers().item(0).getSource();
      source.clear();
      source.addFeature(feature);
    }
    return feature;
  },

  /**
   * Update the `feature` or `symbolizers` and redraw the feature.
   *
   * Valid options:
   *
   * @param {Object} options Object with properties to be updated.
   * @param {ol.Feature} options.feature The new or updated feature.
   * @param {Array<ol.style.Style> | ol.style.Style} options.symbolizers The
   *     symbolizers.
   */
  update: function (options) {
    if (options.feature) {
      this.setFeature(options.feature);
    }
    if (options.symbolizers) {
      this.setSymbolizers(options.symbolizers);
    }
  },
});