/**
 * Copyright 2012 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
/**
 * @fileoverview Extends OverlayView to provide a canvas "Layer".
 * @author Brendan Kenny
 */

/**
 * Modification Notice 
 * Author: WP Go Maps Development Team (CodeCabin)
 * Date: 2024-03-13
 * 
 * Details:
 * We have altered the way the canvas extension happens, allowing it to be delayed in the event that the Google Maps API is loaded in async mode
 * this is done in order to account for the load delays when loading sub-modules from Google API's, which may cause some of this module
 * to fail when loaded into the DOM
 * 
 * The solution, for the interim is to attempt the extension, and if google is not yet defined, delay this by 500ms
 * before retrying the extension. The extension will be retried a total of 10 times at most, before it fails in it's entirety
 * 
 * No notice will be provided in this case, but a flag will be set on the CanvasLayer definition itself
 */

/**
 * A map layer that provides a canvas over the slippy map and a callback
 * system for efficient animation. Requires canvas and CSS 2D transform
 * support.
 * @constructor
 * @extends google.maps.OverlayView
 * @param {CanvasLayerOptions=} opt_options Options to set in this CanvasLayer.
 */
function CanvasLayer(opt_options) {
  /**
   * If true, canvas is in a map pane and the OverlayView is fully functional.
   * See google.maps.OverlayView.onAdd for more information.
   * @type {boolean}
   * @private
   */
  this.isAdded_ = false;

  /**
   * If true, each update will immediately schedule the next.
   * @type {boolean}
   * @private
   */
  this.isAnimated_ = false;

  /**
   * The name of the MapPane in which this layer will be displayed.
   * @type {string}
   * @private
   */
  this.paneName_ = CanvasLayer.DEFAULT_PANE_NAME_;

  /**
   * A user-supplied function called whenever an update is required. Null or
   * undefined if a callback is not provided.
   * @type {?function=}
   * @private
   */
  this.updateHandler_ = null;

  /**
   * A user-supplied function called whenever an update is required and the
   * map has been resized since the last update. Null or undefined if a
   * callback is not provided.
   * @type {?function}
   * @private
   */
  this.resizeHandler_ = null;

  /**
   * The LatLng coordinate of the top left of the current view of the map. Will
   * be null when this.isAdded_ is false.
   * @type {google.maps.LatLng}
   * @private
   */
  this.topLeft_ = null;

  /**
   * The map-pan event listener. Will be null when this.isAdded_ is false. Will
   * be null when this.isAdded_ is false.
   * @type {?function}
   * @private
   */
  this.centerListener_ = null;

  /**
   * The map-resize event listener. Will be null when this.isAdded_ is false.
   * @type {?function}
   * @private
   */
  this.resizeListener_ = null;

  /**
   * If true, the map size has changed and this.resizeHandler_ must be called
   * on the next update.
   * @type {boolean}
   * @private
   */
  this.needsResize_ = true;

  /**
   * A browser-defined id for the currently requested callback. Null when no
   * callback is queued.
   * @type {?number}
   * @private
   */
  this.requestAnimationFrameId_ = null;

  var canvas = document.createElement('canvas');
  canvas.style.position = 'absolute';
  canvas.style.top = 0;
  canvas.style.left = 0;
  canvas.style.pointerEvents = 'none';

  /**
   * The canvas element.
   * @type {!HTMLCanvasElement}
   */
  this.canvas = canvas;

  /**
   * The CSS width of the canvas, which may be different than the width of the
   * backing store.
   * @private {number}
   */
  this.canvasCssWidth_ = 300;

  /**
   * The CSS height of the canvas, which may be different than the height of
   * the backing store.
   * @private {number}
   */
  this.canvasCssHeight_ = 150;

  /**
   * A value for scaling the CanvasLayer resolution relative to the CanvasLayer
   * display size.
   * @private {number}
   */
  this.resolutionScale_ = 1;

  /**
   * Simple bind for functions with no args for bind-less browsers (Safari).
   * @param {Object} thisArg The this value used for the target function.
   * @param {function} func The function to be bound.
   */
  function simpleBindShim(thisArg, func) {
    return function() { func.apply(thisArg); };
  }

  /**
   * A reference to this.repositionCanvas_ with this bound as its this value.
   * @type {function}
   * @private
   */
  this.repositionFunction_ = simpleBindShim(this, this.repositionCanvas_);

  /**
   * A reference to this.resize_ with this bound as its this value.
   * @type {function}
   * @private
   */
  this.resizeFunction_ = simpleBindShim(this, this.resize_);

  /**
   * A reference to this.update_ with this bound as its this value.
   * @type {function}
   * @private
   */
  this.requestUpdateFunction_ = simpleBindShim(this, this.update_);

  // set provided options, if any
  if (opt_options) {
    this.setOptions(opt_options);
  }
}

/**
 * A very simple way to track the extension status
 */
CanvasLayer.__EXTENSION_STATUS = {
  attempts : 0,
  max : 10,
  delay : 500,
  extended : false
};

/**
 * A somewhat recursive prototype extension module developed to allow async loading within the Google Maps API
 * 
 * Since the original module was written some technical specifications for the Google Maps platform have changed,
 * so by accounting for this in an admittedly rudimentary way, we can allow async loading, without changing the surround code loading order  
*/
function __canvasLayerPrototypeExtension(){
  CanvasLayer.__EXTENSION_STATUS.attempts += 1;

  if(typeof window.google !== 'undefined' && typeof window.google.maps !== 'undefined' && typeof window.google.maps.OverlayView !== 'undefined'){
    /* Perform the extension, this code is unchanged from the original, and simply moved into this callable */
    CanvasLayer.__EXTENSION_STATUS.extended = true;

    if(window.google)
      CanvasLayer.prototype = new google.maps.OverlayView();

    /**
     * The default MapPane to contain the canvas.
     * @type {string}
     * @const
     * @private
     */
    CanvasLayer.DEFAULT_PANE_NAME_ = 'overlayLayer';

    /**
     * Transform CSS property name, with vendor prefix if required. If browser
     * does not support transforms, property will be ignored.
     * @type {string}
     * @const
     * @private
     */
    CanvasLayer.CSS_TRANSFORM_ = (function() {
      var div = document.createElement('div');
      var transformProps = [
        'transform',
        'WebkitTransform',
        'MozTransform',
        'OTransform',
        'msTransform'
      ];
      for (var i = 0; i < transformProps.length; i++) {
        var prop = transformProps[i];
        if (div.style[prop] !== undefined) {
          return prop;
        }
      }

      // return unprefixed version by default
      return transformProps[0];
    })();

    /**
     * The requestAnimationFrame function, with vendor-prefixed or setTimeout-based
     * fallbacks. MUST be called with window as thisArg.
     * @type {function}
     * @param {function} callback The function to add to the frame request queue.
     * @return {number} The browser-defined id for the requested callback.
     * @private
     */
    CanvasLayer.prototype.requestAnimFrame_ =
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function(callback) {
          return window.setTimeout(callback, 1000 / 60);
        };

    /**
     * The cancelAnimationFrame function, with vendor-prefixed fallback. Does not
     * fall back to clearTimeout as some platforms implement requestAnimationFrame
     * but not cancelAnimationFrame, and the cost is an extra frame on onRemove.
     * MUST be called with window as thisArg.
     * @type {function}
     * @param {number=} requestId The id of the frame request to cancel.
     * @private
     */
    CanvasLayer.prototype.cancelAnimFrame_ =
        window.cancelAnimationFrame ||
        window.webkitCancelAnimationFrame ||
        window.mozCancelAnimationFrame ||
        window.oCancelAnimationFrame ||
        window.msCancelAnimationFrame ||
        function(requestId) {};

    /**
     * Sets any options provided. See CanvasLayerOptions for more information.
     * @param {CanvasLayerOptions} options The options to set.
     */
    CanvasLayer.prototype.setOptions = function(options) {
      if (options.animate !== undefined) {
        this.setAnimate(options.animate);
      }

      if (options.paneName !== undefined) {
        this.setPaneName(options.paneName);
      }

      if (options.updateHandler !== undefined) {
        this.setUpdateHandler(options.updateHandler);
      }

      if (options.resizeHandler !== undefined) {
        this.setResizeHandler(options.resizeHandler);
      }

      if (options.resolutionScale !== undefined) {
        this.setResolutionScale(options.resolutionScale);
      }

      if (options.map !== undefined) {
        this.setMap(options.map);
      }
    };

    /**
     * Set the animated state of the layer. If true, updateHandler will be called
     * repeatedly, once per frame. If false, updateHandler will only be called when
     * a map property changes that could require the canvas content to be redrawn.
     * @param {boolean} animate Whether the canvas is animated.
     */
    CanvasLayer.prototype.setAnimate = function(animate) {
      this.isAnimated_ = !!animate;

      if (this.isAnimated_) {
        this.scheduleUpdate();
      }
    };

    /**
     * @return {boolean} Whether the canvas is animated.
     */
    CanvasLayer.prototype.isAnimated = function() {
      return this.isAnimated_;
    };

    /**
     * Set the MapPane in which this layer will be displayed, by name. See
     * {@code google.maps.MapPanes} for the panes available.
     * @param {string} paneName The name of the desired MapPane.
     */
    CanvasLayer.prototype.setPaneName = function(paneName) {
      this.paneName_ = paneName;

      this.setPane_();
    };

    /**
     * @return {string} The name of the current container pane.
     */
    CanvasLayer.prototype.getPaneName = function() {
      return this.paneName_;
    };

    /**
     * Adds the canvas to the specified container pane. Since this is guaranteed to
     * execute only after onAdd is called, this is when paneName's existence is
     * checked (and an error is thrown if it doesn't exist).
     * @private
     */
    CanvasLayer.prototype.setPane_ = function() {
      if (!this.isAdded_) {
        return;
      }

      // onAdd has been called, so panes can be used
      var panes = this.getPanes();
      if (!panes[this.paneName_]) {
        throw new Error('"' + this.paneName_ + '" is not a valid MapPane name.');
      }

      panes[this.paneName_].appendChild(this.canvas);
    };

    /**
     * Set a function that will be called whenever the parent map and the overlay's
     * canvas have been resized. If opt_resizeHandler is null or unspecified, any
     * existing callback is removed.
     * @param {?function=} opt_resizeHandler The resize callback function.
     */
    CanvasLayer.prototype.setResizeHandler = function(opt_resizeHandler) {
      this.resizeHandler_ = opt_resizeHandler;
    };

    /**
     * Sets a value for scaling the canvas resolution relative to the canvas
     * display size. This can be used to save computation by scaling the backing
     * buffer down, or to support high DPI devices by scaling it up (by e.g.
     * window.devicePixelRatio).
     * @param {number} scale
     */
    CanvasLayer.prototype.setResolutionScale = function(scale) {
      if (typeof scale === 'number') {
        this.resolutionScale_ = scale;
        this.resize_();
      }
    };

    /**
     * Set a function that will be called when a repaint of the canvas is required.
     * If opt_updateHandler is null or unspecified, any existing callback is
     * removed.
     * @param {?function=} opt_updateHandler The update callback function.
     */
    CanvasLayer.prototype.setUpdateHandler = function(opt_updateHandler) {
      this.updateHandler_ = opt_updateHandler;
    };

    /**
     * @inheritDoc
     */
    CanvasLayer.prototype.onAdd = function() {
      if (this.isAdded_) {
        return;
      }

      this.isAdded_ = true;
      this.setPane_();

      this.resizeListener_ = google.maps.event.addListener(this.getMap(),
          'resize', this.resizeFunction_);
      this.centerListener_ = google.maps.event.addListener(this.getMap(),
          'center_changed', this.repositionFunction_);

      this.resize_();
      this.repositionCanvas_();
    };

    /**
     * @inheritDoc
     */
    CanvasLayer.prototype.onRemove = function() {
      if (!this.isAdded_) {
        return;
      }

      this.isAdded_ = false;
      this.topLeft_ = null;

      // remove canvas and listeners for pan and resize from map
      this.canvas.parentElement.removeChild(this.canvas);
      if (this.centerListener_) {
        google.maps.event.removeListener(this.centerListener_);
        this.centerListener_ = null;
      }
      if (this.resizeListener_) {
        google.maps.event.removeListener(this.resizeListener_);
        this.resizeListener_ = null;
      }

      // cease canvas update callbacks
      if (this.requestAnimationFrameId_) {
        this.cancelAnimFrame_.call(window, this.requestAnimationFrameId_);
        this.requestAnimationFrameId_ = null;
      }
    };

    /**
     * The internal callback for resize events that resizes the canvas to keep the
     * map properly covered.
     * @private
     */
    CanvasLayer.prototype.resize_ = function() {
      if (!this.isAdded_) {
        return;
      }

      var map = this.getMap();
      var mapWidth = map.getDiv().getElementsByTagName('div')[0].offsetWidth;
      var mapHeight = map.getDiv().getElementsByTagName('div')[0].offsetHeight;

      var newWidth = mapWidth * this.resolutionScale_;
      var newHeight = mapHeight * this.resolutionScale_;
      var oldWidth = this.canvas.width;
      var oldHeight = this.canvas.height;

      // resizing may allocate a new back buffer, so do so conservatively
      if (oldWidth !== newWidth || oldHeight !== newHeight) {
        this.canvas.width = newWidth;
        this.canvas.height = newHeight;

        this.needsResize_ = true;
        this.scheduleUpdate();
      }

      // reset styling if new sizes don't match; resize of data not needed
      if (this.canvasCssWidth_ !== mapWidth ||
          this.canvasCssHeight_ !== mapHeight) {
        this.canvasCssWidth_ = mapWidth;
        this.canvasCssHeight_ = mapHeight;
        this.canvas.style.width = mapWidth + 'px';
        this.canvas.style.height = mapHeight + 'px';
      }
    };

    /**
     * @inheritDoc
     */
    CanvasLayer.prototype.draw = function() {
      this.repositionCanvas_();
    };

    /**
     * Internal callback for map view changes. Since the Maps API moves the overlay
     * along with the map, this function calculates the opposite translation to
     * keep the canvas in place.
     * @private
     */
    CanvasLayer.prototype.repositionCanvas_ = function() {
      // TODO(bckenny): *should* only be executed on RAF, but in current browsers
      //     this causes noticeable hitches in map and overlay relative
      //     positioning.

      var map = this.getMap();

      // topLeft can't be calculated from map.getBounds(), because bounds are
      // clamped to -180 and 180 when completely zoomed out. Instead, calculate
      // left as an offset from the center, which is an unwrapped LatLng.
      var top = map.getBounds().getNorthEast().lat();
      var center = map.getCenter();
      var scale = Math.pow(2, map.getZoom());
      var left = center.lng() - (this.canvasCssWidth_ * 180) / (256 * scale);
      this.topLeft_ = new google.maps.LatLng(top, left);

      // Canvas position relative to draggable map's container depends on
      // overlayView's projection, not the map's. Have to use the center of the
      // map for this, not the top left, for the same reason as above.
      var projection = this.getProjection();
      var divCenter = projection.fromLatLngToDivPixel(center);
      var offsetX = -Math.round(this.canvasCssWidth_ / 2 - divCenter.x);
      var offsetY = -Math.round(this.canvasCssHeight_ / 2 - divCenter.y);
      this.canvas.style[CanvasLayer.CSS_TRANSFORM_] = 'translate(' +
          offsetX + 'px,' + offsetY + 'px)';

      this.scheduleUpdate();
    };

    /**
     * Internal callback that serves as main animation scheduler via
     * requestAnimationFrame. Calls resize and update callbacks if set, and
     * schedules the next frame if overlay is animated.
     * @private
     */
    CanvasLayer.prototype.update_ = function() {
      this.requestAnimationFrameId_ = null;

      if (!this.isAdded_) {
        return;
      }

      if (this.isAnimated_) {
        this.scheduleUpdate();
      }

      if (this.needsResize_ && this.resizeHandler_) {
        this.needsResize_ = false;
        this.resizeHandler_();
      }

      if (this.updateHandler_) {
        this.updateHandler_();
      }
    };

    /**
     * A convenience method to get the current LatLng coordinate of the top left of
     * the current view of the map.
     * @return {google.maps.LatLng} The top left coordinate.
     */
    CanvasLayer.prototype.getTopLeft = function() {
      return this.topLeft_;
    };

    /**
     * Schedule a requestAnimationFrame callback to updateHandler. If one is
     * already scheduled, there is no effect.
     */
    CanvasLayer.prototype.scheduleUpdate = function() {
      if (this.isAdded_ && !this.requestAnimationFrameId_) {
        this.requestAnimationFrameId_ =
            this.requestAnimFrame_.call(window, this.requestUpdateFunction_);
      }
    };

  } else {
    /* Queue this for 500ms from now to allow us to retry until we get it loaded */
    if(CanvasLayer.__EXTENSION_STATUS.attempts < CanvasLayer.__EXTENSION_STATUS.max){
      setTimeout(() => {
        __canvasLayerPrototypeExtension();
      }, CanvasLayer.__EXTENSION_STATUS.delay);
    }
  }
}

// Start the loader
__canvasLayerPrototypeExtension();

