leaflet-heatmap-layer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 leaflet-heatmap-layer contributors
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.
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # leaflet-heatmap-layer
2
+
3
+ A modern heatmap layer plugin for [Leaflet](https://leafletjs.com/).
4
+
5
+ This Plugin is inspired by [Leaflet.heat](https://github.com/Leaflet/Leaflet.heat), built from scratch under the MIT license. It fixes some long-standing bugs and ships as a proper ES module with TypeScript declarations.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install leaflet-heatmap-layer
11
+ ```
12
+
13
+ Or include via script tag (UMD build):
14
+
15
+ ```html
16
+ <script src="https://unpkg.com/leaflet-heatmap-layer/dist/leaflet-heatmap-layer.umd.js"></script>
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```js
22
+ import L from 'leaflet';
23
+ import 'leaflet-heatmap-layer';
24
+
25
+ const map = L.map('map').setView([51.505, -0.09], 13);
26
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
27
+
28
+ const points = [
29
+ [51.5, -0.09, 0.8],
30
+ [51.51, -0.1, 0.5],
31
+ [51.49, -0.08, 1.0],
32
+ // ...
33
+ ];
34
+
35
+ L.heatLayer(points, { radius: 25 }).addTo(map);
36
+ ```
37
+
38
+ You can also use named exports directly:
39
+
40
+ ```js
41
+ import { HeatLayer, heatLayer } from 'leaflet-heatmap-layer';
42
+ ```
43
+
44
+ ## Options
45
+
46
+ | Option | Type | Default | Description |
47
+ |---|---|---|---|
48
+ | `radius` | `number` | `25` | Radius of each heat point in pixels |
49
+ | `blur` | `number` | `15` | Amount of blur in pixels |
50
+ | `minOpacity` | `number` | `0.05` | Minimum opacity of the heatmap |
51
+ | `maxZoom` | `number` | map's maxZoom | Zoom level where points reach full intensity |
52
+ | `max` | `number` | *auto* | Manual intensity cap. When omitted, computed automatically per redraw |
53
+ | `gradient` | `object` | `{0.4:'blue', 0.6:'cyan', 0.7:'lime', 0.8:'yellow', 1:'red'}` | Color gradient stops |
54
+ | `zoomCrossfadeDuration` | `number` | `250` | Duration in ms to crossfade between zoom levels (0 to disable) |
55
+
56
+ ## Methods
57
+
58
+ | Method | Returns | Description |
59
+ |---|---|---|
60
+ | `setLatLngs(points)` | `this` | Replace all data points and redraw |
61
+ | `addLatLng(point)` | `this` | Add a single point and redraw |
62
+ | `setOptions(options)` | `this` | Update options and redraw |
63
+ | `getBounds()` | `L.LatLngBounds` | Return bounds covering all points |
64
+ | `redraw()` | `this` | Force a redraw |
65
+
66
+ ## Point Format
67
+
68
+ Each point can be:
69
+ - `[lat, lng]` — intensity defaults to 1.0
70
+ - `[lat, lng, intensity]` — intensity between 0.0 and 1.0
71
+ - `L.latLng(lat, lng)` — use `.alt` property for intensity
72
+
73
+ ## Migration from Leaflet.heat
74
+
75
+ The API is intentionally compatible. For most users:
76
+
77
+ 1. Replace `leaflet.heat` with `leaflet-heatmap-layer` in `package.json`
78
+ 2. Update your import/script tag
79
+
80
+ Key differences from Leaflet.heat:
81
+ - Intensity is automatically normalized across **all** data points per redraw, fixing the [intensity scaling bug](https://github.com/Leaflet/Leaflet.heat/issues/78)
82
+ - Full TypeScript support with exported types
83
+ - ES module, CommonJS, and UMD builds
84
+
85
+ ## License
86
+
87
+ MIT
@@ -0,0 +1,55 @@
1
+ import * as L from 'leaflet';
2
+ export interface HeatLayerOptions extends L.LayerOptions {
3
+ /** Radius of each heat point in pixels (default: 25). */
4
+ radius?: number;
5
+ /** Additional blur in pixels (default: 15). */
6
+ blur?: number;
7
+ /** Minimum opacity of the heatmap (default: 0.05). */
8
+ minOpacity?: number;
9
+ /** Zoom level at which points reach maximum intensity (default: map maxZoom or 18). */
10
+ maxZoom?: number;
11
+ /**
12
+ * Manual maximum intensity cap. When omitted, computed automatically
13
+ * per redraw from all data points — this is the core fix over Leaflet.heat.
14
+ */
15
+ max?: number;
16
+ /** Color gradient stops (default: blue -> cyan -> lime -> yellow -> red). */
17
+ gradient?: Record<number, string>;
18
+ /** Duration in ms to crossfade between old and new heatmap on zoom (default: 250, 0 to disable). */
19
+ zoomCrossfadeDuration?: number;
20
+ }
21
+ /** Input point: [lat, lng] or [lat, lng, intensity], or L.LatLng (with optional .alt for intensity). */
22
+ export type HeatLatLng = [number, number] | [number, number, number] | L.LatLng;
23
+ export declare class HeatLayer extends L.Layer {
24
+ private _latlngs;
25
+ private _options;
26
+ private _canvas;
27
+ private _renderer;
28
+ private _frame;
29
+ private _snapshotCanvas;
30
+ private _zooming;
31
+ private _crossfadeTimer;
32
+ constructor(latlngs: HeatLatLng[], options?: HeatLayerOptions);
33
+ onAdd(map: L.Map): this;
34
+ onRemove(map: L.Map): this;
35
+ /** Replace all data points and redraw. */
36
+ setLatLngs(latlngs: HeatLatLng[]): this;
37
+ /** Add a single data point and redraw. */
38
+ addLatLng(latlng: HeatLatLng): this;
39
+ /** Update options and redraw. */
40
+ setOptions(options: Partial<HeatLayerOptions>): this;
41
+ /** Return a LatLngBounds covering all data points, or an invalid bounds if empty. */
42
+ getBounds(): L.LatLngBounds;
43
+ /** Schedule a redraw on the next animation frame. */
44
+ redraw(): this;
45
+ private _createCanvas;
46
+ private _onMoveEnd;
47
+ /** Apply CSS transform during zoom animation (same approach as Leaflet's ImageOverlay). */
48
+ private _onZoomAnim;
49
+ private _takeSnapshot;
50
+ private _startCrossfade;
51
+ private _clearCrossfade;
52
+ private _redraw;
53
+ private _getIntensity;
54
+ private _toLatLng;
55
+ }
@@ -0,0 +1,34 @@
1
+ export interface HeatRendererOptions {
2
+ /** Radius of each heat point in pixels. */
3
+ radius: number;
4
+ /** Additional blur applied to each point in pixels. */
5
+ blur: number;
6
+ /** Minimum opacity of the heatmap output (0–1). */
7
+ minOpacity: number;
8
+ /** Color gradient stops, keyed by position (0–1). */
9
+ gradient: Record<number, string>;
10
+ }
11
+ /** A pixel-space data point: [x, y, intensity]. */
12
+ export type HeatPoint = [number, number, number];
13
+ export declare class HeatRenderer {
14
+ private _canvas;
15
+ private _ctx;
16
+ private _circle;
17
+ private _palette;
18
+ private _radius;
19
+ private _blur;
20
+ private _minOpacity;
21
+ private _gradient;
22
+ constructor(canvas: HTMLCanvasElement, options?: Partial<HeatRendererOptions>);
23
+ setOptions(options: Partial<HeatRendererOptions>): void;
24
+ get radius(): number;
25
+ get blur(): number;
26
+ resize(width: number, height: number): void;
27
+ clear(): void;
28
+ /** Draw the heatmap. Intensity values should be pre-normalized to 0–1. */
29
+ draw(points: HeatPoint[]): void;
30
+ private _getCircle;
31
+ private _getPalette;
32
+ /** Map alpha channel (greyscale intensity) to gradient colors. */
33
+ private _colorize;
34
+ }
@@ -0,0 +1,7 @@
1
+ import { HeatLayer, type HeatLayerOptions, type HeatLatLng } from './HeatLayer';
2
+ import { HeatRenderer, type HeatRendererOptions, type HeatPoint } from './HeatRenderer';
3
+ declare function heatLayer(latlngs: HeatLatLng[], options?: HeatLayerOptions): HeatLayer;
4
+ declare module 'leaflet' {
5
+ function heatLayer(latlngs: HeatLatLng[], options?: HeatLayerOptions): HeatLayer;
6
+ }
7
+ export { HeatLayer, HeatRenderer, heatLayer, type HeatLayerOptions, type HeatLatLng, type HeatRendererOptions, type HeatPoint, };
@@ -0,0 +1,422 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ HeatLayer: () => HeatLayer,
34
+ HeatRenderer: () => HeatRenderer,
35
+ heatLayer: () => heatLayer
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+ var import_leaflet = __toESM(require("leaflet"));
39
+
40
+ // src/HeatLayer.ts
41
+ var L = __toESM(require("leaflet"));
42
+
43
+ // src/HeatRenderer.ts
44
+ var DEFAULT_GRADIENT = {
45
+ 0.4: "blue",
46
+ 0.6: "cyan",
47
+ 0.7: "lime",
48
+ 0.8: "yellow",
49
+ 1: "red"
50
+ };
51
+ var HeatRenderer = class {
52
+ constructor(canvas, options) {
53
+ this._circle = null;
54
+ this._palette = null;
55
+ this._canvas = canvas;
56
+ const ctx = canvas.getContext("2d");
57
+ if (!ctx) throw new Error("Could not get 2d context from canvas");
58
+ this._ctx = ctx;
59
+ this._radius = options?.radius ?? 25;
60
+ this._blur = options?.blur ?? 15;
61
+ this._minOpacity = options?.minOpacity ?? 0.05;
62
+ this._gradient = options?.gradient ?? DEFAULT_GRADIENT;
63
+ }
64
+ setOptions(options) {
65
+ if (options.radius !== void 0 || options.blur !== void 0) {
66
+ this._radius = options.radius ?? this._radius;
67
+ this._blur = options.blur ?? this._blur;
68
+ this._circle = null;
69
+ }
70
+ if (options.gradient !== void 0) {
71
+ this._gradient = options.gradient;
72
+ this._palette = null;
73
+ }
74
+ if (options.minOpacity !== void 0) {
75
+ this._minOpacity = options.minOpacity;
76
+ }
77
+ }
78
+ get radius() {
79
+ return this._radius;
80
+ }
81
+ get blur() {
82
+ return this._blur;
83
+ }
84
+ resize(width, height) {
85
+ this._canvas.width = width;
86
+ this._canvas.height = height;
87
+ }
88
+ clear() {
89
+ this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
90
+ }
91
+ /** Draw the heatmap. Intensity values should be pre-normalized to 0–1. */
92
+ draw(points) {
93
+ if (points.length === 0) {
94
+ this.clear();
95
+ return;
96
+ }
97
+ const circle = this._getCircle();
98
+ const fullRadius = this._radius + this._blur;
99
+ this.clear();
100
+ for (const [x, y, intensity] of points) {
101
+ this._ctx.globalAlpha = Math.max(intensity, this._minOpacity);
102
+ this._ctx.drawImage(circle, x - fullRadius, y - fullRadius);
103
+ }
104
+ this._colorize();
105
+ }
106
+ _getCircle() {
107
+ if (this._circle) return this._circle;
108
+ const r = this._radius;
109
+ const blur = this._blur;
110
+ const fullRadius = r + blur;
111
+ const diameter = fullRadius * 2;
112
+ const circle = document.createElement("canvas");
113
+ circle.width = diameter;
114
+ circle.height = diameter;
115
+ const ctx = circle.getContext("2d");
116
+ ctx.shadowOffsetX = diameter;
117
+ ctx.shadowOffsetY = diameter;
118
+ ctx.shadowBlur = blur;
119
+ ctx.shadowColor = "black";
120
+ ctx.beginPath();
121
+ ctx.arc(fullRadius - diameter, fullRadius - diameter, r, 0, Math.PI * 2, true);
122
+ ctx.closePath();
123
+ ctx.fill();
124
+ this._circle = circle;
125
+ return circle;
126
+ }
127
+ _getPalette() {
128
+ if (this._palette) return this._palette;
129
+ const paletteCanvas = document.createElement("canvas");
130
+ paletteCanvas.width = 256;
131
+ paletteCanvas.height = 1;
132
+ const ctx = paletteCanvas.getContext("2d");
133
+ const grad = ctx.createLinearGradient(0, 0, 256, 0);
134
+ for (const [stop, color] of Object.entries(this._gradient)) {
135
+ grad.addColorStop(Number(stop), color);
136
+ }
137
+ ctx.fillStyle = grad;
138
+ ctx.fillRect(0, 0, 256, 1);
139
+ this._palette = ctx.getImageData(0, 0, 256, 1).data;
140
+ return this._palette;
141
+ }
142
+ /** Map alpha channel (greyscale intensity) to gradient colors. */
143
+ _colorize() {
144
+ const w = this._canvas.width;
145
+ const h = this._canvas.height;
146
+ if (w === 0 || h === 0) return;
147
+ const imageData = this._ctx.getImageData(0, 0, w, h);
148
+ const pixels = imageData.data;
149
+ const palette = this._getPalette();
150
+ for (let i = 0, len = pixels.length; i < len; i += 4) {
151
+ const alpha = pixels[i + 3];
152
+ if (alpha === 0) continue;
153
+ const paletteOffset = alpha * 4;
154
+ pixels[i] = palette[paletteOffset];
155
+ pixels[i + 1] = palette[paletteOffset + 1];
156
+ pixels[i + 2] = palette[paletteOffset + 2];
157
+ }
158
+ this._ctx.putImageData(imageData, 0, 0);
159
+ }
160
+ };
161
+
162
+ // src/HeatLayer.ts
163
+ var HeatLayer = class extends L.Layer {
164
+ constructor(latlngs, options) {
165
+ super(options);
166
+ this._canvas = null;
167
+ this._renderer = null;
168
+ this._frame = null;
169
+ this._snapshotCanvas = null;
170
+ this._zooming = false;
171
+ this._crossfadeTimer = null;
172
+ this._latlngs = latlngs;
173
+ this._options = {
174
+ radius: 25,
175
+ blur: 15,
176
+ minOpacity: 0.05,
177
+ ...options
178
+ };
179
+ }
180
+ onAdd(map) {
181
+ this._map = map;
182
+ if (!this._canvas) {
183
+ this._createCanvas();
184
+ }
185
+ const pane = this.getPane();
186
+ if (pane && this._canvas) {
187
+ pane.appendChild(this._canvas);
188
+ }
189
+ map.on("moveend", this._onMoveEnd, this);
190
+ map.on("zoomanim", this._onZoomAnim, this);
191
+ this._onMoveEnd();
192
+ return this;
193
+ }
194
+ onRemove(map) {
195
+ if (this._frame !== null) {
196
+ cancelAnimationFrame(this._frame);
197
+ this._frame = null;
198
+ }
199
+ this._clearCrossfade();
200
+ if (this._snapshotCanvas && this._snapshotCanvas.parentNode) {
201
+ this._snapshotCanvas.parentNode.removeChild(this._snapshotCanvas);
202
+ }
203
+ this._snapshotCanvas = null;
204
+ const pane = this.getPane();
205
+ if (pane && this._canvas) {
206
+ pane.removeChild(this._canvas);
207
+ }
208
+ map.off("moveend", this._onMoveEnd, this);
209
+ map.off("zoomanim", this._onZoomAnim, this);
210
+ return this;
211
+ }
212
+ /** Replace all data points and redraw. */
213
+ setLatLngs(latlngs) {
214
+ this._latlngs = latlngs;
215
+ return this.redraw();
216
+ }
217
+ /** Add a single data point and redraw. */
218
+ addLatLng(latlng) {
219
+ this._latlngs.push(latlng);
220
+ return this.redraw();
221
+ }
222
+ /** Update options and redraw. */
223
+ setOptions(options) {
224
+ Object.assign(this._options, options);
225
+ if (this._renderer) {
226
+ this._renderer.setOptions({
227
+ radius: this._options.radius,
228
+ blur: this._options.blur,
229
+ minOpacity: this._options.minOpacity,
230
+ gradient: this._options.gradient
231
+ });
232
+ }
233
+ return this.redraw();
234
+ }
235
+ /** Return a LatLngBounds covering all data points, or an invalid bounds if empty. */
236
+ getBounds() {
237
+ if (this._latlngs.length === 0) return L.latLngBounds([]);
238
+ return L.latLngBounds(this._latlngs.map((p) => this._toLatLng(p)));
239
+ }
240
+ /** Schedule a redraw on the next animation frame. */
241
+ redraw() {
242
+ if (this._map && this._frame === null) {
243
+ this._frame = requestAnimationFrame(() => {
244
+ this._frame = null;
245
+ this._redraw();
246
+ });
247
+ }
248
+ return this;
249
+ }
250
+ _createCanvas() {
251
+ this._canvas = document.createElement("canvas");
252
+ this._canvas.style.position = "absolute";
253
+ this._canvas.style.pointerEvents = "none";
254
+ this._canvas.style.willChange = "transform";
255
+ this._canvas.style.transformOrigin = "0 0";
256
+ const animated = this._map.options.zoomAnimation && L.Browser.any3d;
257
+ L.DomUtil.addClass(
258
+ this._canvas,
259
+ "leaflet-zoom-" + (animated ? "animated" : "hide")
260
+ );
261
+ this._renderer = new HeatRenderer(this._canvas, {
262
+ radius: this._options.radius,
263
+ blur: this._options.blur,
264
+ minOpacity: this._options.minOpacity,
265
+ gradient: this._options.gradient
266
+ });
267
+ }
268
+ _onMoveEnd() {
269
+ if (!this._map || !this._canvas) return;
270
+ const wasZooming = this._zooming;
271
+ this._zooming = false;
272
+ const duration = this._options.zoomCrossfadeDuration ?? 250;
273
+ if (wasZooming && duration > 0) {
274
+ this._takeSnapshot();
275
+ }
276
+ const size = this._map.getSize();
277
+ const topLeft = this._map.containerPointToLayerPoint([0, 0]);
278
+ L.DomUtil.setPosition(this._canvas, topLeft);
279
+ this._renderer.resize(size.x, size.y);
280
+ this._redraw();
281
+ if (wasZooming && duration > 0) {
282
+ this._startCrossfade(duration);
283
+ }
284
+ }
285
+ /** Apply CSS transform during zoom animation (same approach as Leaflet's ImageOverlay). */
286
+ _onZoomAnim(e) {
287
+ if (!this._map || !this._canvas) return;
288
+ this._zooming = true;
289
+ const map = this._map;
290
+ const scale = map.getZoomScale(e.zoom);
291
+ const canvasPos = L.DomUtil.getPosition(this._canvas);
292
+ const topLeftLatLng = map.layerPointToLatLng(canvasPos);
293
+ const layerTopLeft = map.containerPointToLayerPoint([0, 0]);
294
+ const newPos = map.project(topLeftLatLng, e.zoom).subtract(map.project(e.center, e.zoom)).add(map.getSize().divideBy(2)).add(layerTopLeft).round();
295
+ L.DomUtil.setTransform(this._canvas, newPos, scale);
296
+ }
297
+ _takeSnapshot() {
298
+ if (!this._canvas) return;
299
+ this._clearCrossfade();
300
+ const canvas = this._canvas;
301
+ if (!this._snapshotCanvas) {
302
+ this._snapshotCanvas = document.createElement("canvas");
303
+ this._snapshotCanvas.style.position = "absolute";
304
+ this._snapshotCanvas.style.pointerEvents = "none";
305
+ }
306
+ const snap = this._snapshotCanvas;
307
+ snap.width = canvas.width;
308
+ snap.height = canvas.height;
309
+ snap.getContext("2d").drawImage(canvas, 0, 0);
310
+ snap.style.transform = canvas.style.transform;
311
+ snap.style.transformOrigin = canvas.style.transformOrigin;
312
+ snap.style.opacity = "1";
313
+ snap.style.transition = "";
314
+ if (canvas.parentNode) {
315
+ canvas.after(snap);
316
+ }
317
+ }
318
+ _startCrossfade(duration) {
319
+ const snap = this._snapshotCanvas;
320
+ if (!snap) return;
321
+ snap.style.transition = `opacity ${duration}ms ease-out`;
322
+ snap.offsetHeight;
323
+ snap.style.opacity = "0";
324
+ this._crossfadeTimer = window.setTimeout(() => {
325
+ this._crossfadeTimer = null;
326
+ if (snap) {
327
+ snap.style.transition = "";
328
+ }
329
+ }, duration);
330
+ }
331
+ _clearCrossfade() {
332
+ if (this._crossfadeTimer !== null) {
333
+ clearTimeout(this._crossfadeTimer);
334
+ this._crossfadeTimer = null;
335
+ }
336
+ if (this._snapshotCanvas) {
337
+ this._snapshotCanvas.style.transition = "";
338
+ this._snapshotCanvas.style.opacity = "0";
339
+ }
340
+ }
341
+ _redraw() {
342
+ if (!this._map || !this._renderer || !this._canvas) return;
343
+ const map = this._map;
344
+ const size = map.getSize();
345
+ if (size.x === 0 || size.y === 0) return;
346
+ const renderer = this._renderer;
347
+ const r = renderer.radius + renderer.blur;
348
+ const cellSize = Math.max(1, Math.floor(renderer.radius / 2));
349
+ const maxZoom = this._options.maxZoom ?? map.getMaxZoom();
350
+ const zoom = map.getZoom();
351
+ const v = 1 / Math.pow(2, Math.max(0, Math.min(maxZoom - zoom, 12)));
352
+ const bounds = map.getBounds();
353
+ const padLat = r / size.y * (bounds.getNorth() - bounds.getSouth());
354
+ const padLng = r / size.x * (bounds.getEast() - bounds.getWest());
355
+ const paddedBounds = L.latLngBounds(
356
+ [bounds.getSouth() - padLat, bounds.getWest() - padLng],
357
+ [bounds.getNorth() + padLat, bounds.getEast() + padLng]
358
+ );
359
+ let autoMax = 0;
360
+ if (this._options.max === void 0) {
361
+ for (const p of this._latlngs) {
362
+ const scaled = this._getIntensity(p) * v;
363
+ if (scaled > autoMax) autoMax = scaled;
364
+ }
365
+ }
366
+ const effectiveMax = this._options.max ?? Math.max(autoMax, 1e-10);
367
+ const topLeft = map.containerPointToLayerPoint([0, 0]);
368
+ const grid = {};
369
+ const pixelPoints = [];
370
+ for (const p of this._latlngs) {
371
+ const latlng = this._toLatLng(p);
372
+ if (!paddedBounds.contains(latlng)) continue;
373
+ const lp = map.latLngToLayerPoint(latlng);
374
+ const x = lp.x - topLeft.x;
375
+ const y = lp.y - topLeft.y;
376
+ const intensity = this._getIntensity(p) * v;
377
+ const gx = Math.floor(lp.x / cellSize);
378
+ const gy = Math.floor(lp.y / cellSize);
379
+ const key = `${gx}:${gy}`;
380
+ const cell = grid[key];
381
+ if (cell) {
382
+ cell[0] += x * intensity;
383
+ cell[1] += y * intensity;
384
+ cell[2] += intensity;
385
+ cell[3]++;
386
+ } else {
387
+ grid[key] = [x * intensity, y * intensity, intensity, 1];
388
+ }
389
+ }
390
+ for (const key in grid) {
391
+ const cell = grid[key];
392
+ const totalIntensity = cell[2];
393
+ if (totalIntensity === 0) continue;
394
+ pixelPoints.push([
395
+ cell[0] / totalIntensity,
396
+ cell[1] / totalIntensity,
397
+ totalIntensity / effectiveMax
398
+ ]);
399
+ }
400
+ renderer.draw(pixelPoints);
401
+ }
402
+ _getIntensity(p) {
403
+ if (p instanceof L.LatLng) {
404
+ return p.alt ?? 1;
405
+ }
406
+ if (Array.isArray(p) && p.length >= 3) {
407
+ return p[2];
408
+ }
409
+ return 1;
410
+ }
411
+ _toLatLng(p) {
412
+ if (p instanceof L.LatLng) return p;
413
+ return L.latLng(p[0], p[1]);
414
+ }
415
+ };
416
+
417
+ // src/index.ts
418
+ function heatLayer(latlngs, options) {
419
+ return new HeatLayer(latlngs, options);
420
+ }
421
+ import_leaflet.default.heatLayer = heatLayer;
422
+ //# sourceMappingURL=leaflet-heatmap-layer.cjs.js.map