UNPKG

@progress/kendo-angular-map

Version:
695 lines (694 loc) 25.1 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Input, NgZone, Output, Renderer2, ViewChild } from '@angular/core'; import { hasObservers, isDocumentAvailable } from '@progress/kendo-angular-common'; import { L10N_PREFIX, LocalizationService } from '@progress/kendo-angular-l10n'; import { IconsService } from '@progress/kendo-angular-icons'; import { mapMarkerTargetIcon, mapMarkerIcon, plusIcon, minusIcon, caretAltUpIcon, caretAltDownIcon, caretAltLeftIcon, caretAltRightIcon } from '@progress/kendo-svg-icons'; import { Map } from '@progress/kendo-charts'; import { validatePackage } from '@progress/kendo-licensing'; import { ResizeSensorComponent } from '@progress/kendo-angular-common'; import { combineLatest } from 'rxjs'; import { tap } from 'rxjs/operators'; import { ConfigurationService } from './common/configuration.service'; import { copyChanges } from './common/copy-changes'; import { hasParent } from './common/has-parent'; import { MapInstanceObserver } from './common/map-instance-observer'; import { InstanceEventService } from './events/instance-event.service'; import { packageMetadata } from './package-metadata'; import { TooltipPopupComponent } from './tooltip/tooltip-popup.component'; import { TooltipTemplateService } from './tooltip/tooltip-template.service'; import * as i0 from "@angular/core"; import * as i1 from "./common/configuration.service"; import * as i2 from "./events/instance-event.service"; import * as i3 from "@progress/kendo-angular-l10n"; import * as i4 from "@progress/kendo-angular-icons"; // Static SVG Icons used by the Map const svgIcons = { mapMarkerTargetIcon, mapMarkerIcon, plusIcon, minusIcon, caretAltUpIcon, caretAltDownIcon, caretAltLeftIcon, caretAltRightIcon }; /** * Represents the [Kendo UI Map component for Angular]({% slug overview_map %}). * * Use this component to display interactive maps with markers, layers, and controls. * Configure zoom levels, center coordinates, and user interactions like panning and zooming. * * @example * ```ts * import { Component } from '@angular/core'; * * @Component({ * selector: 'my-app', * template: ` * <kendo-map [center]="center" [zoom]="15"> * <kendo-map-layers> * <kendo-map-tile-layer * [subdomains]="tileSubdomains" * [urlTemplate]="tileUrl" * attribution="&copy; <a href='https://osm.org/copyright'>OpenStreetMap contributors</a>" * > * </kendo-map-tile-layer> * <kendo-map-marker-layer * [data]="markers" * locationField="latlng" * titleField="name" * > * </kendo-map-marker-layer> * </kendo-map-layers> * </kendo-map> * ` * }) * class AppComponent { * tileSubdomains = ["a", "b", "c"]; * tileUrl = (e: TileUrlTemplateArgs): string => * `https://${e.subdomain}.tile.openstreetmap.org/${e.zoom}/${e.x}/${e.y}.png`; * * center = [30.2675, -97.7409]; * markers = [{ latlng: [30.2675, -97.7409], name: "Zevo Toys" }]; * } * ``` * * @remarks * Supported children components are: {@link LayersComponent}. */ export class MapComponent { configurationService; instanceEventService; element; localizationService; changeDetector; ngZone; renderer; iconsService; /** * Limits the automatic resizing of the Map. Sets the maximum number of times per second * that the component redraws its content when the size of its container changes. * To disable the automatic resizing, set it to `0`. * * @default 10 * * @example * ```ts * @Component({ * selector: 'my-app', * template: ` * <kendo-map [resizeRateLimit]="2"> * </kendo-map> * ` * }) * export class AppComponent { * } * ``` */ resizeRateLimit = 10; /** * Specifies the map center coordinates. * Provide coordinates as `[Latitude, Longitude]`. */ center; /** * Specifies the configuration for built-in map controls. */ controls; /** * The minimum zoom level. Typical web maps use zoom levels from 0 (whole world) to 19 (sub-meter features). * * @default 1 */ minZoom; /** * The maximum zoom level. Typical web maps use zoom levels from 0 (whole world) to 19 (sub-meter features). * * @default 19 */ maxZoom; /** * The size of the map in pixels at zoom level 0. * * @default 256 */ minSize; /** * Controls whether the user can pan the map. * * @default true */ pannable; /** * The settings for the tooltip popup. */ popupSettings; /** * Specifies whether the map should wrap around the east-west edges. * * @default true */ wraparound; /** * Specifies the initial zoom level. * Use values from 0 (whole world) to 19 (sub-meter features). * The map size derives from the zoom level and `minScale` options: `size = (2 ^ zoom) * minSize`. * Map zoom rounds floating point numbers to use whole [`zoom levels`](https://wiki.openstreetmap.org/wiki/Zoom_levels) 0 through 19. * * @default 3 */ zoom = 3; /** * Determines whether users can change the map zoom level. * * @default true */ zoomable; /** * Fires immediately before the map resets. This event is typically used for cleanup by layer implementers. */ beforeReset = new EventEmitter(); /** * Fires when the user clicks on the map. */ mapClick = new EventEmitter(); /** * Fires when a marker appears on the map and its DOM element becomes available. */ markerActivate = new EventEmitter(); /** * Fires when the user clicks or taps a marker. */ markerClick = new EventEmitter(); /** * Fires once the map has created a marker, and just before the map displays it. * * Cancelling the event prevents displaying the marker. */ markerCreated = new EventEmitter(); /** * Fires after the map viewport completes panning. */ panEnd = new EventEmitter(); /** * Fires while the map viewport is being moved. */ pan = new EventEmitter(); /** * Fires when the map resets. * * This typically occurs on initial load and after a zoom/center change. */ reset = new EventEmitter(); /** * Fires when a shape is clicked or tapped. */ shapeClick = new EventEmitter(); /** * Fires when a shape is created, but is not rendered yet. */ shapeCreated = new EventEmitter(); /** * Fires when a [GeoJSON Feature](https://geojson.org/geojson-spec.html#feature-objects) is created on a shape layer. */ shapeFeatureCreated = new EventEmitter(); /** * Fires when the mouse enters a shape. * * > This event will fire reliably only for shapes that have set fill color. * > The opacity can still be set to 0 so the shapes appear to have no fill. */ shapeMouseEnter = new EventEmitter(); /** * Fires when the mouse leaves a shape. * * > This event will fire reliably only for shapes that have set fill color. * > The opacity can still be set to 0 so the shapes appear to have no fill. */ shapeMouseLeave = new EventEmitter(); /** * Fires when the map zoom level is about to change. * * Cancelling the event will prevent the user action. */ zoomStart = new EventEmitter(); /** * Fires when the map zoom level changes. */ zoomEnd = new EventEmitter(); /** * Fires when the map center has been changed. */ centerChange = new EventEmitter(); /** * Fires when the map zoom level has been changed. */ zoomChange = new EventEmitter(); tooltipInstance; instance; initResizeSensor = false; options; theme = null; resizeTimeout; redrawTimeout; destroyed; rtl = false; subscriptions; optionsChange; domSubscriptions; iconSettings; constructor(configurationService, instanceEventService, element, localizationService, changeDetector, ngZone, renderer, iconsService) { this.configurationService = configurationService; this.instanceEventService = instanceEventService; this.element = element; this.localizationService = localizationService; this.changeDetector = changeDetector; this.ngZone = ngZone; this.renderer = renderer; this.iconsService = iconsService; validatePackage(packageMetadata); } ngAfterViewInit() { if (this.canRender) { this.ngZone.runOutsideAngular(() => { const mapMouseleave = this.renderer.listen(this.element.nativeElement, 'mouseleave', this.mapMouseleave.bind(this)); this.domSubscriptions = () => { mapMouseleave(); }; }); } this.setDirection(); this.initConfig(); this.subscriptions = this.localizationService.changes.subscribe(() => this.setDirection()); } ngAfterViewChecked() { if (this.instance && this.autoResize) { this.ngZone.runOutsideAngular(() => { clearTimeout(this.resizeTimeout); this.resizeTimeout = setTimeout(() => { this.resize(); }, 0); }); } } ngOnChanges(changes) { if (this.instance) { if (changes['zoom']) { this.instance.zoom(changes['zoom'].currentValue); delete changes['zoom']; } if (changes['center']) { this.instance.center(changes['center'].currentValue); delete changes['center']; } if (Object.keys(changes).length === 0) { return; } } const store = this.configurationService.store; copyChanges(changes, store); store.popupSettings = null; this.configurationService.push(store); } ngOnDestroy() { this.destroyed = true; if (this.optionsChange) { this.optionsChange.unsubscribe(); } if (this.domSubscriptions) { this.domSubscriptions(); this.domSubscriptions = null; } if (this.instance) { this.instance.destroy(); this.instance = null; } if (this.subscriptions) { this.subscriptions.unsubscribe(); } } /** * Gets the marker layers instances. */ get layers() { return this.instance?.layers; } /** * Gets the extent (visible area) of the map. */ get extent() { return this.instance?.extent(); } /** * Sets the extent (visible area) of the map. */ set extent(extent) { this.instance?.extent(extent); } /** * Detects the container size and redraws the Map. * Resizing happens automatically unless you set `resizeRateLimit` to `0`. */ resize() { //this.instance?.resize(); } /** * Gets the size of the visible map area. * * @returns {Object} The width and height of the visible map area. */ viewSize() { return this.instance?.viewSize(); } /** * Gets event coordinates relative to the map element. * Offset coordinates do not sync to a specific map location. * * @param {any} e The mouse event. * @returns {geometry.Point} The event coordinates relative to the map element. */ eventOffset(e) { return this.instance?.eventOffset(e); } /** * Gets projected layer coordinates for this mouse event. * Layer coordinates are absolute and change only when zoom level changes. * * @param {any} e The mouse event. * @returns {geometry.Point} The projected layer coordinates for this event. */ eventToLayer(e) { return this.instance?.eventToLayer(e); } /** * Gets the geographic location for this mouse event. * * @param {any} e The mouse event. * @returns {geometry.Point} The geographic location for this mouse event. */ eventToLocation(e) { return this.instance?.eventToLocation(e); } /** * Gets relative view coordinates for this mouse event. * Layer elements positioned on these coordinates appear under the mouse cursor. * View coordinates become invalid after a map reset. * * @param {any} e The mouse event. * @returns {geometry.Point} The relative view coordinates for this mouse event. */ eventToView(e) { return this.instance?.eventToView(e); } /** * Converts layer coordinates to geographic location. * * @param {geometry.Point | number[]} point The layer coordinates. Arrays use x, y order. * @param {number} zoom Optional. Zoom level to use. Defaults to current zoom level. * @returns {Location} The geographic location for the layer coordinates. */ layerToLocation(point, zoom) { return this.instance?.layerToLocation(point, zoom); } /** * Gets layer coordinates for a geographic location. * * @param {Location | number[]} location The geographic location. Arrays use [Latitude, Longitude] order. * @param {number} zoom Optional. Zoom level to use. Defaults to current zoom level. * @returns {geometry.Point} The layer coordinates. */ locationToLayer(location, zoom) { return this.instance?.locationToLayer(location, zoom); } /** * Gets view coordinates for a geographic location. * * @param {Location | number[]} location The geographic location. Arrays use [Latitude, Longitude] order. * @returns {geometry.Point} The view coordinates for the geographic location. */ locationToView(location) { return this.instance?.locationToView(location); } /** * Gets the geographic location for view coordinates. * * @param {geometry.Point | number[]} point The view coordinates. Arrays use x, y order. * @param {number} zoom Optional. Zoom level to use. Defaults to current zoom level. * @returns {Location} The geographic location for the view coordinates. */ viewToLocation(point, zoom) { return this.instance?.viewToLocation(point, zoom); } /** * @hidden */ onResize() { if (this.autoResize) { this.resize(); } } /** * @hidden */ get canRender() { return isDocumentAvailable() && Boolean(this.element); } get autoResize() { return this.resizeRateLimit > 0; } init() { if (!this.canRender) { return; } const element = this.element.nativeElement; this.renderer.setStyle(element, 'display', 'block'); const instanceObserver = new MapInstanceObserver(this); this.createInstance(element, instanceObserver); } initConfig() { if (!isDocumentAvailable()) { return; } this.ngZone.runOutsideAngular(() => { this.optionsChange = combineLatest([this.configurationService.changes, this.iconsService.changes]) .pipe(tap(([options, iconSettings]) => { this.options = { ...options, icons: { ...iconSettings, svgIcons } }; })) .subscribe(() => { if (!this.instance) { this.init(); return; } this.instance.setOptions(this.options); }); }); } createInstance(element, observer) { this.instance = new Map(element, this.options, this.theme, { observer: observer, rtl: this.rtl, sender: this }); } activeEmitter(name) { const alias = name === 'click' ? 'mapClick' : name; const emitter = this[alias]; if (emitter && emitter.emit && hasObservers(emitter)) { return emitter; } } trigger(name, e) { const emitter = this.activeEmitter(name); if (emitter) { const args = this.instanceEventService.create(name, e, this); if (!this.instance && e.sender) { // Ensure instance reference is populated for events trigger during initialization. this.instance = e.sender; } this.run(() => { emitter.emit(args); }); return args.isDefaultPrevented && args.isDefaultPrevented(); } } run(callback, inZone = true, detectChanges) { if (inZone) { if (detectChanges) { this.changeDetector.markForCheck(); } this.ngZone.run(callback); } else { callback(); if (detectChanges) { this.detectChanges(); } } } detectChanges() { if (!this.destroyed) { this.changeDetector.detectChanges(); } } setDirection() { this.rtl = this.isRTL; if (this.element) { this.renderer.setAttribute(this.element.nativeElement, 'dir', this.rtl ? 'rtl' : 'ltr'); } } get isRTL() { return Boolean(this.localizationService.rtl); } onInit(e) { this.instance = e.sender; } onShowTooltip(e) { this.run(() => { this.tooltipInstance.show(e); }, true, true); } onHideTooltip() { this.run(() => { this.tooltipInstance.hide(); }, true, true); } onCenterChange(e) { this.centerChange.next(e.center); } onZoomChange(e) { this.zoomChange.next(e.zoom); } /** * @hidden */ tooltipMouseleave(e) { const relatedTarget = e.relatedTarget; const chartElement = this.element.nativeElement; if (this.instance && (!relatedTarget || !hasParent(relatedTarget, chartElement))) { this.tooltipInstance.hide(); } } /** * @hidden */ mapMouseleave(e) { const relatedTarget = e.relatedTarget; const chartElement = this.element.nativeElement; if (this.instance && (!relatedTarget || !(this.tooltipInstance.containsElement(relatedTarget) || hasParent(relatedTarget, chartElement)))) { this.tooltipInstance.hide(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapComponent, deps: [{ token: i1.ConfigurationService }, { token: i2.InstanceEventService }, { token: i0.ElementRef }, { token: i3.LocalizationService }, { token: i0.ChangeDetectorRef }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i4.IconsService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: MapComponent, isStandalone: true, selector: "kendo-map", inputs: { resizeRateLimit: "resizeRateLimit", center: "center", controls: "controls", minZoom: "minZoom", maxZoom: "maxZoom", minSize: "minSize", pannable: "pannable", popupSettings: "popupSettings", wraparound: "wraparound", zoom: "zoom", zoomable: "zoomable" }, outputs: { beforeReset: "beforeReset", mapClick: "mapClick", markerActivate: "markerActivate", markerClick: "markerClick", markerCreated: "markerCreated", panEnd: "panEnd", pan: "pan", reset: "reset", shapeClick: "shapeClick", shapeCreated: "shapeCreated", shapeFeatureCreated: "shapeFeatureCreated", shapeMouseEnter: "shapeMouseEnter", shapeMouseLeave: "shapeMouseLeave", zoomStart: "zoomStart", zoomEnd: "zoomEnd", centerChange: "centerChange", zoomChange: "zoomChange" }, providers: [ ConfigurationService, InstanceEventService, LocalizationService, TooltipTemplateService, { provide: L10N_PREFIX, useValue: 'kendo.map' } ], viewQueries: [{ propertyName: "tooltipInstance", first: true, predicate: TooltipPopupComponent, descendants: true, static: true }], exportAs: ["kendoMap"], usesOnChanges: true, ngImport: i0, template: ` <kendo-map-tooltip-popup (leave)="tooltipMouseleave($event)" [popupSettings]="popupSettings"> </kendo-map-tooltip-popup> <div [style.width.%]="100" [style.height.%]="100"><!-- required for resize sensor to initialize properly --> <kendo-resize-sensor (resize)="onResize()" [rateLimit]="resizeRateLimit"></kendo-resize-sensor> </div> `, isInline: true, dependencies: [{ kind: "component", type: TooltipPopupComponent, selector: "kendo-map-tooltip-popup", inputs: ["animate", "classNames", "wrapperClass"], outputs: ["leave"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: MapComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-map', changeDetection: ChangeDetectionStrategy.OnPush, exportAs: 'kendoMap', providers: [ ConfigurationService, InstanceEventService, LocalizationService, TooltipTemplateService, { provide: L10N_PREFIX, useValue: 'kendo.map' } ], template: ` <kendo-map-tooltip-popup (leave)="tooltipMouseleave($event)" [popupSettings]="popupSettings"> </kendo-map-tooltip-popup> <div [style.width.%]="100" [style.height.%]="100"><!-- required for resize sensor to initialize properly --> <kendo-resize-sensor (resize)="onResize()" [rateLimit]="resizeRateLimit"></kendo-resize-sensor> </div> `, standalone: true, imports: [TooltipPopupComponent, ResizeSensorComponent] }] }], ctorParameters: function () { return [{ type: i1.ConfigurationService }, { type: i2.InstanceEventService }, { type: i0.ElementRef }, { type: i3.LocalizationService }, { type: i0.ChangeDetectorRef }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i4.IconsService }]; }, propDecorators: { resizeRateLimit: [{ type: Input }], center: [{ type: Input }], controls: [{ type: Input }], minZoom: [{ type: Input }], maxZoom: [{ type: Input }], minSize: [{ type: Input }], pannable: [{ type: Input }], popupSettings: [{ type: Input }], wraparound: [{ type: Input }], zoom: [{ type: Input }], zoomable: [{ type: Input }], beforeReset: [{ type: Output }], mapClick: [{ type: Output }], markerActivate: [{ type: Output }], markerClick: [{ type: Output }], markerCreated: [{ type: Output }], panEnd: [{ type: Output }], pan: [{ type: Output }], reset: [{ type: Output }], shapeClick: [{ type: Output }], shapeCreated: [{ type: Output }], shapeFeatureCreated: [{ type: Output }], shapeMouseEnter: [{ type: Output }], shapeMouseLeave: [{ type: Output }], zoomStart: [{ type: Output }], zoomEnd: [{ type: Output }], centerChange: [{ type: Output }], zoomChange: [{ type: Output }], tooltipInstance: [{ type: ViewChild, args: [TooltipPopupComponent, { static: true }] }] } });