├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.html │ ├── leaflet-demo │ │ ├── performance │ │ │ ├── leaflet-wrapper.component.html │ │ │ ├── leaflet-wrapper.component.ts │ │ │ ├── performance-demo.component.ts │ │ │ └── performance-demo.component.html │ │ ├── layers │ │ │ ├── layers-demo.model.ts │ │ │ ├── baselayers-demo.component.html │ │ │ ├── markers-demo.component.html │ │ │ ├── baselayers-demo.component.ts │ │ │ ├── markers-demo.component.ts │ │ │ ├── ngfor-layers-demo.component.ts │ │ │ ├── ngfor-layers-demo.component.html │ │ │ ├── layers-demo.component.html │ │ │ └── layers-demo.component.ts │ │ ├── leaflet-demo.component.html │ │ ├── core │ │ │ ├── multi-map-demo.component.ts │ │ │ ├── core-demo.component.ts │ │ │ ├── multi-map-demo.component.html │ │ │ └── core-demo.component.html │ │ ├── events │ │ │ ├── events-demo.component.ts │ │ │ └── events-demo.component.html │ │ └── leaflet-demo.component.ts │ ├── app.component.ts │ └── app.component.spec.ts ├── favicon.ico ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── styles.scss ├── main.ts ├── test.ts ├── index.html └── polyfills.ts ├── .travis.yml ├── projects └── ngx-leaflet │ ├── ng-package.json │ ├── src │ ├── lib │ │ ├── layers │ │ │ ├── control │ │ │ │ ├── leaflet-control-layers-config.model.ts │ │ │ │ ├── leaflet-control-layers-changes.model.ts │ │ │ │ ├── leaflet-control-layers.wrapper.ts │ │ │ │ └── leaflet-control-layers.directive.ts │ │ │ ├── leaflet-tile-layer-definition.model.ts │ │ │ ├── leaflet-layer.directive.ts │ │ │ ├── leaflet-layers.directive.ts │ │ │ └── base │ │ │ │ └── leaflet-baselayers.directive.ts │ │ ├── core │ │ │ ├── leaflet.directive.wrapper.ts │ │ │ ├── leaflet.util.ts │ │ │ └── leaflet.directive.ts │ │ └── leaflet.module.ts │ ├── test.ts │ └── public-api.ts │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ ├── karma.conf.js │ └── package-lock.json ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── .github └── ISSUE_TEMPLATE.md ├── LICENSE ├── tsconfig.json ├── karma.conf.js ├── package.json ├── CHANGES.md ├── angular.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluehalo/ngx-leaflet/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "12" 4 | cache: npm 5 | script: npm run build 6 | sudo: false 7 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/performance/leaflet-wrapper.component.html: -------------------------------------------------------------------------------- 1 |
4 |
5 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ngx-leaflet", 4 | "lib": { 5 | "entryFile": "src/public-api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/control/leaflet-control-layers-config.model.ts: -------------------------------------------------------------------------------- 1 | import { Layer } from 'leaflet'; 2 | 3 | export class LeafletControlLayersConfig { 4 | baseLayers: { [name: string]: Layer } = {}; 5 | overlays: { [name: string]: Layer } = {}; 6 | } 7 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { LeafletDemoComponent } from './leaflet-demo/leaflet-demo.component'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | imports: [LeafletDemoComponent], 8 | }) 9 | export class AppComponent { 10 | // Empty component 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/control/leaflet-control-layers-changes.model.ts: -------------------------------------------------------------------------------- 1 | export class LeafletControlLayersChanges { 2 | layersRemoved: number = 0; 3 | layersChanged: number = 0; 4 | layersAdded: number = 0; 5 | 6 | changed(): boolean { 7 | return !(this.layersRemoved === 0 && this.layersChanged === 0 && this.layersAdded === 0); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "src/test.ts", 13 | "**/*.spec.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | tab_width = 4 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.ts] 13 | quote_type = single 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | 19 | [*.json] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, provideZoneChangeDetection } from '@angular/core'; 2 | 3 | import { environment } from './environments/environment'; 4 | import { bootstrapApplication } from '@angular/platform-browser'; 5 | import { AppComponent } from './app/app.component'; 6 | 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | bootstrapApplication(AppComponent, { providers: [provideZoneChangeDetection()] }) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js'; 4 | import 'zone.js/testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; 7 | 8 | // First, initialize the Angular testing environment. 9 | getTestBed().initTestEnvironment( 10 | BrowserTestingModule, 11 | platformBrowserTesting(), 12 | ); 13 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // First, initialize the Angular testing environment. 11 | getTestBed().initTestEnvironment( 12 | BrowserDynamicTestingModule, 13 | platformBrowserDynamicTesting(), 14 | ); 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Leaflet Demo Application 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | Loading... 16 |
17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [ 8 | AppComponent 9 | ], 10 | }).compileComponents(); 11 | }); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | 19 | }); 20 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/layers-demo.model.ts: -------------------------------------------------------------------------------- 1 | import { Layer } from 'leaflet'; 2 | 3 | export class LeafletLayersDemoModel { 4 | 5 | constructor( 6 | public baseLayers: { 7 | id: string, 8 | name: string, 9 | enabled: boolean, 10 | layer: Layer 11 | }[], 12 | public baseLayer: string, 13 | public overlayLayers: { 14 | id: string, 15 | name: string, 16 | enabled: boolean, 17 | layer: Layer 18 | }[] = [] 19 | ) { } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluehalo/ngx-leaflet", 3 | "description": "Angular.io components for Leaflet", 4 | "version": "21.0.0", 5 | "author": "BlueHalo, LLC", 6 | "copyright": "Copyright BlueHalo 2007-2025 - All Rights Reserved.", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/bluehalo/ngx-leaflet.git" 11 | }, 12 | 13 | "peerDependencies": { 14 | "@angular/common": "21", 15 | "@angular/core": "21", 16 | "leaflet": "1" 17 | }, 18 | "dependencies": { 19 | "tslib": "^2.8.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/core/leaflet.directive.wrapper.ts: -------------------------------------------------------------------------------- 1 | import { LeafletDirective } from './leaflet.directive'; 2 | 3 | import { Map } from 'leaflet'; 4 | 5 | export class LeafletDirectiveWrapper { 6 | 7 | // Reference to the main leaflet directive 8 | protected leafletDirective: LeafletDirective; 9 | 10 | constructor(leafletDirective: LeafletDirective) { 11 | this.leafletDirective = leafletDirective; 12 | } 13 | 14 | init() { 15 | // Nothing for now 16 | } 17 | 18 | getMap(): Map { 19 | return this.leafletDirective.getMap(); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/leaflet-demo.component.html: -------------------------------------------------------------------------------- 1 | @if (showDemo) { 2 |
3 | 4 | 5 |
6 | 7 | 8 |
9 | 10 | 11 |
12 | 13 | 14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 | } 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/core/leaflet.util.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, NgZone } from '@angular/core'; 2 | 3 | export class LeafletUtil { 4 | 5 | static mapToArray(map: { [ key: string ]: T }): T[] { 6 | const toReturn: T[] = []; 7 | 8 | for (const k in map) { 9 | if (map.hasOwnProperty(k)) { 10 | toReturn.push(map[k]); 11 | } 12 | } 13 | 14 | return toReturn; 15 | } 16 | 17 | static handleEvent(zone: NgZone, eventEmitter: EventEmitter, event: T) { 18 | 19 | // Don't want to emit if there are no observers 20 | if (0 < eventEmitter.observers.length) { 21 | zone.run(() => { 22 | eventEmitter.emit(event); 23 | }); 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | yarn.lock 14 | 15 | # IDEs and editors 16 | .idea/ 17 | .project 18 | .classpath 19 | .c9/ 20 | *.launch 21 | .settings/ 22 | *.sublime-workspace 23 | *.iml 24 | 25 | # Visual Studio Code 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | .history/* 32 | 33 | # Miscellaneous 34 | /.angular/cache 35 | .sass-cache/ 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | testem.log 40 | /typings 41 | *.swp 42 | *.tmp 43 | *.bak 44 | *~ 45 | 46 | # System files 47 | .DS_Store 48 | Thumbs.db 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before submitting an issue, please verify the following: 2 | 3 | If this is a request for help, please use [Stack Overflow](https://stackoverflow.com/questions/tagged/ngx-leaflet) and use the `ngx-leaflet` tag, or review the [Getting Help](https://github.com/Asymmetrik/ngx-leaflet#help) section of the README. We watch the `ngx-leaflet` tag on Stack Overflow and will respond there. We will no longer be responding to help and 'how-to' requests on GitHub. 4 | 5 | If this is a bug, issue, enhancement request, etc. verify the following before submitting your issue: 6 | 7 | - [ ] Review the README to see if your issue is covered, and pay particular attention to the [Getting Help](https://github.com/Asymmetrik/ngx-leaflet#help) section. 8 | - [ ] Tell us what version of Leaflet, Angular, and ngx-leaflet you're using 9 | - [ ] Include code that reproduces your issue 10 | 11 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/leaflet.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { LeafletDirective } from './core/leaflet.directive'; 4 | import { LeafletLayerDirective } from './layers/leaflet-layer.directive'; 5 | import { LeafletLayersDirective } from './layers/leaflet-layers.directive'; 6 | import { LeafletLayersControlDirective } from './layers/control/leaflet-control-layers.directive'; 7 | import { LeafletBaseLayersDirective } from './layers/base/leaflet-baselayers.directive'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | LeafletDirective, 12 | LeafletLayerDirective, 13 | LeafletLayersDirective, 14 | LeafletLayersControlDirective, 15 | LeafletBaseLayersDirective 16 | ], 17 | exports: [ 18 | LeafletDirective, 19 | LeafletLayerDirective, 20 | LeafletLayersDirective, 21 | LeafletLayersControlDirective, 22 | LeafletBaseLayersDirective 23 | ] 24 | }) 25 | export class LeafletModule { 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/performance/leaflet-wrapper.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; 2 | 3 | import { latLng, Layer, tileLayer } from 'leaflet'; 4 | import { LeafletDirective, LeafletLayersDirective } from 'projects/ngx-leaflet/src/public-api'; 5 | 6 | @Component({ 7 | selector: 'leafletWrapper', 8 | templateUrl: './leaflet-wrapper.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | imports: [LeafletDirective, LeafletLayersDirective], 11 | }) 12 | export class LeafletWrapperComponent { 13 | 14 | @Input('leafletMarkers') 15 | markers: Layer[] = []; 16 | 17 | // Open Street Map definitions 18 | LAYER_OSM = tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: 'Open Street Map' }); 19 | 20 | // Values to bind to Leaflet Directive 21 | options = { 22 | layers: [ this.LAYER_OSM ], 23 | zoom: 10, 24 | center: latLng(46.879966, -121.726909) 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/public-api.ts: -------------------------------------------------------------------------------- 1 | export { LeafletModule } from './lib/leaflet.module'; 2 | 3 | export { LeafletDirective } from './lib/core/leaflet.directive'; 4 | export { LeafletDirectiveWrapper } from './lib/core/leaflet.directive.wrapper'; 5 | export { LeafletUtil } from './lib/core/leaflet.util'; 6 | 7 | export { LeafletLayerDirective } from './lib/layers/leaflet-layer.directive'; 8 | export { LeafletLayersDirective } from './lib/layers/leaflet-layers.directive'; 9 | export { LeafletTileLayerDefinition } from './lib/layers/leaflet-tile-layer-definition.model'; 10 | 11 | export { LeafletBaseLayersDirective } from './lib/layers/base/leaflet-baselayers.directive'; 12 | 13 | export { LeafletLayersControlDirective } from './lib/layers/control/leaflet-control-layers.directive'; 14 | export { LeafletControlLayersWrapper } from './lib/layers/control/leaflet-control-layers.wrapper'; 15 | export { LeafletControlLayersConfig } from './lib/layers/control/leaflet-control-layers-config.model'; 16 | export { LeafletControlLayersChanges } from './lib/layers/control/leaflet-control-layers-changes.model'; 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2007-2024 BlueHalo, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "esModuleInterop": true, 9 | "strict": false, 10 | "noImplicitOverride": true, 11 | "noPropertyAccessFromIndexSignature": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "sourceMap": true, 15 | "paths": { 16 | "ngx-leaflet": [ 17 | "dist/ngx-leaflet" 18 | ] 19 | }, 20 | "declaration": false, 21 | "experimentalDecorators": true, 22 | "moduleResolution": "bundler", 23 | "importHelpers": true, 24 | "target": "ES2022", 25 | "module": "es2020", 26 | "lib": [ 27 | "es2020", 28 | "dom" 29 | ], 30 | "useDefineForClassFields": false 31 | }, 32 | "angularCompilerOptions": { 33 | "enableBlockSyntax": true, 34 | "enableI18nLegacyMessageIdFormat": false, 35 | "strictInjectionParameters": true, 36 | "strictInputAccessModifiers": true, 37 | "strictTemplates": true 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/core/multi-map-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { latLng, MapOptions, tileLayer } from 'leaflet'; 4 | import { LeafletDirective } from 'projects/ngx-leaflet/src/public-api'; 5 | 6 | interface MapSpec { 7 | options: MapOptions; 8 | } 9 | 10 | @Component({ 11 | selector: 'leafletMultiMapDemo', 12 | templateUrl: './multi-map-demo.component.html', 13 | imports: [LeafletDirective], 14 | }) 15 | export class LeafletMultiMapDemoComponent { 16 | 17 | optionsSpec: any = { 18 | layers: [{ url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: 'Open Street Map' }], 19 | zoom: 5, 20 | center: [ 46.879966, -121.726909 ] 21 | }; 22 | 23 | maps: MapSpec[] = []; 24 | 25 | doAddMap() { 26 | this.maps.push(this.createMapSpec(this.optionsSpec)); 27 | } 28 | 29 | doRemoveMap() { 30 | this.maps.pop(); 31 | } 32 | 33 | private createMapSpec(optionsSpec: any): MapSpec { 34 | return { 35 | options: { 36 | layers: [ tileLayer(optionsSpec.layers[0].url, { attribution: optionsSpec.layers[0].attribution }) ], 37 | zoom: optionsSpec.zoom, 38 | center: latLng(optionsSpec.center) 39 | } 40 | }; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/baselayers-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Simple Base Layers

5 |

6 | In this example, we create a map with multiple base layers using [leafletBaseLayers]. 7 | The [leafletBaseLayers] directive automatically adds the layers control so the user can switch between base layers. 8 | You can optionally provide options for the layers control using [leafletLayersControlOptions]. 9 |

10 | 11 |
12 |
13 | 14 |
15 | <div leaflet style="height: 300px;"
16 |         [leafletOptions]="options"
17 |         [leafletBaseLayers]="baseLayers"
18 |         [leafletLayersControlOptions]="layersControlOptions">
19 | </div>
20 | 
21 | 22 |
23 |
24 |
25 |
26 | 27 |
28 |
32 |
33 |
34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/performance/performance-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | 3 | import { icon, Layer, marker } from 'leaflet'; 4 | import { LeafletWrapperComponent } from './leaflet-wrapper.component'; 5 | 6 | @Component({ 7 | selector: 'leafletPerformanceDemo', 8 | templateUrl: './performance-demo.component.html', 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | imports: [LeafletWrapperComponent], 11 | }) 12 | export class LeafletPerformanceDemoComponent { 13 | 14 | markers: Layer[] = []; 15 | 16 | mutableAdd() { 17 | const newMarker = marker( 18 | [ 46.879966 + 0.1 * (Math.random() - 0.5), -121.726909 + 0.1 * (Math.random() - 0.5) ], 19 | { 20 | icon: icon({ 21 | iconSize: [ 25, 41 ], 22 | iconAnchor: [ 13, 41 ], 23 | iconUrl: 'assets/leaflet/marker-icon.png', 24 | iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png', 25 | shadowUrl: 'assets/leaflet/marker-shadow.png' 26 | }) 27 | } 28 | ); 29 | 30 | this.markers.push(newMarker); 31 | } 32 | 33 | mutableRemove() { 34 | this.markers.pop(); 35 | } 36 | 37 | newArray() { 38 | this.markers = this.markers.slice(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/events/events-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Subject } from 'rxjs'; 3 | import { debounceTime, scan } from 'rxjs/operators'; 4 | 5 | import { latLng, LeafletMouseEvent, tileLayer } from 'leaflet'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { LeafletDirective, LeafletLayerDirective } from 'projects/ngx-leaflet/src/public-api'; 8 | 9 | 10 | @Component({ 11 | selector: 'leafletEventsDemo', 12 | templateUrl: './events-demo.component.html', 13 | imports: [FormsModule, LeafletDirective, LeafletLayerDirective], 14 | }) 15 | export class LeafletEventsDemoComponent { 16 | 17 | eventCount = 0; 18 | eventLog = ''; 19 | 20 | options = { 21 | zoom: 5, 22 | center: latLng([ 46.879966, -121.726909 ]) 23 | }; 24 | baselayer = tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: 'Open Street Map' }); 25 | 26 | eventSubject = new Subject(); 27 | 28 | constructor() { 29 | this.eventSubject.pipe( 30 | scan((acc: string, v: string) => `${++this.eventCount}: ${v}\n${acc}`, ''), 31 | debounceTime(50) 32 | ) 33 | .subscribe((v: string) => { this.eventLog = v; } ); 34 | } 35 | 36 | handleEvent(eventType: string, event: any) { 37 | this.eventSubject.next(eventType); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/leaflet-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { LeafletCoreDemoComponent } from './core/core-demo.component'; 3 | import { LeafletBaseLayersDemoComponent } from './layers/baselayers-demo.component'; 4 | import { LeafletLayersDemoComponent } from './layers/layers-demo.component'; 5 | import { LeafletNgForLayersDemoComponent } from './layers/ngfor-layers-demo.component'; 6 | import { LeafletMarkersDemoComponent } from './layers/markers-demo.component'; 7 | import { LeafletPerformanceDemoComponent } from './performance/performance-demo.component'; 8 | import { LeafletEventsDemoComponent } from './events/events-demo.component'; 9 | import { LeafletMultiMapDemoComponent } from './core/multi-map-demo.component'; 10 | 11 | @Component({ 12 | selector: 'leafletDemo', 13 | templateUrl: './leaflet-demo.component.html', 14 | imports: [ 15 | LeafletCoreDemoComponent, 16 | LeafletBaseLayersDemoComponent, 17 | LeafletLayersDemoComponent, 18 | LeafletNgForLayersDemoComponent, 19 | LeafletMarkersDemoComponent, 20 | LeafletPerformanceDemoComponent, 21 | LeafletEventsDemoComponent, 22 | LeafletMultiMapDemoComponent, 23 | ], 24 | }) 25 | export class LeafletDemoComponent { 26 | showDemo = false; 27 | 28 | ngOnInit() { 29 | // Primarily for debugging 30 | this.showDemo = true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | ], 14 | client: { 15 | jasmine: { 16 | // you can add configuration options for Jasmine here 17 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 18 | // for example, you can disable the random execution with `random: false` 19 | // or set a specific seed with `seed: 4321` 20 | }, 21 | clearContext: false // leave Jasmine Spec Runner output visible in browser 22 | }, 23 | jasmineHtmlReporter: { 24 | suppressAll: true // removes the duplicated traces 25 | }, 26 | coverageReporter: { 27 | dir: require('path').join(__dirname, './coverage/ngx-leaflet-demo'), 28 | subdir: '.', 29 | reporters: [ 30 | { type: 'html' }, 31 | { type: 'text-summary' } 32 | ] 33 | }, 34 | reporters: ['progress', 'kjhtml'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false, 41 | restartOnFileChange: true 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | ], 14 | client: { 15 | jasmine: { 16 | // you can add configuration options for Jasmine here 17 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 18 | // for example, you can disable the random execution with `random: false` 19 | // or set a specific seed with `seed: 4321` 20 | }, 21 | clearContext: false // leave Jasmine Spec Runner output visible in browser 22 | }, 23 | jasmineHtmlReporter: { 24 | suppressAll: true // removes the duplicated traces 25 | }, 26 | coverageReporter: { 27 | dir: require('path').join(__dirname, '../../coverage/ngx-leaflet'), 28 | subdir: '.', 29 | reporters: [ 30 | { type: 'html' }, 31 | { type: 'text-summary' } 32 | ] 33 | }, 34 | reporters: ['progress', 'kjhtml'], 35 | port: 9876, 36 | colors: true, 37 | logLevel: config.LOG_INFO, 38 | autoWatch: true, 39 | browsers: ['Chrome'], 40 | singleRun: false, 41 | restartOnFileChange: true 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/markers-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Markers

5 |

6 | Markers are just like any other layer. 7 | You can add them to a map by creating them and adding them to an array bound to leafletLayers. 8 |

9 | 10 |
11 |
12 | 13 |
14 | <div leaflet style="height: 300px;"
15 |         [leafletOptions]="options"
16 |         [leafletLayers]="markers">
17 | </div>
18 | 
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 | 31 | 32 | 35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 | 43 |
46 |
47 | 48 |
49 | 50 |
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/baselayers-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { Control, latLng, tileLayer } from 'leaflet'; 4 | import LayersOptions = Control.LayersOptions; 5 | import { LeafletBaseLayersDirective, LeafletDirective } from 'projects/ngx-leaflet/src/public-api'; 6 | 7 | @Component({ 8 | selector: 'leafletBaselayersDemo', 9 | templateUrl: './baselayers-demo.component.html', 10 | imports: [LeafletDirective, LeafletBaseLayersDirective], 11 | }) 12 | export class LeafletBaseLayersDemoComponent { 13 | 14 | // Open Street Map and Open Cycle Map definitions 15 | LAYER_OCM = { 16 | id: 'opencyclemap', 17 | name: 'Open Cycle Map', 18 | enabled: true, 19 | layer: tileLayer('http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png', { 20 | maxZoom: 18, 21 | attribution: 'Open Cycle Map' 22 | }) 23 | }; 24 | LAYER_OSM = { 25 | id: 'openstreetmap', 26 | name: 'Open Street Map', 27 | enabled: false, 28 | layer: tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 29 | maxZoom: 18, 30 | attribution: 'Open Street Map' 31 | }) 32 | }; 33 | 34 | // Values to bind to Leaflet Directive 35 | layersControlOptions: LayersOptions = { position: 'bottomright' }; 36 | baseLayers = { 37 | 'Open Street Map': this.LAYER_OSM.layer, 38 | 'Open Cycle Map': this.LAYER_OCM.layer 39 | }; 40 | options = { 41 | zoom: 10, 42 | center: latLng(46.879966, -121.726909) 43 | }; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluehalo/ngx-leaflet", 3 | "version": "0.0.0", 4 | "copyright": "Copyright BlueHalo 2007-2024 - All Rights Reserved.", 5 | "license": "MIT", 6 | "scripts": { 7 | "ng": "ng", 8 | "demo": "ng serve ngx-leaflet-demo", 9 | "build": "ng build ngx-leaflet --configuration production && copyfiles README.md LICENSE CHANGES.md ./dist/ngx-leaflet", 10 | "watch": "ng build ngx-leaflet --watch --configuration development", 11 | "test": "ng test ngx-leaflet" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "^21.0.1", 16 | "@angular/common": "^21.0.1", 17 | "@angular/compiler": "^21.0.1", 18 | "@angular/core": "^21.0.1", 19 | "@angular/forms": "^21.0.1", 20 | "@angular/platform-browser": "^21.0.1", 21 | "@angular/platform-browser-dynamic": "^21.0.1", 22 | "@angular/router": "^21.0.1", 23 | "@types/leaflet": "1", 24 | "bootstrap": "^5.3.8", 25 | "copyfiles": "^2.4.1", 26 | "leaflet": "1", 27 | "rxjs": "~7.8.2", 28 | "tslib": "^2.8.1", 29 | "zone.js": "~0.16.0" 30 | }, 31 | "devDependencies": { 32 | "@angular/build": "^21.0.1", 33 | "@angular/cli": "^21.0.1", 34 | "@angular/compiler-cli": "^21.0.1", 35 | "@types/jasmine": "~5.1.13", 36 | "@types/node": "^24.10.1", 37 | "jasmine-core": "~5.12.1", 38 | "karma": "~6", 39 | "karma-chrome-launcher": "~3.2.0", 40 | "karma-coverage": "~2.2.1", 41 | "karma-jasmine": "~5.1.0", 42 | "karma-jasmine-html-reporter": "~2.1.0", 43 | "ng-packagr": "^21.0.0", 44 | "typescript": "~5.9.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/markers-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { icon, latLng, Layer, marker, tileLayer } from 'leaflet'; 4 | import { LeafletDirective, LeafletLayersDirective } from 'projects/ngx-leaflet/src/public-api'; 5 | 6 | @Component({ 7 | selector: 'leafletMarkersDemo', 8 | templateUrl: './markers-demo.component.html', 9 | imports: [LeafletDirective, LeafletLayersDirective], 10 | }) 11 | export class LeafletMarkersDemoComponent { 12 | 13 | // Open Street Map definitions 14 | LAYER_OSM = tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: 'Open Street Map' }); 15 | 16 | // Values to bind to Leaflet Directive 17 | options = { 18 | layers: [ this.LAYER_OSM ], 19 | zoom: 10, 20 | center: latLng(46.879966, -121.726909) 21 | }; 22 | 23 | markers: Layer[] = []; 24 | 25 | addMarker() { 26 | const newMarker = marker( 27 | [ 46.879966 + 0.1 * (Math.random() - 0.5), -121.726909 + 0.1 * (Math.random() - 0.5) ], 28 | { 29 | icon: icon({ 30 | iconSize: [ 25, 41 ], 31 | iconAnchor: [ 13, 41 ], 32 | iconUrl: 'assets/leaflet/marker-icon.png', 33 | iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png', 34 | shadowUrl: 'assets/leaflet/marker-shadow.png' 35 | }) 36 | } 37 | ); 38 | 39 | this.markers.push(newMarker); 40 | } 41 | 42 | removeMarker() { 43 | this.markers.pop(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/ngfor-layers-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { icon, latLng, marker, Marker, tileLayer } from 'leaflet'; 4 | import { LeafletDirective, LeafletLayerDirective } from 'projects/ngx-leaflet/src/public-api'; 5 | 6 | @Component({ 7 | selector: 'leafletNgForLayersDemo', 8 | templateUrl: './ngfor-layers-demo.component.html', 9 | imports: [LeafletDirective, LeafletLayerDirective], 10 | }) 11 | export class LeafletNgForLayersDemoComponent { 12 | 13 | // Open Street Map definitions 14 | LAYER_OSM = tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: 'Open Street Map' }); 15 | 16 | // Values to bind to Leaflet Directive 17 | options = { 18 | layers: [ this.LAYER_OSM ], 19 | zoom: 10, 20 | center: latLng(46.879966, -121.726909) 21 | }; 22 | 23 | markers: Marker[] = []; 24 | 25 | addMarker() { 26 | const newMarker = marker( 27 | [ 46.879966 + 0.1 * (Math.random() - 0.5), -121.726909 + 0.1 * (Math.random() - 0.5) ], 28 | { 29 | icon: icon({ 30 | iconSize: [ 25, 41 ], 31 | iconAnchor: [ 13, 41 ], 32 | iconUrl: 'assets/leaflet/marker-icon.png', 33 | iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png', 34 | shadowUrl: 'assets/leaflet/marker-shadow.png' 35 | }) 36 | } 37 | ); 38 | 39 | this.markers.push(newMarker); 40 | } 41 | 42 | removeMarker() { 43 | this.markers.pop(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/core/core-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { JsonPipe } from '@angular/common'; 2 | import { Component } from '@angular/core'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { latLng, LatLng, tileLayer } from 'leaflet'; 6 | import { LeafletDirective } from 'projects/ngx-leaflet/src/public-api'; 7 | 8 | @Component({ 9 | selector: 'leafletCoreDemo', 10 | templateUrl: './core-demo.component.html', 11 | imports: [FormsModule, JsonPipe, LeafletDirective], 12 | }) 13 | export class LeafletCoreDemoComponent { 14 | 15 | optionsSpec: any = { 16 | layers: [{ url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: 'Open Street Map' }], 17 | zoom: 5, 18 | center: [ 46.879966, -121.726909 ] 19 | }; 20 | 21 | // Leaflet bindings 22 | zoom = this.optionsSpec.zoom; 23 | center = latLng(this.optionsSpec.center); 24 | options = { 25 | layers: [ tileLayer(this.optionsSpec.layers[0].url, { attribution: this.optionsSpec.layers[0].attribution }) ], 26 | zoom: this.optionsSpec.zoom, 27 | center: latLng(this.optionsSpec.center) 28 | }; 29 | 30 | // Form bindings 31 | formZoom = this.zoom; 32 | zoomLevels = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 ]; 33 | lat = this.center.lat; 34 | lng = this.center.lng; 35 | 36 | // Output binding for center 37 | onCenterChange(center: LatLng) { 38 | this.lat = center.lat; 39 | this.lng = center.lng; 40 | } 41 | 42 | onZoomChange(zoom: number) { 43 | this.formZoom = zoom; 44 | } 45 | 46 | doApply() { 47 | this.center = latLng(this.lat, this.lng); 48 | this.zoom = this.formZoom; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/ngfor-layers-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Layers and @for

5 |

6 | You can also use @for or @if to show/hide individual layers by nesting them inside the map element. 7 | In this case, you can use [leafletLayer] to bind an individual layer. 8 |

9 | 10 |
11 |
12 | 13 |
14 | <div leaflet style="height: 300px;" [leafletOptions]="options">
15 |     @for(m of markers; track $index) {{ "{" }}
16 |         <div [leafletLayer]="m"></div>
17 |     {{ "}" }}
18 | </div>
19 | 
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 |
29 | 30 | 33 | 34 | 37 | 38 |
39 |
40 |
41 | 42 |
43 | 44 | 45 |
46 | @for (m of markers; track $index) { 47 |
48 | } 49 |
50 | 51 | 52 |
53 | 54 |
55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/core/multi-map-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Adding Multiple Maps

5 |

6 | This is a demonstration of creating and managing multiple maps on a single template. 7 | This code demonstrates the proper way of making sure to not reuse Leaflet objects between multiple maps. 8 |

9 | 10 |
11 |
12 | 13 |
14 | @for(m of maps; track $index) {{ "{}" }}
15 |     <div leaflet style="height: 300px;"
16 |         [leafletOptions]="m.options">
17 |     </div>
18 | {{ "}" }}
19 | 
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 |
42 | 43 | 44 | @for (m of maps; track $index) { 45 |
46 | 47 |
50 |
51 | 52 |
53 | } 54 | 55 |
56 | 57 |
58 |
59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/leaflet-tile-layer-definition.model.ts: -------------------------------------------------------------------------------- 1 | import { tileLayer, TileLayer } from 'leaflet'; 2 | 3 | export class LeafletTileLayerDefinition { 4 | 5 | constructor( 6 | public type: string, 7 | public url: string, 8 | public options: any) { } 9 | 10 | 11 | /** 12 | * Creates a TileLayer from the provided definition. This is a convenience function 13 | * to help with generating layers from objects. 14 | * 15 | * @param layerDef The layer to create 16 | * @returns {TileLayer} The TileLayer that has been created 17 | */ 18 | static createTileLayer(layerDef: LeafletTileLayerDefinition): TileLayer { 19 | let layer: TileLayer; 20 | 21 | switch (layerDef.type) { 22 | case 'xyz': 23 | layer = tileLayer(layerDef.url, layerDef.options); 24 | break; 25 | case 'wms': 26 | default: 27 | layer = tileLayer.wms(layerDef.url, layerDef.options); 28 | break; 29 | } 30 | 31 | return layer; 32 | } 33 | 34 | /** 35 | * Creates a TileLayer for each key in the incoming map. This is a convenience function 36 | * for generating an associative array of layers from an associative array of objects 37 | * 38 | * @param layerDefs A map of key to tile layer definition 39 | * @returns {{[p: string]: TileLayer}} A new map of key to TileLayer 40 | */ 41 | static createTileLayers(layerDefs: { [ key: string ]: LeafletTileLayerDefinition }): { [ key: string ]: TileLayer } { 42 | const layers: { [ key: string ]: TileLayer } = {}; 43 | 44 | for (const k in layerDefs) { 45 | if (layerDefs.hasOwnProperty(k)) { 46 | layers[k] = (LeafletTileLayerDefinition.createTileLayer(layerDefs[k])); 47 | } 48 | } 49 | 50 | return layers; 51 | } 52 | 53 | /** 54 | * Create a Tile Layer from the current state of this object 55 | * 56 | * @returns {TileLayer} A new TileLayer 57 | */ 58 | createTileLayer(): TileLayer { 59 | return LeafletTileLayerDefinition.createTileLayer(this); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/performance/performance-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Efficient Change Detection

4 |

5 | We currently use iterable and key/value differs to track changes to allow mutable changes to the bound objects/arrays. 6 | This can become inefficient for large layer arrays because we have to compare the contents of the arrays to tell if they are different. 7 |

8 |

9 | There are several strategies for mitigating this performance impact if it becomes an issue: 10 |

11 |
    12 |
  • 13 | First, you can wrap the directive in a component that uses the OnPush change detection strategy. 14 | That way, you can ensure that change detection is only applied when you want. 15 |
  • 16 |
  • 17 | Second, you can wrap large sets of layers (e.g., markers) in Leaflet LayerGroup layers to keep the bound array size small. 18 | But, this will be difficult to manage 19 |
  • 20 |
21 |

22 | This example shows how to use OnPush to improve change detection efficiency. See the README for more details. 23 |

24 | 25 |
26 |
27 | 28 |
29 |
30 | 33 | 34 | 37 | 38 | 41 |
42 |
43 |
44 | 45 |
46 | 47 | 48 | 49 |
50 |
51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js'; // Included with Angular CLI. 49 | 50 | 51 | /*************************************************************************************************** 52 | * APPLICATION IMPORTS 53 | */ 54 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/events/events-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Map and Layer Events

5 |

6 | In this example, we demonstrate how to use the various Map and Layer events using output binding. 7 | Mouse events are exposed using a pattern similar to (leafletClick), (leafletMouseDown), etc. 8 | The map Zoom/Move events are exposed using (leafletMapMove), (leafletMapZoom), etc. 9 |

10 | 11 |
12 |
13 | 14 |
15 | <div leaflet style="height: 300px;"
16 |         [leafletOptions]="options"
17 |         (leafletClick)="handleEvent('click', $event)"
18 |         (leafletMapMoveEnd)="handleEvent('mapMoveEnd', $event)"
19 |         (leafletMapZoomEnd)="handleEvent('mapZoomEnd', $event)">
20 |     <div [leafletLayer]="baselayer"
21 |          (leafletLayerAdd)="handleEvent('layerAdd', $event)">
22 |     </div>
23 | </div>
24 | 
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 | 46 |
47 | 48 |
49 | 50 |
51 | 52 | 53 |
54 | 55 |
60 | 61 |
63 |
64 | 65 |
66 | 67 |
68 | 69 |
70 | 71 |
72 |
73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluehalo/ngx-leaflet", 3 | "version": "21.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@bluehalo/ngx-leaflet", 9 | "version": "21.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "tslib": "^2.8.0" 13 | }, 14 | "peerDependencies": { 15 | "@angular/common": "21", 16 | "@angular/core": "21", 17 | "leaflet": "1" 18 | } 19 | }, 20 | "node_modules/@angular/common": { 21 | "version": "21.0.3", 22 | "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.0.3.tgz", 23 | "integrity": "sha512-y8U5jlaK5x3fhI7WOsuiwwNYghC5TBDfmqJdQ2YT4RFG0vB4b22RW5RY5GDbQ5La4AAcpcjoqb4zca8auLCe+g==", 24 | "license": "MIT", 25 | "peer": true, 26 | "dependencies": { 27 | "tslib": "^2.3.0" 28 | }, 29 | "engines": { 30 | "node": "^20.19.0 || ^22.12.0 || >=24.0.0" 31 | }, 32 | "peerDependencies": { 33 | "@angular/core": "21.0.3", 34 | "rxjs": "^6.5.3 || ^7.4.0" 35 | } 36 | }, 37 | "node_modules/@angular/core": { 38 | "version": "21.0.3", 39 | "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.0.3.tgz", 40 | "integrity": "sha512-/7a2FyZp5cyjNiwuNLr889KA8DVKSTcTtZJpz57Z9DpmZhPscDOWQqLn9f8jeEwbWllvgrXJi8pKSa78r8JAwA==", 41 | "license": "MIT", 42 | "peer": true, 43 | "dependencies": { 44 | "tslib": "^2.3.0" 45 | }, 46 | "engines": { 47 | "node": "^20.19.0 || ^22.12.0 || >=24.0.0" 48 | }, 49 | "peerDependencies": { 50 | "@angular/compiler": "21.0.3", 51 | "rxjs": "^6.5.3 || ^7.4.0", 52 | "zone.js": "~0.15.0 || ~0.16.0" 53 | }, 54 | "peerDependenciesMeta": { 55 | "@angular/compiler": { 56 | "optional": true 57 | }, 58 | "zone.js": { 59 | "optional": true 60 | } 61 | } 62 | }, 63 | "node_modules/leaflet": { 64 | "version": "1.9.3", 65 | "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.3.tgz", 66 | "integrity": "sha512-iB2cR9vAkDOu5l3HAay2obcUHZ7xwUBBjph8+PGtmW/2lYhbLizWtG7nTeYht36WfOslixQF9D/uSIzhZgGMfQ==", 67 | "peer": true 68 | }, 69 | "node_modules/rxjs": { 70 | "version": "7.8.2", 71 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", 72 | "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", 73 | "license": "Apache-2.0", 74 | "peer": true, 75 | "dependencies": { 76 | "tslib": "^2.1.0" 77 | } 78 | }, 79 | "node_modules/tslib": { 80 | "version": "2.8.1", 81 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 82 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 83 | "license": "0BSD" 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/leaflet-layer.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, EventEmitter, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, 3 | SimpleChange 4 | } from '@angular/core'; 5 | 6 | import { Layer, LeafletEvent } from 'leaflet'; 7 | 8 | import { LeafletDirective } from '../core/leaflet.directive'; 9 | import { LeafletDirectiveWrapper } from '../core/leaflet.directive.wrapper'; 10 | import { LeafletUtil } from '../core/leaflet.util'; 11 | 12 | 13 | /** 14 | * Layer directive 15 | * 16 | * This directive is used to directly control a single map layer. The purpose of this directive is to 17 | * be used as part of a child structural directive of the map element. 18 | * 19 | */ 20 | @Directive({ 21 | selector: '[leafletLayer]', 22 | }) 23 | export class LeafletLayerDirective 24 | implements OnChanges, OnDestroy, OnInit { 25 | 26 | @Input('leafletLayer') layer: Layer; 27 | 28 | // Layer Events 29 | @Output('leafletLayerAdd') onAdd = new EventEmitter(); 30 | @Output('leafletLayerRemove') onRemove = new EventEmitter(); 31 | 32 | // Layer Event handlers 33 | private onAddLayerHandler: any; 34 | private onRemoveLayerHandler: any; 35 | 36 | // Wrapper for the leaflet directive (manages the parent directive) 37 | private leafletDirective: LeafletDirectiveWrapper; 38 | 39 | constructor(leafletDirective: LeafletDirective, private zone: NgZone) { 40 | this.leafletDirective = new LeafletDirectiveWrapper(leafletDirective); 41 | } 42 | 43 | ngOnInit() { 44 | 45 | // Init the map 46 | this.leafletDirective.init(); 47 | 48 | } 49 | 50 | ngOnDestroy() { 51 | 52 | if (null != this.layer) { 53 | 54 | // Unregister the event handlers 55 | this.removeLayerEventListeners(this.layer); 56 | 57 | // Remove the layer from the map 58 | this.layer.remove(); 59 | } 60 | 61 | } 62 | 63 | ngOnChanges(changes: { [key: string]: SimpleChange }) { 64 | 65 | if (changes['layer']) { 66 | 67 | // Update the layer 68 | const p: Layer = changes['layer'].previousValue; 69 | const n = changes['layer'].currentValue; 70 | 71 | this.zone.runOutsideAngular(() => { 72 | if (null != p) { 73 | this.removeLayerEventListeners(p); 74 | p.remove(); 75 | } 76 | if (null != n) { 77 | this.addLayerEventListeners(n); 78 | this.leafletDirective.getMap().addLayer(n); 79 | } 80 | }); 81 | 82 | } 83 | 84 | } 85 | 86 | private addLayerEventListeners(l: Layer) { 87 | 88 | this.onAddLayerHandler = (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onAdd, e); 89 | l.on('add', this.onAddLayerHandler); 90 | 91 | this.onRemoveLayerHandler = (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onRemove, e); 92 | l.on('remove', this.onRemoveLayerHandler); 93 | 94 | } 95 | 96 | private removeLayerEventListeners(l: Layer) { 97 | 98 | l.off('add', this.onAddLayerHandler); 99 | l.off('remove', this.onRemoveLayerHandler); 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/control/leaflet-control-layers.wrapper.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter, KeyValueChanges, NgZone } from '@angular/core'; 2 | 3 | import { control, Control, Layer } from 'leaflet'; 4 | 5 | import { LeafletControlLayersChanges } from './leaflet-control-layers-changes.model'; 6 | 7 | export class LeafletControlLayersWrapper { 8 | 9 | // The layers control object 10 | protected layersControl: Control.Layers; 11 | 12 | // Event Emitter for when the control is ready 13 | protected layersControlReady: EventEmitter; 14 | 15 | constructor(private zone: NgZone, layersControlReady: EventEmitter) { 16 | this.layersControlReady = layersControlReady; 17 | } 18 | 19 | getLayersControl() { 20 | return this.layersControl; 21 | } 22 | 23 | init(controlConfig: any, controlOptions: any): Control.Layers { 24 | 25 | const baseLayers = controlConfig.baseLayers || {}; 26 | const overlays = controlConfig.overlays || {}; 27 | 28 | // Create the control outside of angular to ensure events don't trigger change detection 29 | this.zone.runOutsideAngular(() => { 30 | this.layersControl = control.layers(baseLayers, overlays, controlOptions); 31 | }); 32 | 33 | 34 | this.layersControlReady.emit(this.layersControl); 35 | 36 | return this.layersControl; 37 | } 38 | 39 | applyBaseLayerChanges(changes: KeyValueChanges): LeafletControlLayersChanges { 40 | let results: LeafletControlLayersChanges = new LeafletControlLayersChanges(); 41 | 42 | if (null != this.layersControl) { 43 | results = this.applyChanges(changes, this.layersControl.addBaseLayer); 44 | } 45 | 46 | return results; 47 | } 48 | 49 | applyOverlayChanges(changes: KeyValueChanges): LeafletControlLayersChanges { 50 | let results: LeafletControlLayersChanges = new LeafletControlLayersChanges(); 51 | 52 | if (null != this.layersControl) { 53 | results = this.applyChanges(changes, this.layersControl.addOverlay); 54 | } 55 | 56 | return results; 57 | } 58 | 59 | private applyChanges(changes: KeyValueChanges, addFn: (layer: Layer, name: string) => void): LeafletControlLayersChanges { 60 | const results: LeafletControlLayersChanges = new LeafletControlLayersChanges(); 61 | 62 | if (null != changes) { 63 | 64 | // All layer management is outside angular to avoid layer events from triggering change detection 65 | this.zone.runOutsideAngular(() => { 66 | 67 | changes.forEachChangedItem((c) => { 68 | this.layersControl.removeLayer(c.previousValue); 69 | addFn.call(this.layersControl, c.currentValue, c.key); 70 | results.layersChanged++; 71 | }); 72 | changes.forEachRemovedItem((c) => { 73 | this.layersControl.removeLayer(c.previousValue); 74 | results.layersRemoved++; 75 | }); 76 | changes.forEachAddedItem((c) => { 77 | addFn.call(this.layersControl, c.currentValue, c.key); 78 | results.layersAdded++; 79 | }); 80 | 81 | }); 82 | 83 | } 84 | 85 | return results; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 21.0 4 | Support for Angular.io 21. 5 | 6 | One note for v21 - Angular moved to Zoneless detection by default (https://angular.dev/guide/zoneless). This has implications, particularly for this library. For example, events are emitted outside of the change detection zone. 7 | You may need to make some changes to your application if you rely on the events generated by the maps. See the migration guide for details (https://angular.dev/update-guide?v=20.0-21.0&l=1). 8 | 9 | ## 20.0 10 | Support for Angular.io 20. 11 | 12 | ## 19.0 13 | Support for Angular.io 19. 14 | 15 | ## 18.0 16 | Support for Angular.io 18. 17 | 18 | Also, we are deprecating the @asymmetrik namespace. 19 | This will be the last version of this package we publish under that namespace. This and future versions will be under the @bluehalo namespace. 20 | 🫗 21 | 22 | ## 17.0 23 | Support for Angular.io 17. 🎉 24 | 25 | ## 16.0 26 | Support for Angular.io 16. 27 | 28 | ## 15.0 29 | Support for Angular.io 15. 🎉 30 | 31 | ## 14.0 32 | Support for Angular.io 14. 🎉 33 | 34 | ## 13.0 35 | Support for Angular.io 13. 🎉 36 | We skipped a bunch of versions to get to the Ivy built, Angular-CLI based latest. 37 | This was a big migration to a new structure and build process, so file a bug if you encounter any issues. 38 | 39 | ### 13.0.1 40 | Minor cleanup in the project and removed an accidental dependency 41 | 42 | ## 8.1 43 | Added call to Map.remove in OnDestroy handler. 44 | This should ensure that any outstanding event handlers are cleaned up. 45 | Added demo example for adding/removing maps dynamically. 46 | 47 | ## 8.0 48 | Support for Angular.io 10. 49 | 50 | ## 7.0 51 | Support for Angular.io 9. 🎉 52 | 53 | - Are your markers broken? In Leaflet 1.6, the marker icons changed enough to create new hashes. See [README](https://github.com/Asymmetrik/ngx-leaflet/blob/master/README.md#a-note-about-markers) for more details. 54 | - Renamed UMD bundle to `ngx-leaflet.umd.js`. This shouldn't affect anyone unless you're manually including the bundle. 55 | - Angular deprecated parameterless forRoot, so I removed the static function. You may need to update your import. 56 | 57 | ### 7.0.1 58 | Fixed an error running the demo. 59 | Cleanup in the README. 60 | Fixing minification to exclude comments and include license. 61 | 62 | 63 | ## 6.0 64 | Support for Angular.io 8. 65 | 66 | 67 | ## 5.0 68 | Support for Angular.io 7. 69 | Also moved demo to `localhost:4200`. 70 | 71 | 72 | ## 4.0 73 | Support for Angular 6. 74 | Also migrated to using npm scripts for the build (no more dev dependency on gulp). 75 | 76 | ### 4.1.0 77 | Exporting the `LeafletUtil` class. 78 | 79 | 80 | ## 3.0 81 | Support for Angular 5. Also cleaned up some of the functionality related to Angular zone management. 82 | Added documentation to README on Zone management. 83 | 84 | ### 3.1.0 85 | Added [map events](#map-events), [layer events](#layer-events). 86 | Added several input bound map options including ```[leafletMaxBounds]```, ```[leafletMaxZoom]```, and ```[leafletMinZoom]```. 87 | Added output binding for map center - ```(leafletMapCenter)``` and map zoom - ```(leafletMapZoom)```. 88 | 89 | 90 | ## 2.0 91 | Support for Angular 4. 92 | 93 | ### 2.6.0 94 | Wrapping several map operations in ```NgZone.runOutsideAngular``` in order to prevent excessive dirty checking. 95 | If you encounter an unexpected issue due to this change, please file an issue. 96 | 97 | ### 2.5.0 98 | Added the ```[leafletLayer]``` directive for adding/removing individual layers. 99 | 100 | ### 2.3.0 101 | Renamed the package to ```ngx-leaflet``` 102 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/layers-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Layers and Layer Controls

5 |

6 | This example demonstrates how to add/remove arbitrary layers to the map using the [leafletLayers] directive 7 | together with the layers control using the [leafletLayersControl] directive. 8 |

9 | 10 |
11 |
12 | 13 |
14 | <div leaflet style="height: 300px;"
15 |         [leafletOptions]="options"
16 |         [leafletLayers]="layers"
17 |         [leafletLayersControl]="layersControl">
18 | </div>
19 | 
20 | 21 |
22 |
23 | 24 |
25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | @for (layer of model.baseLayers; track $index) { 38 |
39 |
40 | 48 |
49 |
50 | } 51 |
52 | 53 | 54 | @if (model.overlayLayers?.length > 0) { 55 |
56 | 57 | @for (layer of model.overlayLayers; track $index; let i = $index) { 58 |
59 |
60 | 67 |
68 |
69 | } 70 |
71 | } 72 |
73 | 74 |
75 | 76 | 77 |
78 | 79 |
83 |
84 | 85 |
86 | 87 |
88 | 89 |
90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/leaflet-layers.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, DoCheck, Input, IterableDiffer, IterableDiffers, NgZone, OnDestroy, OnInit } from '@angular/core'; 2 | 3 | import { Layer} from 'leaflet'; 4 | 5 | import { LeafletDirective } from '../core/leaflet.directive'; 6 | import { LeafletDirectiveWrapper } from '../core/leaflet.directive.wrapper'; 7 | 8 | 9 | /** 10 | * Layers directive 11 | * 12 | * This directive is used to directly control map layers. As changes are made to the input array of 13 | * layers, the map is synched to the array. As layers are added or removed from the input array, they 14 | * are also added or removed from the map. The input array is treated as immutable. To detect changes, 15 | * you must change the array instance. 16 | * 17 | * Important Note: The input layers array is assumed to be immutable. This means you need to use an 18 | * immutable array implementation or create a new copy of your array when you make changes, otherwise 19 | * this directive won't detect the change. This is by design. It's for performance reasons. Change 20 | * detection of mutable arrays requires diffing the state of the array on every DoCheck cycle, which 21 | * is extremely expensive from a time complexity perspective. 22 | * 23 | */ 24 | @Directive({ 25 | selector: '[leafletLayers]', 26 | }) 27 | export class LeafletLayersDirective 28 | implements DoCheck, OnDestroy, OnInit { 29 | 30 | // Array of configured layers 31 | layersValue: Layer[]; 32 | 33 | // Differ to do change detection on the array 34 | layersDiffer: IterableDiffer; 35 | 36 | // Set/get the layers 37 | @Input('leafletLayers') 38 | set layers(v: Layer[]) { 39 | this.layersValue = v; 40 | 41 | // Now that we have a differ, do an immediate layer update 42 | this.updateLayers(); 43 | } 44 | get layers(): Layer[] { 45 | return this.layersValue; 46 | } 47 | 48 | // Wrapper for the leaflet directive (manages the parent directive) 49 | private leafletDirective: LeafletDirectiveWrapper; 50 | 51 | constructor(leafletDirective: LeafletDirective, private differs: IterableDiffers, private zone: NgZone) { 52 | this.leafletDirective = new LeafletDirectiveWrapper(leafletDirective); 53 | this.layersDiffer = this.differs.find([]).create(); 54 | } 55 | 56 | ngDoCheck() { 57 | this.updateLayers(); 58 | } 59 | 60 | ngOnInit() { 61 | 62 | // Init the map 63 | this.leafletDirective.init(); 64 | 65 | // Update layers once the map is ready 66 | this.updateLayers(); 67 | 68 | } 69 | 70 | ngOnDestroy() { 71 | this.layers = []; 72 | } 73 | 74 | /** 75 | * Update the state of the layers. 76 | * We use an iterable differ to synchronize the map layers with the state of the bound layers array. 77 | * This is important because it allows us to react to changes to the contents of the array as well 78 | * as changes to the actual array instance. 79 | */ 80 | private updateLayers() { 81 | 82 | const map = this.leafletDirective.getMap(); 83 | 84 | if (null != map && null != this.layersDiffer) { 85 | 86 | const changes = this.layersDiffer.diff(this.layersValue); 87 | if (null != changes) { 88 | 89 | // Run outside angular to ensure layer events don't trigger change detection 90 | this.zone.runOutsideAngular(() => { 91 | 92 | changes.forEachRemovedItem((c) => { 93 | map.removeLayer(c.item); 94 | }); 95 | changes.forEachAddedItem((c) => { 96 | map.addLayer(c.item); 97 | }); 98 | 99 | }); 100 | 101 | } 102 | 103 | } 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/layers/layers-demo.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { circle, geoJSON, icon, latLng, Layer, marker, polygon, tileLayer } from 'leaflet'; 4 | 5 | import { LeafletLayersDemoModel } from './layers-demo.model'; 6 | import { FormsModule } from '@angular/forms'; 7 | import { LeafletDirective, LeafletLayersControlDirective, LeafletLayersDirective } from 'projects/ngx-leaflet/src/public-api'; 8 | 9 | @Component({ 10 | selector: 'leafletLayersDemo', 11 | templateUrl: './layers-demo.component.html', 12 | imports: [ 13 | FormsModule, 14 | LeafletDirective, 15 | LeafletLayersDirective, 16 | LeafletLayersControlDirective, 17 | ], 18 | }) 19 | export class LeafletLayersDemoComponent { 20 | 21 | // Open Street Map and Open Cycle Map definitions 22 | LAYER_OCM = { 23 | id: 'opencyclemap', 24 | name: 'Open Cycle Map', 25 | enabled: true, 26 | layer: tileLayer('http://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png', { 27 | maxZoom: 18, 28 | attribution: 'Open Cycle Map' 29 | }) 30 | }; 31 | LAYER_OSM = { 32 | id: 'openstreetmap', 33 | name: 'Open Street Map', 34 | enabled: false, 35 | layer: tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 36 | maxZoom: 18, 37 | attribution: 'Open Street Map' 38 | }) 39 | }; 40 | 41 | circle = { 42 | id: 'circle', 43 | name: 'Circle', 44 | enabled: true, 45 | layer: circle([ 46.95, -122 ], { radius: 5000 }) 46 | }; 47 | polygon = { 48 | id: 'polygon', 49 | name: 'Polygon', 50 | enabled: true, 51 | layer: polygon([[ 46.8, -121.85 ], [ 46.92, -121.92 ], [ 46.87, -121.8 ]]) 52 | }; 53 | square = { 54 | id: 'square', 55 | name: 'Square', 56 | enabled: true, 57 | layer: polygon([[ 46.8, -121.55 ], [ 46.9, -121.55 ], [ 46.9, -121.7 ], [ 46.8, -121.7 ]]) 58 | }; 59 | marker = { 60 | id: 'marker', 61 | name: 'Marker', 62 | enabled: true, 63 | layer: marker([ 46.879966, -121.726909 ], { 64 | icon: icon({ 65 | iconSize: [ 25, 41 ], 66 | iconAnchor: [ 13, 41 ], 67 | iconUrl: 'assets/leaflet/marker-icon.png', 68 | iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png', 69 | shadowUrl: 'assets/leaflet/marker-shadow.png' 70 | }) 71 | }) 72 | }; 73 | geoJSON = { 74 | id: 'geoJSON', 75 | name: 'Geo JSON Polygon', 76 | enabled: true, 77 | layer: geoJSON( 78 | ({ 79 | type: 'Polygon', 80 | coordinates: [[ 81 | [ -121.6, 46.87 ], 82 | [ -121.5, 46.87 ], 83 | [ -121.5, 46.93], 84 | [ -121.6, 46.87 ] 85 | ]] 86 | }) as any, 87 | { style: () => ({ color: '#ff7800' })}) 88 | }; 89 | 90 | // Form model object 91 | model = new LeafletLayersDemoModel( 92 | [ this.LAYER_OSM, this.LAYER_OCM ], 93 | this.LAYER_OCM.id, 94 | [ this.circle, this.polygon, this.square, this.marker, this.geoJSON ] 95 | ); 96 | 97 | 98 | // Values to bind to Leaflet Directive 99 | layers: Layer[]; 100 | layersControl = { 101 | baseLayers: { 102 | 'Open Street Map': this.LAYER_OSM.layer, 103 | 'Open Cycle Map': this.LAYER_OCM.layer 104 | }, 105 | overlays: { 106 | Circle: this.circle.layer, 107 | Square: this.square.layer, 108 | Polygon: this.polygon.layer, 109 | Marker: this.marker.layer, 110 | GeoJSON: this.geoJSON.layer 111 | } 112 | }; 113 | options = { 114 | zoom: 10, 115 | center: latLng(46.879966, -121.726909) 116 | }; 117 | 118 | constructor() { 119 | this.apply(); 120 | } 121 | 122 | apply() { 123 | 124 | // Get the active base layer 125 | const baseLayer = this.model.baseLayers.find((l: any) => (l.id === this.model.baseLayer)); 126 | 127 | // Get all the active overlay layers 128 | const newLayers = this.model.overlayLayers 129 | .filter((l: any) => l.enabled) 130 | .map((l: any) => l.layer); 131 | newLayers.unshift(baseLayer.layer); 132 | 133 | this.layers = newLayers; 134 | 135 | return false; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/app/leaflet-demo/core/core-demo.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |

Core - Using options

5 |

6 | This is a demonstration of setting up a basic map with initial options, including a base layer, an initial zoom level, and a center LatLng position. 7 | The zoom level and center position are bound as inputs - [leafletZoom] and [leafletCenter] respectively - so changes are automatically applied to the map. 8 | There are also outputs for (leafletZoomChange) and (leafletCenterChange) as well, so changes to the map are communicated to the component. 9 | The map is initialized with the options bound to [leafletOptions]. 10 | After initialization, changes to the options are ignored. 11 |

12 | 13 |
14 |
15 | 16 |
 17 | <div leaflet style="height: 300px;"
 18 |         [leafletOptions]="options"
 19 |         [leafletZoom]="zoom"
 20 |         (leafletZoomChange)="onZoomChange($event)"
 21 |         [leafletCenter]="center"
 22 |         (leafletCenterChange)="onCenterChange($event)">
 23 | </div>
 24 | 
25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |
38 | 39 |
40 | 41 | {{ optionsSpec | json }} 42 |
43 | 44 | 45 |
46 |
47 | 50 |
51 |
52 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 | 74 |
75 |
76 | 77 |
78 |
79 | 80 |
81 |
82 |
83 | 86 |
87 |
88 |
89 |
90 | 91 |
92 | 93 | 94 |
95 | 96 |
102 |
103 | 104 |
105 | 106 |
107 | 108 |
109 |
110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/control/leaflet-control-layers.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, DoCheck, EventEmitter, Input, KeyValueDiffer, KeyValueDiffers, NgZone, OnDestroy, OnInit, 3 | Output 4 | } from '@angular/core'; 5 | 6 | import { Control, Layer } from 'leaflet'; 7 | 8 | import { LeafletDirective } from '../../core/leaflet.directive'; 9 | import { LeafletDirectiveWrapper } from '../../core/leaflet.directive.wrapper'; 10 | import { LeafletControlLayersWrapper } from './leaflet-control-layers.wrapper'; 11 | import { LeafletControlLayersConfig } from './leaflet-control-layers-config.model'; 12 | 13 | 14 | /** 15 | * Layers Control 16 | * 17 | * This directive is used to configure the layers control. The input accepts an object with two 18 | * key-value maps of layer name -> layer. Mutable changes are detected. On changes, a differ is 19 | * used to determine what changed so that layers are appropriately added or removed. 20 | * 21 | * To specify which layer to show as the 'active' baselayer, you will want to add it to the map 22 | * using the layers directive. Otherwise, the last one it sees will be used. 23 | */ 24 | @Directive({ 25 | selector: '[leafletLayersControl]', 26 | }) 27 | export class LeafletLayersControlDirective 28 | implements DoCheck, OnDestroy, OnInit { 29 | 30 | // Control Layers Configuration 31 | layersControlConfigValue: LeafletControlLayersConfig; 32 | 33 | baseLayersDiffer: KeyValueDiffer; 34 | overlaysDiffer: KeyValueDiffer; 35 | 36 | @Input('leafletLayersControl') 37 | set layersControlConfig(v: LeafletControlLayersConfig) { 38 | 39 | // Validation/init stuff 40 | if (null == v) { v = new LeafletControlLayersConfig(); } 41 | if (null == v.baseLayers) { v.baseLayers = {}; } 42 | if (null == v.overlays) { v.overlays = {}; } 43 | 44 | // Store the value 45 | this.layersControlConfigValue = v; 46 | 47 | // Update the map 48 | this.updateLayers(); 49 | 50 | } 51 | get layersControlConfig(): LeafletControlLayersConfig { 52 | return this.layersControlConfigValue; 53 | } 54 | 55 | @Input('leafletLayersControlOptions') layersControlOptions: any; 56 | 57 | @Output('leafletLayersControlReady') layersControlReady = new EventEmitter(); 58 | 59 | private controlLayers: LeafletControlLayersWrapper; 60 | private leafletDirective: LeafletDirectiveWrapper; 61 | 62 | constructor(leafletDirective: LeafletDirective, private differs: KeyValueDiffers, private zone: NgZone) { 63 | this.leafletDirective = new LeafletDirectiveWrapper(leafletDirective); 64 | this.controlLayers = new LeafletControlLayersWrapper(this.zone, this.layersControlReady); 65 | 66 | // Generate differs 67 | this.baseLayersDiffer = this.differs.find({}).create(); 68 | this.overlaysDiffer = this.differs.find({}).create(); 69 | 70 | } 71 | 72 | ngOnInit() { 73 | 74 | // Init the map 75 | this.leafletDirective.init(); 76 | 77 | // Set up control outside of angular to avoid change detection when using the control 78 | this.zone.runOutsideAngular(() => { 79 | 80 | // Set up all the initial settings 81 | this.controlLayers 82 | .init({}, this.layersControlOptions) 83 | .addTo(this.leafletDirective.getMap()); 84 | 85 | }); 86 | 87 | this.updateLayers(); 88 | 89 | } 90 | 91 | ngOnDestroy() { 92 | this.layersControlConfig = { baseLayers: {}, overlays: {} }; 93 | this.controlLayers.getLayersControl().remove(); 94 | } 95 | 96 | ngDoCheck() { 97 | this.updateLayers(); 98 | } 99 | 100 | protected updateLayers() { 101 | 102 | const map = this.leafletDirective.getMap(); 103 | const layersControl = this.controlLayers.getLayersControl(); 104 | 105 | if (null != map && null != layersControl) { 106 | 107 | // Run the baselayers differ 108 | if (null != this.baseLayersDiffer && null != this.layersControlConfigValue.baseLayers) { 109 | const changes = this.baseLayersDiffer.diff(this.layersControlConfigValue.baseLayers); 110 | this.controlLayers.applyBaseLayerChanges(changes); 111 | } 112 | 113 | // Run the overlays differ 114 | if (null != this.overlaysDiffer && null != this.layersControlConfigValue.overlays) { 115 | const changes = this.overlaysDiffer.diff(this.layersControlConfigValue.overlays); 116 | this.controlLayers.applyOverlayChanges(changes); 117 | } 118 | 119 | } 120 | 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/layers/base/leaflet-baselayers.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, DoCheck, EventEmitter, Input, KeyValueDiffer, KeyValueDiffers, NgZone, OnDestroy, 3 | OnInit, Output 4 | } from '@angular/core'; 5 | 6 | import { Control, Layer } from 'leaflet'; 7 | 8 | import { LeafletUtil } from '../../core/leaflet.util'; 9 | import { LeafletDirective } from '../../core/leaflet.directive'; 10 | import { LeafletDirectiveWrapper } from '../../core/leaflet.directive.wrapper'; 11 | import { LeafletControlLayersWrapper } from '../control/leaflet-control-layers.wrapper'; 12 | 13 | 14 | /** 15 | * Baselayers directive 16 | * 17 | * This directive is provided as a convenient way to add baselayers to the map. The input accepts 18 | * a key-value map of layer name -> layer. Mutable changed are detected. On changes, a differ is 19 | * used to determine what changed so that layers are appropriately added or removed. This directive 20 | * will also add the layers control so users can switch between available base layers. 21 | * 22 | * To specify which layer to show as the 'active' baselayer, you will want to add it to the map 23 | * using the layers directive. Otherwise, the plugin will use the last one it sees. 24 | */ 25 | @Directive({ 26 | selector: '[leafletBaseLayers]', 27 | }) 28 | export class LeafletBaseLayersDirective 29 | implements DoCheck, OnDestroy, OnInit { 30 | 31 | // Base Layers 32 | baseLayersValue: { [name: string]: Layer }; 33 | 34 | // Base Layers Map Differ 35 | baseLayersDiffer: KeyValueDiffer; 36 | 37 | // Set/get baseLayers 38 | @Input('leafletBaseLayers') 39 | set baseLayers(v: { [name: string]: Layer }) { 40 | this.baseLayersValue = v; 41 | 42 | this.updateBaseLayers(); 43 | } 44 | get baseLayers(): { [name: string]: Layer } { 45 | return this.baseLayersValue; 46 | } 47 | 48 | // Control Options 49 | @Input('leafletLayersControlOptions') layersControlOptions: Control.LayersOptions; 50 | 51 | // Output for once the layers control is ready 52 | @Output('leafletLayersControlReady') layersControlReady = new EventEmitter(); 53 | 54 | // Active Base Layer 55 | private baseLayer: Layer; 56 | 57 | private leafletDirective: LeafletDirectiveWrapper; 58 | private controlLayers: LeafletControlLayersWrapper; 59 | 60 | constructor(leafletDirective: LeafletDirective, private differs: KeyValueDiffers, private zone: NgZone) { 61 | this.leafletDirective = new LeafletDirectiveWrapper(leafletDirective); 62 | this.controlLayers = new LeafletControlLayersWrapper(this.zone, this.layersControlReady); 63 | this.baseLayersDiffer = this.differs.find({}).create(); 64 | } 65 | 66 | ngOnDestroy() { 67 | this.baseLayers = {}; 68 | if (null != this.controlLayers.getLayersControl()) { 69 | this.controlLayers.getLayersControl().remove(); 70 | } 71 | } 72 | 73 | ngOnInit() { 74 | 75 | // Init the map 76 | this.leafletDirective.init(); 77 | 78 | // Create the control outside angular to prevent events from triggering chnage detection 79 | this.zone.runOutsideAngular(() => { 80 | 81 | // Initially configure the controlLayers 82 | this.controlLayers 83 | .init({}, this.layersControlOptions) 84 | .addTo(this.leafletDirective.getMap()); 85 | 86 | }); 87 | 88 | this.updateBaseLayers(); 89 | 90 | } 91 | 92 | ngDoCheck() { 93 | this.updateBaseLayers(); 94 | } 95 | 96 | protected updateBaseLayers() { 97 | 98 | const map = this.leafletDirective.getMap(); 99 | const layersControl = this.controlLayers.getLayersControl(); 100 | 101 | if (null != map && null != layersControl && null != this.baseLayersDiffer) { 102 | const changes = this.baseLayersDiffer.diff(this.baseLayersValue); 103 | const results = this.controlLayers.applyBaseLayerChanges(changes); 104 | 105 | if (results.changed()) { 106 | this.syncBaseLayer(); 107 | } 108 | } 109 | 110 | } 111 | 112 | /** 113 | * Check the current base layer and change it to the new one if necessary 114 | */ 115 | protected syncBaseLayer() { 116 | 117 | const map = this.leafletDirective.getMap(); 118 | const layers = LeafletUtil.mapToArray(this.baseLayers); 119 | let foundLayer: Layer; 120 | 121 | // Search all the layers in the map to see if we can find them in the baselayer array 122 | map.eachLayer((l: Layer) => { 123 | foundLayer = layers.find((bl) => (l === bl)); 124 | }); 125 | 126 | // Did we find the layer? 127 | if (null != foundLayer) { 128 | // Yes - set the baselayer to the one we found 129 | this.baseLayer = foundLayer; 130 | } 131 | else { 132 | // No - set the baselayer to the first in the array and add it to the map 133 | if (layers.length > 0) { 134 | this.baseLayer = layers[0]; 135 | 136 | // Add layers outside of angular to prevent events from triggering change detection 137 | this.zone.runOutsideAngular(() => { 138 | this.baseLayer.addTo(map); 139 | }); 140 | } 141 | } 142 | 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ngx-leaflet-demo": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | }, 12 | "@schematics/angular:application": { 13 | "strict": true 14 | } 15 | }, 16 | "root": "", 17 | "sourceRoot": "src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular/build:application", 22 | "options": { 23 | "outputPath": { 24 | "base": "dist/ngx-leaflet-demo" 25 | }, 26 | "index": "src/index.html", 27 | "polyfills": [ 28 | "src/polyfills.ts" 29 | ], 30 | "tsConfig": "tsconfig.app.json", 31 | "inlineStyleLanguage": "scss", 32 | "assets": [ 33 | "src/favicon.ico", 34 | "src/assets", 35 | { 36 | "glob": "*.png", 37 | "input": "node_modules/leaflet/dist/images/", 38 | "output": "assets/leaflet/" 39 | } 40 | ], 41 | "styles": [ 42 | "src/styles.scss", 43 | "node_modules/bootstrap/dist/css/bootstrap.css", 44 | "node_modules/leaflet/dist/leaflet.css" 45 | ], 46 | "scripts": [], 47 | "browser": "src/main.ts" 48 | }, 49 | "configurations": { 50 | "production": { 51 | "budgets": [ 52 | { 53 | "type": "initial", 54 | "maximumWarning": "500kb", 55 | "maximumError": "1mb" 56 | }, 57 | { 58 | "type": "anyComponentStyle", 59 | "maximumWarning": "2kb", 60 | "maximumError": "4kb" 61 | } 62 | ], 63 | "fileReplacements": [ 64 | { 65 | "replace": "src/environments/environment.ts", 66 | "with": "src/environments/environment.prod.ts" 67 | } 68 | ], 69 | "outputHashing": "all" 70 | }, 71 | "development": { 72 | "optimization": false, 73 | "extractLicenses": false, 74 | "sourceMap": true, 75 | "namedChunks": true 76 | } 77 | }, 78 | "defaultConfiguration": "production" 79 | }, 80 | "serve": { 81 | "builder": "@angular/build:dev-server", 82 | "configurations": { 83 | "production": { 84 | "buildTarget": "ngx-leaflet-demo:build:production" 85 | }, 86 | "development": { 87 | "buildTarget": "ngx-leaflet-demo:build:development" 88 | } 89 | }, 90 | "defaultConfiguration": "development" 91 | }, 92 | "extract-i18n": { 93 | "builder": "@angular/build:extract-i18n", 94 | "options": { 95 | "buildTarget": "ngx-leaflet-demo:build" 96 | } 97 | }, 98 | "test": { 99 | "builder": "@angular/build:karma", 100 | "options": { 101 | "main": "src/test.ts", 102 | "polyfills": "src/polyfills.ts", 103 | "tsConfig": "tsconfig.spec.json", 104 | "karmaConfig": "karma.conf.js", 105 | "inlineStyleLanguage": "scss", 106 | "assets": [ 107 | "src/favicon.ico", 108 | "src/assets" 109 | ], 110 | "styles": [ 111 | "src/styles.scss" 112 | ], 113 | "scripts": [] 114 | } 115 | } 116 | } 117 | }, 118 | "ngx-leaflet": { 119 | "projectType": "library", 120 | "root": "projects/ngx-leaflet", 121 | "sourceRoot": "projects/ngx-leaflet/src", 122 | "prefix": "lib", 123 | "architect": { 124 | "build": { 125 | "builder": "@angular/build:ng-packagr", 126 | "options": { 127 | "project": "projects/ngx-leaflet/ng-package.json" 128 | }, 129 | "configurations": { 130 | "production": { 131 | "tsConfig": "projects/ngx-leaflet/tsconfig.lib.prod.json" 132 | }, 133 | "development": { 134 | "tsConfig": "projects/ngx-leaflet/tsconfig.lib.json" 135 | } 136 | }, 137 | "defaultConfiguration": "production" 138 | }, 139 | "test": { 140 | "builder": "@angular/build:karma", 141 | "options": { 142 | "main": "projects/ngx-leaflet/src/test.ts", 143 | "tsConfig": "projects/ngx-leaflet/tsconfig.spec.json", 144 | "karmaConfig": "projects/ngx-leaflet/karma.conf.js" 145 | } 146 | } 147 | } 148 | } 149 | }, 150 | "cli": { 151 | "analytics": false 152 | }, 153 | "schematics": { 154 | "@schematics/angular:component": { 155 | "type": "component" 156 | }, 157 | "@schematics/angular:directive": { 158 | "type": "directive" 159 | }, 160 | "@schematics/angular:service": { 161 | "type": "service" 162 | }, 163 | "@schematics/angular:guard": { 164 | "typeSeparator": "." 165 | }, 166 | "@schematics/angular:interceptor": { 167 | "typeSeparator": "." 168 | }, 169 | "@schematics/angular:module": { 170 | "typeSeparator": "." 171 | }, 172 | "@schematics/angular:pipe": { 173 | "typeSeparator": "." 174 | }, 175 | "@schematics/angular:resolver": { 176 | "typeSeparator": "." 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /projects/ngx-leaflet/src/lib/core/leaflet.directive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Directive, ElementRef, EventEmitter, HostListener, Input, NgZone, OnChanges, OnDestroy, OnInit, Output, 3 | SimpleChange 4 | } from '@angular/core'; 5 | 6 | import { latLng, LatLng, LatLngBounds, LeafletEvent, LeafletMouseEvent, map, Map, MapOptions } from 'leaflet'; 7 | 8 | import { LeafletUtil } from './leaflet.util'; 9 | 10 | @Directive({ 11 | selector: '[leaflet]', 12 | }) 13 | export class LeafletDirective 14 | implements OnChanges, OnDestroy, OnInit { 15 | 16 | readonly DEFAULT_ZOOM = 1; 17 | readonly DEFAULT_CENTER = latLng(38.907192, -77.036871); 18 | readonly DEFAULT_FPZ_OPTIONS = {}; 19 | 20 | resizeTimer: any; 21 | 22 | // Reference to the primary map object 23 | map: Map; 24 | 25 | @Input('leafletFitBoundsOptions') fitBoundsOptions = this.DEFAULT_FPZ_OPTIONS; 26 | @Input('leafletPanOptions') panOptions = this.DEFAULT_FPZ_OPTIONS; 27 | @Input('leafletZoomOptions') zoomOptions = this.DEFAULT_FPZ_OPTIONS; 28 | @Input('leafletZoomPanOptions') zoomPanOptions = this.DEFAULT_FPZ_OPTIONS; 29 | 30 | 31 | // Default configuration 32 | @Input('leafletOptions') options: MapOptions = {}; 33 | 34 | // Configure callback function for the map 35 | @Output('leafletMapReady') mapReady = new EventEmitter(); 36 | 37 | // Zoom level for the map 38 | @Input('leafletZoom') zoom: number; 39 | @Output('leafletZoomChange') zoomChange = new EventEmitter(); 40 | 41 | // Center of the map 42 | @Input('leafletCenter') center: LatLng; 43 | @Output('leafletCenterChange') centerChange = new EventEmitter(); 44 | 45 | // Set fit bounds for map 46 | @Input('leafletFitBounds') fitBounds: LatLngBounds; 47 | 48 | // Set the max bounds for the map 49 | @Input('leafletMaxBounds') maxBounds: LatLngBounds; 50 | 51 | // Set the min zoom for the map 52 | @Input('leafletMinZoom') minZoom: number; 53 | 54 | // Set the max zoom for the map 55 | @Input('leafletMaxZoom') maxZoom: number; 56 | 57 | 58 | // Mouse Map Events 59 | @Output('leafletClick') onClick = new EventEmitter(); 60 | @Output('leafletDoubleClick') onDoubleClick = new EventEmitter(); 61 | @Output('leafletMouseDown') onMouseDown = new EventEmitter(); 62 | @Output('leafletMouseUp') onMouseUp = new EventEmitter(); 63 | @Output('leafletMouseMove') onMouseMove = new EventEmitter(); 64 | @Output('leafletMouseOver') onMouseOver = new EventEmitter(); 65 | @Output('leafletMouseOut') onMouseOut = new EventEmitter(); 66 | 67 | // Map Move Events 68 | @Output('leafletMapMove') onMapMove = new EventEmitter(); 69 | @Output('leafletMapMoveStart') onMapMoveStart = new EventEmitter(); 70 | @Output('leafletMapMoveEnd') onMapMoveEnd = new EventEmitter(); 71 | 72 | // Map Zoom Events 73 | @Output('leafletMapZoom') onMapZoom = new EventEmitter(); 74 | @Output('leafletMapZoomStart') onMapZoomStart = new EventEmitter(); 75 | @Output('leafletMapZoomEnd') onMapZoomEnd = new EventEmitter(); 76 | 77 | constructor(private element: ElementRef, private zone: NgZone) { 78 | // Nothing here 79 | } 80 | 81 | ngOnInit() { 82 | 83 | // Create the map outside of angular so the various map events don't trigger change detection 84 | this.zone.runOutsideAngular(() => { 85 | 86 | // Create the map with some reasonable defaults 87 | this.map = map(this.element.nativeElement, this.options); 88 | this.addMapEventListeners(); 89 | 90 | }); 91 | 92 | // Only setView if there is a center/zoom 93 | if (null != this.center && null != this.zoom) { 94 | this.setView(this.center, this.zoom); 95 | } 96 | 97 | // Set up all the initial settings 98 | if (null != this.fitBounds) { 99 | this.setFitBounds(this.fitBounds); 100 | } 101 | 102 | if (null != this.maxBounds) { 103 | this.setMaxBounds(this.maxBounds); 104 | } 105 | 106 | if (null != this.minZoom) { 107 | this.setMinZoom(this.minZoom); 108 | } 109 | 110 | if (null != this.maxZoom) { 111 | this.setMaxZoom(this.maxZoom); 112 | } 113 | 114 | this.doResize(); 115 | 116 | // Fire map ready event 117 | this.mapReady.emit(this.map); 118 | 119 | } 120 | 121 | ngOnChanges(changes: { [key: string]: SimpleChange }) { 122 | 123 | /* 124 | * The following code is to address an issue with our (basic) implementation of 125 | * zooming and panning. From our testing, it seems that a pan operation followed 126 | * by a zoom operation in the same thread will interfere with each other. The zoom 127 | * operation interrupts/cancels the pan, resulting in a final center point that is 128 | * inaccurate. The solution seems to be to either separate them with a timeout or 129 | * to collapse them into a setView call. 130 | */ 131 | 132 | // Zooming and Panning 133 | if (changes['zoom'] && changes['center'] && null != this.zoom && null != this.center) { 134 | this.setView(changes['center'].currentValue, changes['zoom'].currentValue); 135 | } 136 | // Set the zoom level 137 | else if (changes['zoom']) { 138 | this.setZoom(changes['zoom'].currentValue); 139 | } 140 | // Set the map center 141 | else if (changes['center']) { 142 | this.setCenter(changes['center'].currentValue); 143 | } 144 | 145 | // Other options 146 | if (changes['fitBounds']) { 147 | this.setFitBounds(changes['fitBounds'].currentValue); 148 | } 149 | 150 | if (changes['maxBounds']) { 151 | this.setMaxBounds(changes['maxBounds'].currentValue); 152 | } 153 | 154 | if (changes['minZoom']) { 155 | this.setMinZoom(changes['minZoom'].currentValue); 156 | } 157 | 158 | if (changes['maxZoom']) { 159 | this.setMaxZoom(changes['maxZoom'].currentValue); 160 | } 161 | 162 | } 163 | 164 | ngOnDestroy() { 165 | // If this directive is destroyed, the map is too 166 | if (null != this.map) { 167 | this.map.remove(); 168 | } 169 | } 170 | 171 | public getMap() { 172 | return this.map; 173 | } 174 | 175 | 176 | @HostListener('window:resize', []) 177 | onResize() { 178 | this.delayResize(); 179 | } 180 | 181 | private addMapEventListeners() { 182 | 183 | const registerEventHandler = (eventName: string, handler: (e: LeafletEvent) => any) => { 184 | this.map.on(eventName, handler); 185 | }; 186 | 187 | 188 | // Add all the pass-through mouse event handlers 189 | registerEventHandler('click', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onClick, e)); 190 | registerEventHandler('dblclick', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onDoubleClick, e)); 191 | registerEventHandler('mousedown', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onMouseDown, e)); 192 | registerEventHandler('mouseup', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onMouseUp, e)); 193 | registerEventHandler('mouseover', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onMouseOver, e)); 194 | registerEventHandler('mouseout', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onMouseOut, e)); 195 | registerEventHandler('mousemove', (e: LeafletMouseEvent) => LeafletUtil.handleEvent(this.zone, this.onMouseMove, e)); 196 | 197 | registerEventHandler('zoomstart', (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onMapZoomStart, e)); 198 | registerEventHandler('zoom', (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onMapZoom, e)); 199 | registerEventHandler('zoomend', (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onMapZoomEnd, e)); 200 | registerEventHandler('movestart', (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onMapMoveStart, e)); 201 | registerEventHandler('move', (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onMapMove, e)); 202 | registerEventHandler('moveend', (e: LeafletEvent) => LeafletUtil.handleEvent(this.zone, this.onMapMoveEnd, e)); 203 | 204 | 205 | // Update any things for which we provide output bindings 206 | const outputUpdateHandler = () => { 207 | const zoom = this.map.getZoom(); 208 | if (zoom !== this.zoom) { 209 | this.zoom = zoom; 210 | LeafletUtil.handleEvent(this.zone, this.zoomChange, zoom); 211 | } 212 | 213 | const center = this.map.getCenter(); 214 | if (null != center || null != this.center) { 215 | 216 | if (((null == center || null == this.center) && center !== this.center) 217 | || (center.lat !== this.center.lat || center.lng !== this.center.lng)) { 218 | 219 | this.center = center; 220 | LeafletUtil.handleEvent(this.zone, this.centerChange, center); 221 | 222 | } 223 | } 224 | }; 225 | 226 | registerEventHandler('moveend', outputUpdateHandler); 227 | registerEventHandler('zoomend', outputUpdateHandler); 228 | } 229 | 230 | /** 231 | * Resize the map to fit it's parent container 232 | */ 233 | private doResize() { 234 | 235 | // Run this outside of angular so the map events stay outside of angular 236 | this.zone.runOutsideAngular(() => { 237 | 238 | // Invalidate the map size to trigger it to update itself 239 | if (null != this.map) { 240 | this.map.invalidateSize({}); 241 | } 242 | 243 | }); 244 | 245 | } 246 | 247 | /** 248 | * Manage a delayed resize of the component 249 | */ 250 | private delayResize() { 251 | if (null != this.resizeTimer) { 252 | clearTimeout(this.resizeTimer); 253 | } 254 | this.resizeTimer = setTimeout(this.doResize.bind(this), 200); 255 | } 256 | 257 | 258 | /** 259 | * Set the view (center/zoom) all at once 260 | * @param center The new center 261 | * @param zoom The new zoom level 262 | */ 263 | private setView(center: LatLng, zoom: number) { 264 | 265 | if (null != this.map && null != center && null != zoom) { 266 | this.map.setView(center, zoom, this.zoomPanOptions); 267 | } 268 | 269 | } 270 | 271 | /** 272 | * Set the map zoom level 273 | * @param zoom the new zoom level for the map 274 | */ 275 | private setZoom(zoom: number) { 276 | 277 | if (null != this.map && null != zoom) { 278 | this.map.setZoom(zoom, this.zoomOptions); 279 | } 280 | 281 | } 282 | 283 | /** 284 | * Set the center of the map 285 | * @param center the center point 286 | */ 287 | private setCenter(center: LatLng) { 288 | 289 | if (null != this.map && null != center) { 290 | this.map.panTo(center, this.panOptions); 291 | } 292 | 293 | } 294 | 295 | /** 296 | * Fit the map to the bounds 297 | * @param latLngBounds the boundary to set 298 | */ 299 | private setFitBounds(latLngBounds: LatLngBounds) { 300 | 301 | if (null != this.map && null != latLngBounds) { 302 | this.map.fitBounds(latLngBounds, this.fitBoundsOptions); 303 | } 304 | 305 | } 306 | 307 | /** 308 | * Set the map's max bounds 309 | * @param latLngBounds the boundary to set 310 | */ 311 | private setMaxBounds(latLngBounds: LatLngBounds) { 312 | 313 | if (null != this.map && null != latLngBounds) { 314 | this.map.setMaxBounds(latLngBounds); 315 | } 316 | 317 | } 318 | 319 | /** 320 | * Set the map's min zoom 321 | * @param number the new min zoom 322 | */ 323 | private setMinZoom(zoom: number) { 324 | 325 | if (null != this.map && null != zoom) { 326 | this.map.setMinZoom(zoom); 327 | } 328 | 329 | } 330 | 331 | /** 332 | * Set the map's min zoom 333 | * @param number the new min zoom 334 | */ 335 | private setMaxZoom(zoom: number) { 336 | 337 | if (null != this.map && null != zoom) { 338 | this.map.setMaxZoom(zoom); 339 | } 340 | 341 | } 342 | 343 | } 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @bluehalo/ngx-leaflet 2 | 3 | [![Build Status][travis-image]][travis-url] 4 | 5 | [travis-url]: https://travis-ci.org/Asymmetrik/ngx-leaflet/ 6 | [travis-image]: https://travis-ci.org/Asymmetrik/ngx-leaflet.svg?branch=master 7 | 8 | > Leaflet packages for Angular.io. 9 | > Provides flexible and extensible components for integrating Leaflet v0.7.x and v1.x into Angular.io projects. 10 | 11 | ## Table of Contents 12 | - [Install](#install) 13 | - [Usage](#usage) 14 | - [API](#api) 15 | - [Extensions](#extensions) 16 | - [Getting Help](#help) 17 | - [Contribute](#contribute) 18 | - [License](#license) 19 | - [Credits](#credits) 20 | 21 | 22 | 23 | ## Install 24 | Install the package and its peer dependencies via npm (or yarn): 25 | ``` 26 | npm install leaflet 27 | npm install @bluehalo/ngx-leaflet 28 | ``` 29 | 30 | If you intend to use this library in a typescript project (utilizing the typings), you'll need to install the leaflet typings: 31 | ``` 32 | npm install --save-dev @types/leaflet 33 | ``` 34 | 35 | If you want to run the demo, clone the repository, perform an ```npm install```, ```npm run demo``` and then go to http://localhost:4200 36 | 37 | Not using the latest version of Angular.io? Have a look in [CHANGES.md](/CHANGES.md) to find the right version for your project. 38 | 39 | ## Usage 40 | > NOTE: We've simplified the getting started instructions to be more targeted at the most recent versions of Angular.io and the use of Angular CLI. 41 | 42 | Generally, the steps are: 43 | 44 | * Install Leaflet, this library, and potentially the Leaflet typings (see above). 45 | * Import the Leaflet stylesheet 46 | * Import the `LeafletDirective` etc. into your Module or standalone Component 47 | * Create and configure a map (see docs below and/or demo) 48 | 49 | Alternatively, you can use the `LeafletModule` to import the module into your application. 50 | 51 | ### Import the Leaflet Stylesheet 52 | For leaflet to work, you need to have the leaflet stylesheets loaded into your application. 53 | If you've installed via npm, you will need to load ```./node_modules/leaflet/dist/leaflet.css```. 54 | How you include the stylesheet will depend on your specific setup. Here are a few examples: 55 | 56 | #### Direct Import from HTML 57 | If you are just building a webpage and not using a bundler for your css, you'll want to directly import the css file in your HTML page. 58 | 59 | ```angular181html 60 | 61 | ... 62 | 63 | ... 64 | 65 | ``` 66 | 67 | #### Adding Styles in Angular CLI 68 | If you are using Angular CLI, you will need to add the Leaflet CSS file to the styles array contained in ```angular.json``` 69 | 70 | ```json 71 | { 72 | ... 73 | "styles": [ 74 | "styles.css", 75 | "./node_modules/leaflet/dist/leaflet.css" 76 | ], 77 | ... 78 | } 79 | ``` 80 | 81 | #### A Note About Markers 82 | Leaflet marker URLs don't play well with the Angular CLI build pipeline without some special handling. 83 | The demo contained in this project demonstrates how to get around this problem. Here is a rough overview of the steps taken to get them working. 84 | 85 | 1. Include the leaflet marker assets so they are copied intact to the build output. 86 | ```json 87 | { 88 | ... 89 | "assets": [ 90 | { 91 | "glob": "**/*", 92 | "input": "public" 93 | }, 94 | { 95 | "glob": "**/*", 96 | "input": "./node_modules/leaflet/dist/images", 97 | "output": "assets/" 98 | } 99 | ], 100 | ... 101 | } 102 | ``` 103 | 104 | 1. Configure Leaflet to use the asset URLs as custom marker images. 105 | 106 | ```typescript 107 | let layer = marker([ 46.879966, -121.726909 ], { 108 | icon: icon({ 109 | ...Icon.Default.prototype.options, 110 | iconUrl: 'assets/marker-icon.png', 111 | iconRetinaUrl: 'assets/marker-icon-2x.png', 112 | shadowUrl: 'assets/marker-shadow.png' 113 | }) 114 | }); 115 | ``` 116 | 117 | 118 | ### Import LeafletDirective 119 | 120 | Before you can use the Leaflet components in your Angular.io app, you'll need to import it in your application. 121 | Depending on if you're using standalone mode or not, you will import it into your modules and/or components. 122 | 123 | ```typescript 124 | import { LeafletDirective } from '@bluehalo/ngx-leaflet'; 125 | 126 | @Component({ 127 | imports: [ 128 | LeafletDirective // Import the LeafletDirective here 129 | // import other directives as needed 130 | ], 131 | }) 132 | ``` 133 | 134 | Alternatively: 135 | ```typescript 136 | import { LeafletModule } from '@bluehalo/ngx-leaflet'; 137 | 138 | @NgModule({ 139 | imports: [ 140 | LeafletModule // Import the LeafletModule here 141 | ], 142 | }) 143 | ``` 144 | 145 | ### Create and Configure a Map 146 | To get a basic map to work, you have to: 147 | 148 | * Apply the ```leaflet``` attribute directive (see the example below) to an existing DOM element. 149 | * Style the map DOM element with a height. Otherwise, it'll render with a 0 pixel height. 150 | * Provide an initial zoom/center and set of layers either via ```leafletOptions``` or by binding to ```leafletZoom```, ```leafletCenter```, and ```leafletLayers```. 151 | 152 | Template: 153 | ```angular181html 154 |
157 |
158 | ``` 159 | 160 | Example leafletOptions object: 161 | ```typescript 162 | options = { 163 | layers: [ 164 | tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '...' }) 165 | ], 166 | zoom: 5, 167 | center: latLng(46.879966, -121.726909) 168 | }; 169 | ``` 170 | 171 | Changes to leafletOptions are ignored after they are initially set. 172 | This is because these options are passed into the map constructor, so they can't be changed anyways. 173 | So, make sure the object exists before the map is created. 174 | You'll want to create the object in ```ngOnInit``` or hide the map DOM element with ```@if``` until you can create the options object. 175 | 176 | 177 | ### Add a Layers Control 178 | The ```[leafletLayersControl]``` input bindings give you the ability to add the layers control to the map. 179 | The layers control lets the user toggle layers and overlays on and off. 180 | 181 | Template: 182 | ```angular181html 183 |
187 |
188 | ``` 189 | 190 | Component with example layersControl object: 191 | ```typescript 192 | import { LeafletDirective, LeafletLayersControlDirective } from '@bluehalo/ngx-leaflet'; 193 | 194 | @Component({ 195 | imports: [ 196 | LeafletDirective, 197 | LeafletLayersControlDirective, 198 | ], 199 | }) 200 | export class MyComponent { 201 | protected readonly layersControl = { 202 | baseLayers: { 203 | 'Open Street Map': tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '...' }), 204 | 'Open Cycle Map': tileLayer('https://{s}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png', { maxZoom: 18, attribution: '...' }) 205 | }, 206 | overlays: { 207 | 'Big Circle': circle([ 46.95, -122 ], { radius: 5000 }), 208 | 'Big Square': polygon([[ 46.8, -121.55 ], [ 46.9, -121.55 ], [ 46.9, -121.7 ], [ 46.8, -121.7 ]]) 209 | } 210 | } 211 | } 212 | ``` 213 | 214 | You can add any kind of Leaflet layer you want to the ```overlays``` map. 215 | This includes markers, shapes, geojson, custom layers from other libraries, etc. 216 | 217 | 218 | ### Add Custom Layers (base layers, markers, shapes, etc.) 219 | There are several different ways to add layers to the map. 220 | You can add layers (baselayers, markers, or custom layers) to the map without showing them in the layer control using the ```[leafletLayers]``` directive. 221 | 222 | Template: 223 | ```angular181html 224 |
228 |
229 | ``` 230 | 231 | Layers array: 232 | ```typescript 233 | layers = [ 234 | circle([ 46.95, -122 ], { radius: 5000 }), 235 | polygon([[ 46.8, -121.85 ], [ 46.92, -121.92 ], [ 46.87, -121.8 ]]), 236 | marker([ 46.879966, -121.726909 ]) 237 | ]; 238 | ``` 239 | 240 | You can also add an individual layer to the map using the ```[leafletLayer]``` directive. 241 | Using this approach allows you to use ```@for``` and ```@if``` to control whether individual layers are added to or removed from the map. 242 | 243 | Template: 244 | ```angular181html 245 |
248 | 249 | @if (showLayer) { 250 |
251 | } 252 |
253 | ``` 254 | 255 | Layer: 256 | ```typescript 257 | layer = circle([ 46.95, -122 ], { radius: 5000 }); 258 | ``` 259 | 260 | 261 | ### Dynamically Change Map Layers using [leafletLayers] 262 | 263 | > **Layer inputs (arrays and maps) are mutable** 264 | > Previous versions of this plugin treated layers arrays and layer control objects as immutable data structures. 265 | > We've changed that behavior. 266 | > Now, mutable changes to the ```leafletLayers```, ```leafletBaseLayers```, and ```leafletLayersControl``` inputs are detected. 267 | 268 | The plugin is now using internal ngx iterable and key/value differs to detect and track changes to mutable data structures. 269 | This approach requires a deep compare of the contents of the data structure (which can be slow when the contents are really big). 270 | For immutable data structures, all that is needed is a top-level instance equality check (which is way faster). 271 | This change is backwards compatible and was motivated by feedback and confusion. 272 | While there is a performance impact for some use cases, this approach is more intuitive. 273 | 274 | There are at least two good approaches to improving performance when there are a lot of layers bound to the map. 275 | First, you can use the OnPush change detection strategy. There's an example of this in the demo. 276 | Second, you can wrap a large number of layers into a Leaflet layer group, which will reduce the number of layers the plugin actually has to track during diffs. 277 | 278 | 279 | ### Working with Leaflet Events 280 | Often, you'll want to make changes based on a map click or other Leaflet interaction. 281 | The ngx-leaflet plugin supports several [map events](#map-events) and [layer events](#layer-events) as documented in the API section. 282 | 283 | You may occasionally need to handle events that aren't exposed through the plugin, however. 284 | When that happens, you will need to be aware of how Zones and change detection work to ensure your event handling works as expected. 285 | Take a look at [A Note About Change Detection](#a-note-about-change-detection) for more details. 286 | This is by design and a common thing to deal with when using third party libraries and Angular. 287 | 288 | 289 | ## API 290 | This section includes more detailed documentation of the functionality of the directives included in this library. 291 | 292 | ### Advanced Map Configuration 293 | There are several input bindings available for configuring the map. 294 | 295 | ```angular181html 296 |
302 |
303 | ``` 304 | 305 | #### [leafletOptions] 306 | Input binding for the initial leaflet map options (see [Leaflet's](https://leafletjs.com/SlavaUkraini/reference.html#map-option) docs). These options can only be set initially because they are used to create the map. Later changes are ignored. 307 | 308 | #### [leafletPanOptions] 309 | Input binding for pan options (see [Leaflet's](https://leafletjs.com/SlavaUkraini/reference.html#pan-options) docs). These options are stored and used whenever pan operations are invoked. 310 | 311 | #### [leafletZoomOptions] 312 | Input binding for zoom options (see [Leaflet's](https://leafletjs.com/SlavaUkraini/reference.html#zoom-options) docs). These options are stored and used whenever zoom operations are invoked. 313 | 314 | #### [leafletZoomPanOptions] 315 | Input binding for zoom/pan options (see [Leaflet's](https://leafletjs.com/SlavaUkraini/reference.html#zoom/pan-options) docs). These options are stored and used whenever zoom/pan operations are invoked. 316 | 317 | #### [leafletFitBoundsOptions] 318 | Input binding for FitBounds options (see [Leaflet's](https://leafletjs.com/SlavaUkraini/reference.html#fitbounds-options) docs). These options are stored and used whenever FitBounds operations are invoked. 319 | 320 | 321 | ### Dynamically changing zoom level, center, fitBounds, etc. 322 | ```angular181html 323 |
328 |
329 | ``` 330 | 331 | #### [(leafletZoom)]: number 332 | Input and Output binding for the map zoom level. 333 | 334 | #### [leafletMaxZoom]: number 335 | Input binding for the maximum zoom level for the map. 336 | 337 | #### [leafletMinZoom]: number 338 | Input binding for the minimum zoom level for the map. 339 | 340 | #### [(leafletCenter)]: LatLng 341 | Input and Output binding for the map center position. 342 | 343 | #### Note: center/zoom operations may interfere with each other 344 | Zoom/Center operations that are applied in rapid succession may interfere with or cancel each other. 345 | If both changes are picked up at the same time, the component applies the changes as a map.setView() operation to ensure both are processed. 346 | Additionally, if a zoom level or center is applied that is not allowed (e.g., beyond max zoom level or outside of max bounds), Leaflet will determine the new value. 347 | 348 | #### [leafletFitBounds]: LatLngBounds 349 | Input bind a ```LatLngBounds``` value that will be applied to the map using ```Map.setFitBounds()```. 350 | This operation has no output binding because the input fitBounds usually results in a slightly different map bounds. 351 | 352 | #### [leafletMaxBounds]: LatLngBounds 353 | Input bind a ```LatLngBounds``` value that will be applied to the map using ```Map.setMaxBounds()```. 354 | 355 | 356 | ### Simple Layer Management: Setting Baselayers 357 | There is a convenience input binding for setting the baselayers on the map called ```[leafletBaseLayers]```. 358 | You can also provide ```[leafletLayersControlOptions]``` if you want to show the control on the map that allows you to switch between baselayers. 359 | If you plan to show more than just baselayers, you should use the more advanced layers controls described in *Advanced Layer Management* below. 360 | 361 | For an example of the basic map setup, you should check out the *Simple Base Layers* demo. 362 | 363 | ```angular181html 364 |
368 |
369 | ``` 370 | 371 | #### [leafletBaseLayers]: Control.LayersObject 372 | Input bind an ```Control.LayersObject``` to be synced to the map. 373 | 374 | ```typescript 375 | baseLayers: { 376 | 'layer1': Layer, 377 | 'layer2': Layer 378 | } 379 | ``` 380 | 381 | On changes, the component syncs the baseLayers on the map with the layers in this object. 382 | Syncing is performed by tracking the current baselayer and on changes, searching the map to see if any of the current baselayers is added to the map. 383 | If it finds a baselayer that is still added to the map, it will assume that is still the baselayer and leave it. 384 | If none of the baselayers can be found on the map, it will add the first layer it finds in the ```Control.LayersObject``` and use that as the new baselayer. 385 | Layers are compared using instance equality. 386 | 387 | If you use this directive, you can still manually use the ```[leafletLayers]``` directive, but you will not be able to use the ```[leafletLayersControl]``` directive. 388 | This directive internally uses the layers control, so if you add both, they'll interfere with each other. 389 | Because it uses ```control.layers``` under the hood, you can still provide options for the layers control. 390 | 391 | 392 | #### [leafletLayersControlOptions] 393 | Input binding for Control.Layers options (see [Leaflet's](https://leafletjs.com/SlavaUkraini) docs). 394 | These options are passed into the layers control constructor on creation. 395 | 396 | 397 | ### Advanced Layer Management: Layers, and Layers Control 398 | The ```[leafletLayers]``` and ```[leafletLayersControl]``` input bindings give you direct access to manipulate layers and the layers control. 399 | When the array bound to ```[leafletLayers]``` is changed, the directive will synchronize the layers on the map to the layers in the array. 400 | This includes tile layers and any added shapes. 401 | 402 | The ```[leafletLayersControl]``` input binding allows you to provide a set of base layers and overlay layers that can be managed within leaflet using the layers control. 403 | When the user manipulates the control via Leaflet, Leaflet will automatically manage the layers, but the input bound layer array isn't going to get updated to reflect those changes. 404 | 405 | So, use ```[leafletLayers]``` to add a collection of layers to the map. 406 | And, use ```[leafletLayersControl]``` to allow users to optionally turn layers/overlays on and off. 407 | 408 | For an example of using the layers controls, you should check out the *Layers and Layer Controls* demo. 409 | 410 | ```angular181html 411 |
416 |
417 | ``` 418 | 419 | #### [leafletLayers]: Layer[] 420 | Input bind an array of all layers to be synced (and made visible) in the map. 421 | 422 | On changes, the component syncs the layers on the map with the layers in this array. 423 | Syncing is performed by selectively adding or removing layers. 424 | Layers are compared using instance equality. 425 | As a result of how the map is synced, the order of layers is not guaranteed to be consistent as changes are made. 426 | 427 | 428 | #### [leafletLayersControl]: Control.Layers 429 | Input bind a Control.Layers specification. The object contains properties for each of the two constructor arguments for the Control.Layers constructor. 430 | 431 | ```typescript 432 | layersControl: { 433 | baseLayers: { 434 | 'layerName': Layer 435 | }, 436 | overlays: { 437 | 'overlayName': Layer 438 | } 439 | } 440 | ``` 441 | 442 | #### [leafletLayersControlOptions] 443 | Input binding for Control.Layers options (see [Leaflet's](https://leafletjs.com/SlavaUkraini) docs). 444 | These options are passed into the constructor on creation. 445 | 446 | 447 | ### Advanced Layer Management: Individual Layers and @for / @if 448 | The ```[leafletLayer]``` input bindings gives you the ability to add a single layer to the map. 449 | While this may seem limiting, you can nest elements inside the map element, each with a ```[leafletLayer]``` input. 450 | The result of this is that each layer will be added to the map. 451 | If you add a structural directive - ```@for``` or ```@if``` - you can get some added flexibility when controlling layers. 452 | 453 | ```angular181html 454 |
456 | 457 | @for (layer of layers; track layer.id) { 458 |
459 | } 460 | 461 |
462 | ``` 463 | 464 | In this example, each layer in the ```layers``` array will create a new child ```div``` element. 465 | Each element will have a ```[leafletLayer]``` input binding, which will result in the layer being added to the map. 466 | For more details, you should check out the *Layers and ngFor* demo. 467 | 468 | There are several layer events that are available when you are using this approach to controlling layers. 469 | 470 | ### Layer Events 471 | When you are using the ```[leafletLayer]``` directive to add a layer, you can also access output bindings for layer events. 472 | Two events that are currently exposed include: ```(leafletLayerAdd)``` and ```(leafletLayerRemove)```. 473 | Each of these emits a ```LeafletEvent``` object. 474 | 475 | 476 | ### Map Events 477 | Leaflet exposes a lot of map events including map zoom, map move, and mouse interactions. 478 | The plugin exposes several of the most common events. 479 | For each of these events, the event is emitted in the Angular Zone, so you shouldn't have to do anything extra to get change detection to work. 480 | For a working example, check out the events section of the demo. 481 | 482 | #### Mouse Interactions: LeafletMouseEvent 483 | The following events are provided: 484 | * ```(leafletClick)``` 485 | * ```(leafletDoubleClick)``` 486 | * ```(leafletMouseDown)``` 487 | * ```(leafletMouseUp)``` 488 | * ```(leafletMouseMove)``` 489 | * ```(leafletMouseOver)``` 490 | * ```(leafletMouseOut)``` 491 | 492 | #### Map Zoom and Move: LeafletEvent 493 | The following events are provided: 494 | * ```(leafletMapMove)``` 495 | * ```(leafletMapMoveStart)``` 496 | * ```(leafletMapMoveEnd)``` 497 | * ```(leafletMapZoom)``` 498 | * ```(leafletMapZoomStart)``` 499 | * ```(leafletMapZoomEnd)``` 500 | 501 | 502 | 503 | ### Getting a Reference to the Map 504 | Occasionally, you may need to directly access the Leaflet map instance. 505 | For example, to call ```invalidateSize()``` when the map div changes size or is shown/hidden. 506 | There are a couple of different ways to achieve this depending on what you're trying to do. 507 | 508 | The easiest and most flexible way is to use the output binding ```leafletMapReady```. 509 | This output is invoked after the map is created, the argument of the event being the ```Map``` instance. 510 | 511 | The second is to get a reference to the leaflet directive itself - and there are a couple of ways to do this. 512 | With a reference to the directive, you can invoke the ```getMap()``` function to get a reference to the ```Map``` instance. 513 | 514 | 515 | #### (leafletMapReady): Map 516 | This output is emitted when once when the map is initially created inside of the Leaflet directive. 517 | The event will only fire when the map exists and is ready for manipulation. 518 | 519 | ```angular181html 520 |
523 |
524 | ``` 525 | 526 | ```typescript 527 | onMapReady(map: Map) { 528 | // Do stuff with map 529 | } 530 | ``` 531 | 532 | This method of getting the map makes the most sense if you are using the Leaflet directive inside your own component 533 | and just need to add some limited functionality or register some event handlers. 534 | 535 | 536 | #### Inject LeafletDirective into your Component 537 | This is the more advanced technique and it won't always work depending on your setup. 538 | In particular, this will likely not work unless you are writing your own third-party library that extends the functionality of `ngx-leaflet`. 539 | If this approach does not work for you, try using the `leafletMapReady` event described above. 540 | 541 | In Angular.io, directives are injectable the same way that Services are. 542 | This means that you can create your own component or directive and inject the ```LeafletDirective``` into it. 543 | This will only work if your custom component/directive exists on the same DOM element and is ordered after the injected LeafletDirective, or if it is on a child DOM element. 544 | 545 | 546 | ```angular181html 547 | 548 |
549 | 550 | 551 |
552 |
553 |
554 | ``` 555 | 556 | ```typescript 557 | 558 | @Directive({ 559 | selector: '[myCustomDirective]' 560 | }) 561 | export class MyCustomDirective { 562 | readonly #leafletDirective = inject(LeafletDirective); 563 | 564 | someFunction() { 565 | if (null !== this.#leafletDirective.getMap()) { 566 | // Do stuff with the map 567 | } 568 | } 569 | } 570 | ``` 571 | 572 | The benefit of this approach is it's a bit cleaner if you're interested in adding some reusable capability to the existing leaflet map directive. 573 | As mentioned above, it might not work depending on how you are packaging your component. 574 | This is how the ```@bluehalo/ngx-leaflet-draw``` and ```@bluehalo/ngx-leaflet-d3``` packages work, so you can use them as references. 575 | 576 | 577 | ### A Note About Change Detection 578 | Change detection is at the core of how Angular works. 579 | Angular.io uses Zone.js to scope how and when (events, actions, etc.) to trigger change detection. 580 | It's important to scope it carefully because change detection can be fairly expensive, so you don't want it to happen constantly. 581 | 582 | Libraries like ngx-leaflet have to decide what to do inside and outside of the Angular zone, balancing convenience and performance. 583 | Leaflet registers handlers for a lot of mouse events. 584 | To mitigate the performance impact of constantly running change detection on all mouse events (including mousemove), ngx-leaflet runs most of the Leaflet code outside of the Angular zone. 585 | The impact of this is that Angular won't automatically detect changes that you make inside of a Leaflet event callback. 586 | 587 | The solution is to either make sure that Angular relevant changes are made inside of Angular's zone or to manually tell Angular to detect changes. 588 | 589 | #### Running Inside of Angular's Zone 590 | Leaflet event handlers run outside of Angular's zone, where changes to input bound fields will not be detected automatically. 591 | To ensure your changes are detected and applied, you need to make those changed inside of Angular's zone. 592 | Fortunately, this is extremely easy. 593 | 594 | ```typescript 595 | fitBounds: any = null; 596 | circle = circle([ 46.95, -122 ], { radius: 5000 }); 597 | 598 | // Inject the Change Detector into your component 599 | constructor(private zone: NgZone) {} 600 | 601 | ngOnInit() { 602 | 603 | // The 'add' event callback handler happens outside of the Angular zone 604 | this.circle.on('add', () => { 605 | 606 | // But, we can run stuff inside of Angular's zone by calling NgZone.run() 607 | // everything inside the arrow function body happens inside of Angular's zone, where changes will be detected 608 | this.zone.run(() => { 609 | this.fitBounds = this.circle.getBounds(); 610 | }); 611 | 612 | }); 613 | } 614 | ``` 615 | 616 | #### Manually Triggering Change Detection 617 | Another option is to manually tell the change detector to detect changes. 618 | The drawback to this option is that it is less precise. 619 | This will trigger change detection for this component and all of its children. 620 | 621 | ```typescript 622 | fitBounds: any = null; 623 | circle = circle([ 46.95, -122 ], { radius: 5000 }); 624 | 625 | // Inject the Change Detector into your component 626 | constructor(private changeDetector: ChangeDetectorRef) {} 627 | 628 | ngOnInit() { 629 | 630 | // The 'add' event callback happens outside of the Angular zone 631 | this.circle.on('add', () => { 632 | 633 | // Because we're outside of Angular's zone, this change won't be detected 634 | this.fitBounds = this.circle.getBounds(); 635 | 636 | // But, it will if we tell Angular to detect changes 637 | this.changeDetector.detectChanges(); 638 | 639 | }); 640 | } 641 | ``` 642 | 643 | ## Extensions 644 | There are several libraries that extend the core functionality of ngx-leaflet: 645 | * [Leaflet Draw](https://github.com/BlueHalo/ngx-leaflet-draw) 646 | * [Leaflet Markercluster](https://github.com/BlueHalo/ngx-leaflet-markercluster) 647 | * [Leaflet D3 (Hexbins)](https://github.com/BlueHalo/ngx-leaflet-d3) 648 | 649 | 650 | ## Getting Help 651 | Here's a list of articles, tutorials, guides, and help resources: 652 | * [ngx-leaflet on Stack Overflow](https://stackoverflow.com/questions/tagged/ngx-leaflet) 653 | * [High-level intro to @bluehalo/ngx-leaflet](https://github.com/BlueHalo/ngx-leaflet/wiki) 654 | * [Using @bluehalo/ngx-leaflet in Angular CLI projects](https://github.com/BlueHalo/ngx-leaflet/wiki/Getting-Started-Tutorial) 655 | * [Integrating 3rd Party Leaflet Libraries with @bluehalo/ngx-leaflet and @angular/cli](https://github.com/BlueHalo/ngx-leaflet/wiki/Integrating-Plugins) 656 | 657 | 658 | ## Contribute 659 | PRs accepted. If you are part of BlueHalo, please make contributions on feature branches off of the ```develop``` branch. If you are outside of BlueHalo, please fork our repo to make contributions. 660 | 661 | 662 | ## License 663 | See LICENSE in repository for details. 664 | 665 | 666 | ## Credits 667 | **[Leaflet](http://leafletjs.com/)** Is an awesome mapping package. 668 | --------------------------------------------------------------------------------