Triển khai và Cấu hình Lớp Di Chuyển Dữ Liệu (Migration Layer) trên Leaflet

Để tạo hiệu ứng đường bay của các vì sao băng trên bản đồ Leaflet, chúng ta có thể tùy chỉnh một lớp di chuyển dữ liệu hiện có. Bài viết này sẽ hướng dẫn cách triển khai và cấu hình lớp này, bao gồm các tham số quan trọng.

Tệp HTML

Trước tiên, cần chuẩn bị tệp HTML để nhúng bản đồ và các tập lệnh cần thiết.


<!DOCTYPE html>
<html lang="vi">
<head>
    <meta charset="UTF-8">
    <title>Hiệu ứng Sao Băng trên Leaflet</title>
    <link rel="stylesheet" href="./leaflet/leaflet.css">
    <style>
        html, body {
            height: 100%;
            margin: 0;
        }
        .leaflet-container {
            height: 100vh;
            width: 100%;
            max-width: 100%;
            max-height: 100%;
        }
        .my-div-icon {
            background-color: rgb(10, 214, 214);
            border-radius: 50%;
            width: 10px;
            height: 10px;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <script src="/leaflet/leaflet.js"></script>
    <script src="/leaflet/migrationLayer.js"></script>

    <script>
        // Cấu hình bản đồ cơ sở
        const mapProviderUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
        const baseTileLayer = new L.TileLayer(mapProviderUrl);
        const initialCenter = new L.latLng(39.5, 116.89);

        const leafletMap = new L.Map("map", {
            CRS: 'Simple',
            center: initialCenter,
            zoom: 6,
            layers: [baseTileLayer],
            minZoom: 0,
            maxZoom: 16,
            opacity: 0.8,
        });

        // Hàm tạo tọa độ ngẫu nhiên
        const generateRandomCoords = () => {
            const latitude = Math.random() * 45; // Giới hạn vĩ độ
            const longitude = Math.random() * 150; // Giới hạn kinh độ
            return [longitude, latitude];
        }

        // Hàm tạo màu HEX ngẫu nhiên
        const generateRandomHexColor = () => {
            const hexChars = '0123456789ABCDEF';
            let colorCode = '#';
            for (let i = 0; i < 6; i++) {
                colorCode += hexChars[Math.floor(Math.random() * 16)];
            }
            return colorCode;
        }

        // Dữ liệu cho các dòng di chuyển
        const migrationData = [];
        for (let i = 0; i < 10; i++) {
            migrationData.push({
                color: generateRandomHexColor(),  // Màu của sao băng
                from: generateRandomCoords(),     // Điểm bắt đầu
                to: generateRandomCoords(),       // Điểm kết thúc
                labels: ["Điểm A", "Điểm B"],     // Nhãn cho điểm bắt đầu và kết thúc
                beArrow: false,                   // Có hiển thị mũi tên không (mũi tên dẫn đường và mũi tên kết thúc)
                bePulse: true,                    // Có hiển thị vòng tròn lan tỏa ở điểm kết thúc không
                arrowSize: 0.1,                   // Kích thước mũi tên
            });
        }

        // Khởi tạo lớp MigrationLayer
        const migrationOverlay = new L.migrationLayer({
            map: leafletMap,
            data: migrationData,
            pulseRadius: 5,                  // Bán kính của vòng tròn lan tỏa
            pulseColor: '#f00f00',           // Màu của vòng tròn lan tỏa
            pulseBorderWidth: 1,             // Độ rộng viền của vòng tròn lan tỏa
            starWidth: 4,                    // Độ rộng của vệt sao băng
            arcWidth: 2,                     // Độ rộng của đường cong (vệt sao băng)
            arcLabel: false,                 // Có hiển thị nhãn trên đường cong không
            arcAlpha: 0.5,                   // Độ trong suốt của đường cong
            arcStrokeColor: '#f00f00',       // Màu của đường cong
            arcLabelFont: '10px sans-serif', // Phông chữ cho nhãn đường cong
        });

        migrationOverlay.addTo(); // Thêm lớp vào bản đồ

        // Xử lý sự kiện click trên đường cong
        migrationOverlay.onClick((arcData) => {
            console.log('Đường cong được nhấp vào:', arcData);
        });
    </script>
</body>
</html>

Tệp migrationLayer.js

Tệp này chứa mã nguồn của lớp L.MigrationLayer, bao gồm các thành phần để vẽ các đối tượng đồ họa như điểm đánh dấu, đường cong, vòng tròn lan tỏa và vệt sao băng.


(function (window) {
    // Các hàm tiện ích để xử lý màu sắc và mảng
    const utils = {
        calculateColor: function (color, opacity) {
            if (color.indexOf('#') === 0) {
                const hexColor = color.slice(1);
                const r = parseInt(hexColor.slice(0, 2), 16);
                const g = parseInt(hexColor.slice(2, 4), 16);
                const b = parseInt(hexColor.slice(4), 16);
                return `rgba(${r},${g},${b},${opacity})`;
            } else if (/^rgb\(/.test(color)) {
                return color.replace(/rgb/, 'rgba').replace(')', `,${opacity})`);
            } else {
                return color.split(',').slice(0, 3).join(',') + `,${opacity})`;
            }
        }
    };
    const arrayUtils = {
        forEach: function (arr, callback, scope) {
            if (typeof Array.prototype.forEach === 'function') {
                arr.forEach(callback, scope);
            } else {
                for (let i = 0, len = arr.length; i < len; i++) {
                    callback.apply(scope, [arr[i], i, arr]);
                }
            }
        },
        map: function (arr, callback, scope) {
            if (typeof Array.prototype.map === 'function') {
                return arr.map(callback, scope);
            } else {
                const mappedArray = [];
                for (let i = 0, len = arr.length; i < len; i++) {
                    mappedArray[i] = callback.apply(scope, [arr[i], i, arr]);
                }
                return mappedArray;
            }
        }
    };

    // Lớp Marker để vẽ các điểm đánh dấu và mũi tên
    const Marker = (function () {
        function Marker(options) {
            this.x = options.x;
            this.y = options.y;
            this.rotation = options.rotation;
            this.style = options.style; // 'circle' hoặc 'arrow'
            this.color = options.color;
            this.size = options.size;
            this.borderWidth = options.borderWidth;
            this.borderColor = options.borderColor;
        }

        Marker.prototype.draw = function (context) {
            context.save();
            context.translate(this.x, this.y);
            context.rotate(this.rotation);

            context.lineWidth = this.borderWidth || 0;
            context.strokeStyle = this.borderColor || '#000';
            context.fillStyle = this.color || '#000';
            context.beginPath();

            if (this.style === 'circle') {
                context.arc(0, 0, this.size, 0, Math.PI * 2, false);
            } else if (this.style === 'arrow') {
                context.moveTo(-this.size, -this.size);
                context.lineTo(this.size, 0);
                context.lineTo(-this.size, this.size);
                context.lineTo(-this.size / 4, 0);
                context.lineTo(-this.size, -this.size);
            }
            context.closePath();
            context.stroke();
            context.fill();
            context.restore();
        };
        return Marker;
    })();

    // Lớp Arc để vẽ đường cong
    const Arc = (function () {
        function Arc(options) {
            const startX = options.startX, startY = options.startY, endX = options.endX, endY = options.endY;
            // Tính toán tâm và bán kính của cung tròn để tạo hiệu ứng cong
            const dx = endX - startX;
            const dy = endY - startY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const midX = (startX + endX) / 2;
            const midY = (startY + endY) / 2;
            // Hệ số để điều chỉnh độ cong của cung
            const curvatureFactor = 1.5;

            // Tính toán tâm cung tròn
            const centerX = midX - dy * curvatureFactor * dist / 100;
            const centerY = midY + dx * curvatureFactor * dist / 100;

            // Tính bán kính cung tròn
            const radius = Math.sqrt(Math.pow(dist / 2, 2) + Math.pow(dist * curvatureFactor / 10, 2));
            const startAngle = Math.atan2(startY - centerY, startX - centerX);
            const endAngle = Math.atan2(endY - centerY, endX - centerX);

            this.startX = startX;
            this.startY = startY;
            this.endX = endX;
            this.endY = endY;
            this.centerX = centerX;
            this.centerY = centerY;
            this.startAngle = startAngle;
            this.endAngle = endAngle;
            this.startLabel = options.labels && options.labels[0];
            this.endLabel = options.labels && options.labels[1];
            this.radius = radius;
            this.lineWidth = options.width || 1;
            this.strokeStyle = options.color || '#000';
            this.arcAlpha = options.arcAlpha || .2;
            this.arcStrokeColor = options.arcStrokeColor || '#000';
            this.hasLabel = options.label;
            this.font = options.font;
        }

        Arc.prototype.draw = function (context) {
            context.save();
            context.lineWidth = this.lineWidth;
            context.strokeStyle = this.strokeStyle;
            context.globalAlpha = this.arcAlpha;
            context.shadowColor = this.strokeStyle;
            context.lineCap = 'round';

            context.beginPath();
            context.arc(this.centerX, this.centerY, this.radius, this.startAngle, this.endAngle, false);
            context.stroke();
            context.restore();

            // Vẽ nhãn nếu có
            context.save();
            context.fillStyle = this.strokeStyle;
            if (this.hasLabel) {
                context.font = this.font;
                if (this.startLabel) {
                    const startLabelX = this.startX - 15;
                    const startLabelY = this.startY + 5;
                    context.fillText(this.startLabel, startLabelX, startLabelY);
                }
                if (this.endLabel) {
                    const endLabelX = this.endX - 15;
                    const endLabelY = this.endY - 5;
                    context.fillText(this.endLabel, endLabelX, endLabelY);
                }
            }
            context.restore();
        };
        return Arc;
    })();

    // Lớp Pulse để vẽ vòng tròn lan tỏa tại điểm kết thúc
    const Pulse = (function () {
        function Pulse(options) {
            this.x = options.x;
            this.y = options.y;
            this.maxRadius = options.radius;
            this.color = options.color;
            this.lineWidth = options.borderWidth;
            this.currentRadius = 0;
        }

        Pulse.prototype.draw = function (context) {
            const expansionRate = 0.5; // Tốc độ lan tỏa
            this.currentRadius += expansionRate;
            context.save();
            context.translate(this.x, this.y);

            let strokeColor = this.color;
            const opacity = 1 - this.currentRadius / this.maxRadius;
            strokeColor = utils.calculateColor(strokeColor, opacity > 0 ? opacity : 0);

            context.strokeStyle = strokeColor;
            context.shadowColor = strokeColor;
            context.lineWidth = this.lineWidth;
            context.beginPath();
            context.arc(0, 0, this.currentRadius, 0, Math.PI * 2, false);
            context.stroke();
            context.restore();

            // Reset bán kính khi đạt đến tối đa để tạo hiệu ứng lặp lại
            if (Math.abs(this.maxRadius - this.currentRadius) < 0.8) {
                this.currentRadius = 0;
            }
        }
        return Pulse;
    })();

    // Lớp Spark để vẽ vệt sao băng
    const Spark = (function () {
        function Spark(options) {
            const startX = options.startX, startY = options.startY, endX = options.endX, endY = options.endY;
            // Tính toán tâm và bán kính của cung tròn tương tự lớp Arc
            const dx = endX - startX;
            const dy = endY - startY;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const midX = (startX + endX) / 2;
            const midY = (startY + endY) / 2;
            const curvatureFactor = 1.5;

            const centerX = midX - dy * curvatureFactor * dist / 100;
            const centerY = midY + dx * curvatureFactor * dist / 100;
            const radius = Math.sqrt(Math.pow(dist / 2, 2) + Math.pow(dist * curvatureFactor / 10, 2));
            const startAngle = Math.atan2(startY - centerY, startX - centerX);
            const endAngle = Math.atan2(endY - centerY, endX - centerX);

            // Đảm bảo góc của Spark không vượt quá PI
            if (startAngle * endAngle < 0) {
                if (startAngle < 0) {
                    startAngle += Math.PI * 2;
                    endAngle += Math.PI * 2;
                } else {
                    endAngle += Math.PI * 2;
                }
            }

            this.showArrow = options.beArrow;
            this.arrowSize = options.arrowSize || 3;
            this.tailPointsCount = 20; // Số lượng điểm tạo hiệu ứng đuôi
            this.centerX = centerX;
            this.centerY = centerY;
            this.startAngle = startAngle;
            this.endAngle = endAngle;
            this.radius = radius;
            this.lineWidth = options.width || 0;
            this.starWidth = options.starWidth;
            this.strokeStyle = options.color || '#fff';
            this.angleVelocity = (80 / Math.min(this.radius, 10000)) / this.tailPointsCount; // Tốc độ di chuyển góc
            this.currentTrailAngle = this.startAngle; // Góc hiện tại của đuôi
            this.animationActive = true;

            this.arrowMarker = new Marker({
                x: 0, // Tọa độ sẽ được cập nhật trong hàm draw
                y: 0,
                rotation: 0,
                style: 'arrow',
                color: this.strokeStyle,
                size: this.arrowSize,
                borderWidth: 5,
                borderColor: this.strokeStyle
            });
        }

        Spark.prototype.drawArcSegment = function (context, strokeColor, lineWidth, startAngle, endAngle) {
            context.save();
            context.lineWidth = lineWidth;
            context.strokeStyle = strokeColor;
            context.shadowColor = this.strokeStyle;
            context.lineCap = "round";
            context.beginPath();
            context.arc(this.centerX, this.centerY, this.radius, startAngle, endAngle, false);
            context.stroke();
            context.restore();
        };

        Spark.prototype.draw = function (context) {
            const endAngle = this.endAngle;
            // Cập nhật góc di chuyển của vệt sao băng
            const angleStep = this.angleVelocity;
            let currentAngle = this.currentTrailAngle + angleStep;
            this.currentTrailAngle = currentAngle;

            // Vẽ các đoạn đuôi với độ mờ giảm dần
            for (let i = 0; i < this.tailPointsCount; i++) {
                const tailOpacity = 0.3 - 0.3 / this.tailPointsCount * i;
                const tailColor = utils.calculateColor(this.strokeStyle, tailOpacity);
                const tailLineWidth = 5;
                const segmentStartAngle = this.currentTrailAngle - this.angleVelocity * i;
                if (segmentStartAngle > this.startAngle) {
                    this.drawArcSegment(context, tailColor,
                        this.starWidth,
                        segmentStartAngle,
                        this.currentTrailAngle
                    );
                }
            }

            // Vẽ mũi tên dẫn đường nếu được cấu hình
            context.save();
            context.translate(this.centerX, this.centerY);
            if (this.showArrow) {
                this.arrowMarker.x = Math.cos(this.currentTrailAngle) * this.radius;
                this.arrowMarker.y = Math.sin(this.currentTrailAngle) * this.radius;
                this.arrowMarker.rotation = this.currentTrailAngle + Math.PI / 2;
                this.arrowMarker.draw(context);
            }
            context.restore();

            // Dừng hoạt ảnh khi sao băng đi hết quỹ đạo
            if ((endAngle - this.currentTrailAngle) * 180 / Math.PI < 0.5) {
                this.currentTrailAngle = this.startAngle;
                this.animationActive = false;
            }
        };
        return Spark;
    })();

    // Lớp chính MigrationLayer để quản lý tất cả các đối tượng và hoạt ảnh
    const Migration = (function () {
        function Migration(options) {
            this.data = options.data;
            this.store = { // Lưu trữ các đối tượng đồ họa
                arcs: [],
                markers: [],
                pulses: [],
                sparks: []
            };
            this.isAnimating = true;
            this.hasStarted = false;
            this.context = options.context;
            this.styleConfig = options.style;
            this.init();
        }

        Migration.prototype.init = function () {
            this.updateData(this.data);
        };

        Migration.prototype.clear = function () {
            this.store = {
                arcs: [],
                markers: [],
                pulses: [],
                sparks: []
            };
            this.isAnimating = true;
            this.hasStarted = false;
            window.cancelAnimationFrame(this.animationFrameId); // Hủy bỏ hoạt ảnh đang chạy
        };

        Migration.prototype.updateData = function (newData) {
            if (!newData || newData.length === 0) {
                return;
            }
            this.clear(); // Xóa dữ liệu cũ trước khi cập nhật
            this.data = newData;

            if (this.data && this.data.length > 0) {
                arrayUtils.forEach(this.data, function (element) {
                    // Tạo đối tượng Arc
                    const arc = new Arc({
                        startX: element.from[0],
                        startY: element.from[1],
                        endX: element.to[0],
                        endY: element.to[1],
                        labels: element.labels,
                        label: this.styleConfig.arc.label,
                        font: this.styleConfig.arc.font,
                        width: this.styleConfig.arc.width,
                        arcAlpha: this.styleConfig.arc.arcAlpha,
                        color: this.styleConfig.arc.arcStrokeColor,
                    });

                    // Tạo đối tượng Marker cho điểm kết thúc (nếu có mũi tên)
                    const marker = new Marker({
                        x: element.to[0],
                        y: element.to[1],
                        rotation: arc.endAngle + Math.PI / 2,
                        style: 'arrow',
                        color: this.styleConfig.arc.arcStrokeColor,
                        size: 6,
                        borderWidth: 0,
                        borderColor: this.styleConfig.arc.arcStrokeColor,
                    });

                    // Tạo đối tượng Pulse cho điểm kết thúc (nếu có hiệu ứng lan tỏa)
                    const pulse = new Pulse({
                        x: element.to[0],
                        y: element.to[1],
                        radius: this.styleConfig.pulse.radius,
                        color: this.styleConfig.pulse.color || element.color,
                        borderWidth: this.styleConfig.pulse.borderWidth
                    });

                    // Tạo đối tượng Spark cho vệt sao băng
                    const spark = new Spark({
                        startX: element.from[0],
                        startY: element.from[1],
                        endX: element.to[0],
                        endY: element.to[1],
                        width: 5,
                        starWidth: this.styleConfig.star.width,
                        color: element.color,
                        beArrow: element.beArrow,
                        arrowSize: element.arrowSize,
                    });

                    this.store.arcs.push(arc);
                    element.beArrow && this.store.markers.push(marker);
                    element.bePulse && this.store.pulses.push(pulse);
                    this.store.sparks.push(spark);
                }, this);
            }
        };

        Migration.prototype.start = function (canvas) {
            const that = this;
            if (!this.hasStarted) {
                (function drawFrame() {
                    that.animationFrameId = window.requestAnimationFrame(drawFrame, canvas);
                    if (that.isAnimating) {
                        // Làm mới canvas để xóa khung hình trước đó
                        canvas.width = canvas.width; // Hoặc canvas.width += 1; canvas.width -= 1;
                        // Vẽ tất cả các đối tượng đồ họa
                        for (const layerType in that.store) {
                            const shapes = that.store[layerType];
                            for (let i = 0, len = shapes.length; i < len; i++) {
                                shapes[i].draw(that.context);
                            }
                        }
                    }
                })();
                this.hasStarted = true;
            }
        };

        Migration.prototype.play = function () {
            this.isAnimating = true;
        };

        Migration.prototype.pause = function () {
            this.isAnimating = false;
        };
        return Migration;
    })();

    // Định nghĩa lớp L.MigrationLayer kế thừa từ L.Class
    L.MigrationLayer = L.Class.extend({
        options: {
            map: {},
            data: {},
            pulseRadius: 25,
            pulseBorderWidth: 3,
            arcWidth: 1,
            arcLabel: true,
            arcAlpha: 1,
            arcLabelFont: '15px sans-serif',
            starWidth: 4,
            arcStrokeColor: '#f00f00',
        },

        _setOptions: function (obj, options) {
            if (!obj.options) {
                obj.options = {};
            }
            for (const i in options) {
                obj.options[i] = options[i];
            }
            return obj.options;
        },

        initialize: function (options) {
            this._setOptions(this, options);
            this._map = this.options.map || {};
            this._data = this.options.data || [];
            // Cấu hình kiểu dáng cho các đối tượng đồ họa
            this._styleConfig = {
                pulse: {
                    radius: this.options.pulseRadius,
                    borderWidth: this.options.pulseBorderWidth,
                    color: this.options.pulseColor,
                },
                arc: {
                    width: this.options.arcWidth,
                    label: this.options.arcLabel,
                    font: this.options.arcLabelFont,
                    arcAlpha: this.options.arcAlpha,
                    arcStrokeColor: this.options.arcStrokeColor,
                },
                star: {
                    width: this.options.starWidth,
                },
            };
            this._isVisible = true;
            this._init();
        },

        _init: function () {
            // Tạo container cho canvas
            const container = L.DomUtil.create('div', 'leaflet-MigrationLayer-container');
            container.style.position = 'absolute';
            container.style.width = this._map.getSize().x + "px";
            container.style.height = this._map.getSize().y + "px";
            this.container = container;

            // Tạo canvas và lấy context 2D
            this.canvas = document.createElement('canvas');
            this.context = this.canvas.getContext('2d');
            container.appendChild(this.canvas);
            this._map.getPanes().overlayPane.appendChild(container); // Thêm container vào DOM của Leaflet

            // Khởi tạo lớp Migration nếu chưa có
            if (!this.migrationInstance) {
                const convertedData = this._convertDataToPixels(this._data);
                this.migrationInstance = new Migration({
                    data: convertedData,
                    context: this.context,
                    style: this._styleConfig
                });
            }
        },

        _resizeCanvas: function () {
            // Điều chỉnh kích thước canvas theo kích thước của bản đồ
            const mapContainer = this._map.getContainer();
            this.canvas.setAttribute('width', mapContainer.clientWidth);
            this.canvas.setAttribute('height', mapContainer.clientHeight);
        },

        _convertDataToPixels: function (geoData) {
            // Chuyển đổi dữ liệu địa lý (lat/lng) sang tọa độ pixel trên canvas
            const bounds = this._map.getBounds();
            if (!geoData || !bounds) {
                return [];
            }

            return arrayUtils.map(geoData, (d) => {
                const fromPixel = this._map.latLngToContainerPoint(new L.LatLng(d.from[1], d.from[0]));
                const toPixel = this._map.latLngToContainerPoint(new L.LatLng(d.to[1], d.to[0]));
                return {
                    from: [fromPixel.x, fromPixel.y],
                    to: [toPixel.x, toPixel.y],
                    labels: d.labels,
                    value: d.value,
                    color: d.color,
                    beArrow: d.beArrow,
                    arrowSize: d.arrowSize,
                    bePulse: d.bePulse,
                };
            }, this);
        },

        _bindMapEvents: function () {
            // Gắn kết các sự kiện của bản đồ Leaflet
            const that = this;
            this._map.on('moveend', function () {
                that.migrationInstance.play();
                that._redraw();
            });
            this._map.on('zoomstart', function () {
                that.container.style.display = 'none'; // Ẩn lớp khi zoom để tránh giật
            });
            this._map.on('zoomend', function () {
                if (that._isVisible) {
                    that.container.style.display = '';
                    that._redraw();
                }
            });
        },

        _redraw: function () {
            // Vẽ lại lớp dựa trên trạng thái hiện tại của bản đồ
            this._resizeCanvas();
            this._updateMapPosition();
            const pixelData = this._convertDataToPixels(this._data);
            this.migrationInstance.updateData(pixelData);
            this.migrationInstance.start(this.canvas);
        },

        _updateMapPosition: function () {
            // Cập nhật vị trí của container canvas theo bản đồ
            const bounds = this._map.getBounds();
            const topLeft = this._map.latLngToLayerPoint(bounds.getNorthWest());
            L.DomUtil.setPosition(this.container, topLeft);
        },

        addTo: function () {
            // Phương thức để thêm lớp vào bản đồ
            this._bindMapEvents();
            this._resizeCanvas();
            this._updateMapPosition();
            const pixelData = this._convertDataToPixels(this._data);
            this.migrationInstance.updateData(pixelData);
            this.migrationInstance.start(this.canvas);
            return this;
        },

        onClick: function (callback) {
            // Hàm xử lý sự kiện click trên canvas
            const isPointInArcStroke = (x, y, arc) => {
                this.context.beginPath();
                // Tăng lineWidth tạm thời để dễ dàng click vào đường cong
                const originalLineWidth = this.context.lineWidth;
                this.context.lineWidth = (arc.lineWidth > 6) ? arc.lineWidth : 12;
                this.context.arc(arc.centerX, arc.centerY, arc.radius, arc.startAngle, arc.endAngle, false);
                const isPointIn = this.context.isPointInStroke(x, y);
                this.context.lineWidth = originalLineWidth; // Khôi phục lineWidth ban đầu
                return isPointIn;
            }

            const that = this;
            this.canvas.addEventListener('click', function (event) {
                const rect = that.canvas.getBoundingClientRect();
                const clickX = event.clientX - rect.left;
                const clickY = event.clientY - rect.top;

                // Duyệt qua các cung tròn để kiểm tra xem điểm click có nằm trên cung nào không
                that.migrationInstance.store.arcs.forEach((arc, index) => {
                    if (isPointInArcStroke(clickX, clickY, arc)) {
                        callback(arc); // Gọi hàm callback với dữ liệu của cung tròn được click
                    }
                });
            });
        },

        setData: function (newData) {
            // Cập nhật dữ liệu và vẽ lại lớp
            this._data = newData;
            this._redraw();
        },

        hide: function () {
            // Ẩn lớp
            this.container.style.display = 'none';
            this._isVisible = false;
        },

        show: function () {
            // Hiển thị lớp
            this.container.style.display = '';
            this._isVisible = true;
        },

        play: function () {
            // Tiếp tục hoạt ảnh
            this.migrationInstance.play();
        },

        pause: function () {
            // Tạm dừng hoạt ảnh
            this.migrationInstance.pause();
        },

        destroy: function () {
            // Xóa lớp và các tài nguyên liên quan
            this.migrationInstance.clear();
            // Xóa container khỏi DOM
            if (this.container && this.container.parentNode) {
                this.container.parentNode.removeChild(this.container);
            }
            // Xóa các sự kiện đã đăng ký với bản đồ
            this._map.off('moveend');
            this._map.off('zoomstart');
            this._map.off('zoomend');
        }
    });

    // Hàm tạo ngắn gọn để khởi tạo L.MigrationLayer
    L.migrationLayer = function (options) {
        return new L.MigrationLayer(options);
    };

})(window);

Thẻ: leaflet JavaScript Canvas Vẽ bản đồ Hiệu ứng đồ họa

Đăng vào ngày 16 tháng 8 lúc 13:32