/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module MapLayers
 */
import { assert } from "@itwin/core-bentley";
import { BackgroundMapProvider, BackgroundMapType } from "./BackgroundMapProvider";
;
/** Normalized representation of a [[MapSubLayerProps]] for which values
 * have been validated and default values have been applied where explicit values not defined.
 * A map sub layer represents a set of objects within the layer that can be controlled separately.  These
 * are produced only from map servers that produce images on demand and are not supported by tiled (cached) servers.
 * This class can represent a hierarchy, in this case a sub layer is visible only if all its ancestors are also visible.
 * @see [[MapLayerSettings]]
 * @public
 */
export class MapSubLayerSettings {
    /** Typically Name is a single word used for machine-to-machine communication while the Title is for the benefit of humans (WMS) */
    name;
    /** Title. */
    title;
    /** If true the sub layer is visible.  If part of a hierarchy, a sub layer is visible only if its ancestors are also visible. */
    visible;
    /** A unique string or number that may be used to identify the sub layer (ArcGIS) */
    id;
    /** One or more sublayer children */
    children;
    /** sublayer parent. */
    parent;
    constructor(name, title, visible, id, parent, children) {
        this.name = name;
        this.title = title;
        this.visible = visible !== undefined && visible;
        this.id = (id === undefined) ? this.name : id;
        this.parent = parent;
        this.children = children;
    }
    /** Construct from JSON, performing validation and applying default values for undefined fields. */
    static fromJSON(json) {
        return new MapSubLayerSettings(json.name, json.title, json.visible, (json.id === json.name) ? undefined : json.id, json.parent, json.children);
    }
    toJSON() {
        const props = { name: this.name, visible: this.visible };
        if (undefined !== this.id && this.id !== this.name)
            props.id = this.id;
        if (undefined !== this.title)
            props.title = this.title;
        if (this.children)
            props.children = [...this.children];
        if (undefined !== this.parent)
            props.parent = this.parent;
        return props;
    }
    /** Creating a copy of this MapSubLayer, optionally modifying some if its properties */
    clone(changedProps) {
        if (undefined === changedProps)
            return this;
        const props = {
            name: undefined !== changedProps.name ? changedProps.name : this.name,
            id: undefined !== changedProps.id ? changedProps.id : this.id,
            visible: undefined !== changedProps.visible ? changedProps.visible : this.visible,
            parent: undefined !== changedProps.parent ? changedProps.parent : this.parent,
            children: undefined !== changedProps.children ? changedProps.children.slice() : this.children?.slice(),
            title: undefined !== changedProps.title ? changedProps.title : this.title,
        };
        return MapSubLayerSettings.fromJSON(props);
    }
    /** @internal */
    displayMatches(other) {
        return this.name === other.name && this.visible === other.visible;
    }
    /** return true if this sublayer is named. */
    get isNamed() { return this.name.length > 0; }
    /** return true if this sublayer is a leaf (has no children) */
    get isLeaf() { return this.children === undefined || this.children.length === 0; }
    /** return true if this sublayer is an unnamed group */
    get isUnnamedGroup() { return !this.isLeaf && !this.isNamed; }
    /** return a string representing this sublayer id (converting to string if underlying id is number) */
    get idString() { return (typeof this.id === "number") ? this.id.toString(10) : this.id; }
}
/** The target onto which to drape a model map layer.
 * @beta
 */
export var ModelMapLayerDrapeTarget;
(function (ModelMapLayerDrapeTarget) {
    /** Drape only onto the globe. */
    ModelMapLayerDrapeTarget[ModelMapLayerDrapeTarget["Globe"] = 1] = "Globe";
    /** Drape only onto all attached reality data. */
    ModelMapLayerDrapeTarget[ModelMapLayerDrapeTarget["RealityData"] = 2] = "RealityData";
    /** Drape only onto all models within the iModel. */
    ModelMapLayerDrapeTarget[ModelMapLayerDrapeTarget["IModel"] = 4] = "IModel";
})(ModelMapLayerDrapeTarget || (ModelMapLayerDrapeTarget = {}));
/** Abstract base class for normalized representation of a [[MapLayerProps]] for which values have been validated and default values have been applied where explicit values not defined.
 * This class is extended by [[ImageMapLayerSettings]] and [ModelMapLayerSettings]] to create the settings for image and model based layers.
 * One or more map layers may be included within [[MapImagerySettings]] object.
 * @see [[MapImagerySettings]]
 * @public
 */
export class MapLayerSettings {
    visible;
    name;
    transparency;
    transparentBackground;
    /** @internal */
    constructor(name, visible = true, transparency = 0, transparentBackground = true) {
        this.name = name;
        this.visible = visible;
        this.transparentBackground = transparentBackground;
        this.transparency = transparency;
    }
    /** Create a map layer settings from its JSON representation. */
    static fromJSON(props) {
        return undefined !== props.modelId ? ModelMapLayerSettings.fromJSON(props) : ImageMapLayerSettings.fromJSON(props);
    }
    /** @internal */
    _toJSON() {
        const props = {
            name: this.name,
            visible: this.visible,
        };
        if (0 !== this.transparency)
            props.transparency = this.transparency;
        if (this.transparentBackground === false)
            props.transparentBackground = this.transparentBackground;
        return props;
    }
    /** @internal */
    cloneProps(changedProps) {
        return {
            name: undefined !== changedProps.name ? changedProps.name : this.name,
            visible: undefined !== changedProps.visible ? changedProps.visible : this.visible,
            transparency: undefined !== changedProps.transparency ? changedProps.transparency : this.transparency,
            transparentBackground: undefined !== changedProps.transparentBackground ? changedProps.transparentBackground : this.transparentBackground,
        };
    }
    /** @internal */
    displayMatches(other) {
        return this.name === other.name && this.visible === other.visible && this.transparency === other.transparency && this.transparentBackground === other.transparentBackground;
    }
    /** @internal */
    matchesNameAndSource(name, source) {
        return this.name === name && this.source === source;
    }
}
/** Normalized representation of a [[ImageMapLayerProps]] for which values have been validated and default values have been applied where explicit values not defined.
 * Image map layers are created from servers that produce images that represent map tiles.  Map layers map also be represented by models.
 * One or more map layers may be included within [[MapImagerySettings]] object.
 * @see [[MapImagerySettings]]
 * @see [[ModelMapLayerSettings]] for model based map layer settings.
 * @public
 */
export class ImageMapLayerSettings extends MapLayerSettings {
    formatId;
    url;
    userName;
    password;
    accessKey;
    /** List of query parameters to append to the settings URL and persisted as part of the JSON representation.
     * @note Sensitive information like user credentials should be provided in [[unsavedQueryParams]] to ensure it is never persisted.
     * @beta
    */
    savedQueryParams;
    /** List of query parameters that will get appended to the settings URL that should *not* be be persisted part of the JSON representation.
     * @beta
    */
    unsavedQueryParams;
    /** Properties specific to the map layer provider.
     * @beta
    */
    properties;
    subLayers;
    get source() { return this.url; }
    /** @internal */
    constructor(props) {
        const transparentBackground = props.transparentBackground ?? true;
        super(props.name, props.visible, props.transparency, transparentBackground);
        this.formatId = props.formatId;
        this.url = props.url;
        this.accessKey = props.accessKey;
        if (props.queryParams) {
            this.savedQueryParams = { ...props.queryParams };
        }
        if (props.properties) {
            this.properties = { ...props.properties };
        }
        this.subLayers = [];
        if (!props.subLayers)
            return;
        for (const subLayerProps of props.subLayers) {
            const subLayer = MapSubLayerSettings.fromJSON(subLayerProps);
            if (subLayer)
                this.subLayers.push(subLayer);
        }
    }
    static fromJSON(props) {
        return new this(props);
    }
    /** return JSON representation of this MapLayerSettings object */
    toJSON() {
        const props = super._toJSON();
        props.url = this.url;
        props.formatId = this.formatId;
        if (this.subLayers.length > 0)
            props.subLayers = this.subLayers.map((x) => x.toJSON());
        if (this.savedQueryParams)
            props.queryParams = { ...this.savedQueryParams };
        if (this.properties) {
            props.properties = structuredClone(this.properties);
        }
        return props;
    }
    /** Create a copy of this MapLayerSettings, optionally modifying some of its properties.
     * @param changedProps JSON representation of the properties to change.
     * @returns A MapLayerSettings with all of its properties set to match those of `this`, except those explicitly defined in `changedProps`.
     */
    clone(changedProps) {
        const clone = ImageMapLayerSettings.fromJSON(this.cloneProps(changedProps));
        // Clone members not part of MapLayerProps
        clone.userName = this.userName;
        clone.password = this.password;
        if (this.unsavedQueryParams)
            clone.unsavedQueryParams = { ...this.unsavedQueryParams };
        if (this.savedQueryParams)
            clone.savedQueryParams = { ...this.savedQueryParams };
        return clone;
    }
    /** @internal */
    cloneProps(changedProps) {
        const props = super.cloneProps(changedProps);
        props.formatId = changedProps.formatId ?? this.formatId;
        props.url = changedProps.url ?? this.url;
        props.accessKey = changedProps.accessKey ?? this.accessKey;
        props.subLayers = changedProps.subLayers ?? this.subLayers;
        if (changedProps.queryParams) {
            props.queryParams = { ...changedProps.queryParams };
        }
        else if (this.savedQueryParams) {
            props.queryParams = { ...this.savedQueryParams };
        }
        if (changedProps.properties) {
            props.properties = { ...changedProps.properties };
        }
        else if (this.properties) {
            props.properties = { ...this.properties };
        }
        return props;
    }
    /** @internal */
    displayMatches(other) {
        if (!(other instanceof ImageMapLayerSettings) || !super.displayMatches(other))
            return false;
        if (this.userName !== other.userName || this.password !== other.password || this.subLayers.length !== other.subLayers.length) {
            return false;
        }
        for (let i = 0; i < this.subLayers.length; i++)
            if (!this.subLayers[i].displayMatches(other.subLayers[i]))
                return false;
        return true;
    }
    /** Return a sublayer matching id -- or undefined if not found */
    subLayerById(id) {
        return id === undefined ? undefined : this.subLayers.find((subLayer) => subLayer.id === id);
    }
    hasInvisibleAncestors(subLayer) {
        if (!subLayer || !subLayer.parent)
            return false;
        const parent = this.subLayerById(subLayer.parent);
        if (!parent)
            return false;
        // Visibility of named group has no impact on the visibility of children (only unnamed group does)
        // i.e For WMS, its should be possible to request a child layer when its parent is not visible (if the parent is also named)
        return (!parent.visible && !parent.isNamed) || this.hasInvisibleAncestors(parent);
    }
    /** Return true if sublayer is visible -- testing ancestors for visibility if they exist. */
    isSubLayerVisible(subLayer) {
        if (!subLayer.visible)
            return false;
        return !this.hasInvisibleAncestors(subLayer);
    }
    /** Return true if all sublayers are invisible. */
    get allSubLayersInvisible() {
        if (this.subLayers.length === 0)
            return false;
        return this.subLayers.every((subLayer) => (subLayer.isUnnamedGroup || !this.isSubLayerVisible(subLayer)));
    }
    /** Return the children for a sublayer */
    getSubLayerChildren(subLayer) {
        if (!subLayer.children)
            return undefined;
        const children = new Array();
        subLayer.children.forEach((childId) => {
            const child = this.subLayerById(childId);
            if (child !== undefined)
                children.push(child);
        });
        return children;
    }
    /** @internal */
    static mapTypeName(type) {
        switch (type) {
            case BackgroundMapType.Aerial:
                return "Aerial Imagery";
            default:
            case BackgroundMapType.Hybrid:
                return "Aerial Imagery with labels";
            case BackgroundMapType.Street:
                return "Streets";
        }
    }
    setCredentials(userName, password) {
        this.userName = userName;
        this.password = password;
    }
    /** Collect all query parameters
   * @beta
   */
    collectQueryParams() {
        let queryParams = {};
        if (this.savedQueryParams)
            queryParams = { ...this.savedQueryParams };
        if (this.unsavedQueryParams)
            queryParams = { ...queryParams, ...this.unsavedQueryParams };
        return queryParams;
    }
}
/** Normalized representation of a [[ModelMapLayerProps]] for which values have been validated and default values have been applied where explicit values not defined.
 * Model map layers are produced from models, typically from two dimensional geometry that may originate in a GIS system.
 * One or more map layers may be included within [[MapImagerySettings]] object.
 * @see [[MapImagerySettings]]
 * @see [[ImageMapLayerSettings]] for image based map layer settings.
 * @public
 */
export class ModelMapLayerSettings extends MapLayerSettings {
    /** Specifies the target onto which to drape this model map layer. Defaults to [ModelMapLayerDrapeTarget.Globe]($common).
     * @beta
     */
    drapeTarget;
    modelId;
    get source() { return this.modelId; }
    /** @internal */
    constructor(modelId, name, visible = true, transparency = 0, transparentBackground = true, drapeTarget = ModelMapLayerDrapeTarget.Globe) {
        super(name, visible, transparency, transparentBackground);
        this.modelId = modelId;
        this.drapeTarget = drapeTarget;
    }
    /** Construct from JSON, performing validation and applying default values for undefined fields. */
    static fromJSON(json) {
        const transparentBackground = (json.transparentBackground === undefined) ? true : json.transparentBackground;
        return new this(json.modelId, json.name, json.visible, json.transparency, transparentBackground, json.drapeTarget);
    }
    /** return JSON representation of this MapLayerSettings object */
    toJSON() {
        const props = super._toJSON();
        props.modelId = this.modelId;
        if (this.drapeTarget !== ModelMapLayerDrapeTarget.Globe)
            props.drapeTarget = this.drapeTarget;
        return props;
    }
    /** Create a copy of this MapLayerSettings, optionally modifying some of its properties.
     * @param changedProps JSON representation of the properties to change.
     * @returns A MapLayerSettings with all of its properties set to match those of `this`, except those explicitly defined in `changedProps`.
     */
    clone(changedProps) {
        return ModelMapLayerSettings.fromJSON(this.cloneProps(changedProps));
    }
    /** @internal */
    cloneProps(changedProps) {
        const props = super.cloneProps(changedProps);
        props.modelId = changedProps.modelId ?? this.modelId;
        props.drapeTarget = changedProps.drapeTarget ?? this.drapeTarget;
        return props;
    }
    /** @internal */
    displayMatches(other) {
        if (!(other instanceof ModelMapLayerSettings) || !super.displayMatches(other))
            return false;
        return this.modelId === other.modelId;
    }
    /** Return true if all sublayers are invisible (always false as model layers do not include sublayers). */
    get allSubLayersInvisible() {
        return false;
    }
}
/** A [[ImageMapLayerSettings]] that can serve as the base layer for a [[MapImagerySettings]].
 * The base layer supports all of the same options as any other layer, but also allows for simplified configuration based
 * on a small set of known supported [[BackgroundMapProvider]]s like [Bing Maps](https://www.microsoft.com/en-us/maps).
 * If the base layer was configured from such a provider, that information will be preserved and can be queried; this allows
 * the imagery provider and/or type to be easily modified.
 * @see [[MapImagerySettings.backgroundBase]].
 * @public
 */
export class BaseMapLayerSettings extends ImageMapLayerSettings {
    _provider;
    /** The provider from which this base layer was configured, if any. */
    get provider() { return this._provider; }
    /** Create a base layer from its JSON representation.
     * TODO: This, MapLayerSettings.fromJSON, and MapSubLayerSettings.fromJSON should never return undefined.
     * That means they should not accept undefined for props and should define props such that it fully describes the
     * layer - e.g., url and name must be defined.
     */
    static fromJSON(props) {
        const settings = super.fromJSON(props);
        assert(settings instanceof BaseMapLayerSettings);
        if (props.provider)
            settings._provider = BackgroundMapProvider.fromJSON(props.provider);
        return settings;
    }
    /** Convert this layer to its JSON representation. */
    toJSON() {
        const props = super.toJSON();
        if (this.provider)
            props.provider = this.provider.toJSON();
        return props;
    }
    /** @internal */
    cloneProps(changedProps) {
        const props = super.cloneProps(changedProps);
        if (changedProps.provider)
            props.provider = changedProps.provider;
        else if (this.provider)
            props.provider = this.provider.toJSON();
        return props;
    }
    /** Create a copy of this layer. */
    clone(changedProps) {
        const prevUrl = this.url;
        const clone = BaseMapLayerSettings.fromJSON(this.cloneProps(changedProps));
        if (this.provider && prevUrl !== this.url)
            clone._provider = undefined;
        return clone;
    }
    /** Create a base layer from a BackgroundMapProvider. */
    static fromProvider(provider, options) {
        let formatId, url, name;
        switch (provider.name) {
            case "BingProvider":
            default:
                formatId = "BingMaps";
                let imagerySet;
                switch (provider.type) {
                    case BackgroundMapType.Street:
                        imagerySet = "Road";
                        break;
                    case BackgroundMapType.Aerial:
                        imagerySet = "Aerial";
                        break;
                    case BackgroundMapType.Hybrid:
                    default:
                        imagerySet = "AerialWithLabels";
                        break;
                }
                name = `Bing Maps: ${ImageMapLayerSettings.mapTypeName(provider.type)}`;
                url = `https://dev.virtualearth.net/REST/v1/Imagery/Metadata/${imagerySet}?o=json&incl=ImageryProviders&key={bingKey}`;
                break;
            case "MapBoxProvider":
                formatId = "MapboxImagery";
                name = `MapBox: ${ImageMapLayerSettings.mapTypeName(provider.type)}`;
                switch (provider.type) {
                    case BackgroundMapType.Street:
                        url = "https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/";
                        break;
                    case BackgroundMapType.Aerial:
                        url = "https://api.mapbox.com/styles/v1/mapbox/satellite-v9/tiles/";
                        break;
                    case BackgroundMapType.Hybrid:
                        url = "https://api.mapbox.com/styles/v1/mapbox/satellite-streets-v11/tiles/";
                        break;
                }
                break;
        }
        const settings = super.fromJSON({
            name,
            formatId,
            url,
            transparentBackground: false,
            visible: !options?.invisible,
            transparency: options?.transparency,
        });
        assert(undefined !== settings);
        assert(settings instanceof BaseMapLayerSettings);
        settings._provider = provider;
        return settings;
    }
    /** @internal */
    static fromBackgroundMapProps(props) {
        return this.fromProvider(BackgroundMapProvider.fromBackgroundMapProps(props));
    }
    /** @alpha */
    cloneWithProvider(provider) {
        return BaseMapLayerSettings.fromProvider(provider, { invisible: !this.visible, transparency: this.transparency });
    }
}
//# sourceMappingURL=MapLayerSettings.js.map