@progress/kendo-angular-map
Version:
Kendo UI Map for Angular
695 lines (694 loc) • 25.1 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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="© <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 }]
}] } });