/* *
 *
 *  (c) 2010-2026 Highsoft AS
 *  Author: Torstein Hønsi
 *
 *  A commercial license may be required depending on use.
 *  See www.highcharts.com/license
 *
 *
 * */
'use strict';
import D from '../Core/Defaults.js';
const { setOptions } = D;
import H from '../Core/Globals.js';
const { composed } = H;
import MapNavigationDefaults from './MapNavigationDefaults.js';
import MapPointer from './MapPointer.js';
import MapSymbols from './MapSymbols.js';
import { extend, objectEach, merge, pick, pushUnique, addEvent } from '../Shared/Utilities.js';
/* *
 *
 *  Functions
 *
 * */
/** @internal */
function stopEvent(e) {
    if (e) {
        e.preventDefault?.();
        e.stopPropagation?.();
        e.cancelBubble = true;
    }
}
/* *
 *
 *  Class
 *
 * */
/**
 * The MapNavigation handles buttons for navigation in addition to mousewheel
 * and doubleclick handlers for chart zooming.
 *
 * @internal
 * @class
 * @name MapNavigation
 *
 * @param {Highcharts.Chart} chart
 *        The Chart instance.
 */
class MapNavigation {
    /* *
     *
     *  Static Functions
     *
     * */
    static compose(MapChartClass, PointerClass, SVGRendererClass) {
        MapPointer.compose(PointerClass);
        MapSymbols.compose(SVGRendererClass);
        if (pushUnique(composed, 'Map.Navigation')) {
            // Extend the Chart.render method to add zooming and panning
            addEvent(MapChartClass, 'beforeRender', function () {
                // Render the plus and minus buttons. Doing this before the
                // shapes makes getBBox much quicker, at least in Chrome.
                this.mapNavigation = new MapNavigation(this);
                this.mapNavigation.update();
            });
            setOptions(MapNavigationDefaults);
        }
    }
    /* *
     *
     *  Constructor
     *
     * */
    constructor(chart) {
        this.chart = chart;
        this.navButtons = [];
    }
    /* *
     *
     *  Functions
     *
     * */
    /**
     * Update the map navigation with new options. Calling this is the same as
     * calling `chart.update({ mapNavigation: {} })`.
     *
     * @function MapNavigation#update
     *
     * @param {Partial<Highcharts.MapNavigationOptions>} [options]
     *        New options for the map navigation.
     */
    update(options) {
        const mapNav = this, chart = mapNav.chart, navButtons = mapNav.navButtons, outerHandler = function (e) {
            this.handler.call(chart, e);
            stopEvent(e); // Stop default click event (#4444)
        };
        let navOptions = chart.options.mapNavigation;
        // Merge in new options in case of update, and register back to chart
        // options.
        if (options) {
            navOptions = chart.options.mapNavigation =
                merge(chart.options.mapNavigation, options);
        }
        // Destroy buttons in case of dynamic update
        while (navButtons.length) {
            navButtons.pop()?.destroy();
        }
        if (!chart.renderer.forExport &&
            pick(navOptions.enableButtons, navOptions.enabled)) {
            if (!mapNav.navButtonsGroup) {
                mapNav.navButtonsGroup = chart.renderer.g()
                    .attr({
                    zIndex: 7 // #4955, #8392, #20476
                })
                    .add();
            }
            objectEach(navOptions.buttons, (buttonOptions, n) => {
                buttonOptions = merge(navOptions.buttonOptions, buttonOptions);
                const attr = {
                    padding: buttonOptions.padding
                };
                // Presentational
                if (!chart.styledMode && buttonOptions.theme) {
                    extend(attr, buttonOptions.theme);
                    attr.style = merge(buttonOptions.theme.style, buttonOptions.style // #3203
                    );
                }
                const { text, width = 0, height = 0, padding = 0 } = buttonOptions;
                const button = chart.renderer
                    .button(
                // Display the text from options only if it is not plus
                // or minus
                (text !== '+' && text !== '-' && text) || '', 0, 0, outerHandler, attr, void 0, void 0, void 0, n === 'zoomIn' ? 'topbutton' : 'bottombutton')
                    .addClass('highcharts-map-navigation highcharts-' + {
                    zoomIn: 'zoom-in',
                    zoomOut: 'zoom-out'
                }[n])
                    .attr({
                    width,
                    height,
                    title: chart.options.lang[n],
                    zIndex: 5
                })
                    .add(mapNav.navButtonsGroup);
                // Add SVG paths for the default symbols, because the text
                // representation of + and - is not sharp and position is not
                // easy to control.
                if (text === '+' || text === '-') {
                    // Mysterious +1 to achieve centering
                    const w = width + 1, d = [
                        ['M', padding + 3, padding + height / 2],
                        ['L', padding + w - 3, padding + height / 2]
                    ];
                    if (text === '+') {
                        d.push(['M', padding + w / 2, padding + 3], ['L', padding + w / 2, padding + height - 3]);
                    }
                    chart.renderer
                        .path(d)
                        .addClass('highcharts-button-symbol')
                        .attr(chart.styledMode ? {} : {
                        stroke: buttonOptions.style?.color,
                        'stroke-width': 3,
                        'stroke-linecap': 'round'
                    })
                        .add(button);
                }
                button.handler = buttonOptions.onclick;
                // Stop double click event (#4444)
                addEvent(button.element, 'dblclick', stopEvent);
                navButtons.push(button);
                extend(buttonOptions, {
                    width: button.width,
                    height: 2 * (button.height || 0)
                });
                if (!chart.hasLoaded) {
                    // Align it after the plotBox is known (#12776)
                    const unbind = addEvent(chart, 'load', () => {
                        // #15406: Make sure button hasn't been destroyed
                        if (button.element) {
                            button.align(buttonOptions, false, buttonOptions.alignTo);
                        }
                        unbind();
                    });
                }
                else {
                    button.align(buttonOptions, false, buttonOptions.alignTo);
                }
            });
            // Borrowed from overlapping-datalabels. Consider a shared module.
            const isIntersectRect = (box1, box2) => !(box2.x >= box1.x + box1.width ||
                box2.x + box2.width <= box1.x ||
                box2.y >= box1.y + box1.height ||
                box2.y + box2.height <= box1.y);
            // Check the mapNavigation buttons collision with exporting button
            // and translate the mapNavigation button if they overlap.
            const adjustMapNavBtn = function () {
                const expBtnBBox = chart.exporting?.group?.getBBox();
                if (expBtnBBox) {
                    const navBtnsBBox = mapNav.navButtonsGroup.getBBox();
                    // If buttons overlap
                    if (isIntersectRect(expBtnBBox, navBtnsBBox)) {
                        // Adjust the mapNav buttons' position by translating
                        // them above or below the exporting button
                        const aboveExpBtn = -navBtnsBBox.y -
                            navBtnsBBox.height + expBtnBBox.y - 5, belowExpBtn = expBtnBBox.y + expBtnBBox.height -
                            navBtnsBBox.y + 5, mapNavVerticalAlign = (navOptions.buttonOptions &&
                            navOptions.buttonOptions.verticalAlign);
                        // If bottom aligned and adjusting the mapNav button
                        // would translate it out of the plotBox, translate it
                        // up instead of down
                        mapNav.navButtonsGroup.attr({
                            translateY: mapNavVerticalAlign === 'bottom' ?
                                aboveExpBtn :
                                belowExpBtn
                        });
                    }
                }
            };
            if (!chart.hasLoaded) {
                // Align it after the plotBox is known (#12776) and after the
                // hamburger button's position is known so they don't overlap
                // (#15782)
                addEvent(chart, 'render', adjustMapNavBtn);
            }
        }
        this.updateEvents(navOptions);
    }
    /**
     * Update events, called internally from the update function. Add new event
     * handlers, or unbinds events if disabled.
     *
     * @function MapNavigation#updateEvents
     *
     * @param {Partial<Highcharts.MapNavigationOptions>} options
     *        Options for map navigation.
     */
    updateEvents(options) {
        const chart = this.chart;
        // Add the double click event
        if (pick(options.enableDoubleClickZoom, options.enabled) ||
            options.enableDoubleClickZoomTo) {
            this.unbindDblClick = this.unbindDblClick || addEvent(chart.container, 'dblclick', function (e) {
                chart.pointer.onContainerDblClick(e);
            });
        }
        else if (this.unbindDblClick) {
            // Unbind and set unbinder to undefined
            this.unbindDblClick = this.unbindDblClick();
        }
        // Add the mousewheel event
        if (pick(options.enableMouseWheelZoom, options.enabled)) {
            this.unbindMouseWheel = this.unbindMouseWheel || addEvent(chart.container, 'wheel', function (e) {
                // Prevent scrolling when the pointer is over the element
                // with that class, for example annotation popup #12100.
                if (!chart.pointer.inClass(e.target, 'highcharts-no-mousewheel')) {
                    const initialZoom = chart.mapView?.zoom;
                    chart.pointer.onContainerMouseWheel(e);
                    // If the zoom level changed, prevent the default action
                    // which is to scroll the page
                    if (initialZoom !== chart.mapView?.zoom) {
                        stopEvent(e);
                    }
                }
                return false;
            });
        }
        else if (this.unbindMouseWheel) {
            // Unbind and set unbinder to undefined
            this.unbindMouseWheel = this.unbindMouseWheel();
        }
    }
}
/* *
 *
 *  Default Export
 *
 * */
/** @internal */
export default MapNavigation;
