MapLibre GL JS第20课:更新GeoJSON多边形

📌 学习目标

  • 掌握更新GeoJSON多边形的实现方法
  • 理解相关API的使用
  • 能够独立完成类似功能开发

🎯 核心概念

使用可更新的GeoJSONVT更新GeoJSON多边形。

💻 完 整 代 码

代码示例

js 复制代码
const map = new maplibregl.Map({
    container: 'map',
    style: 'https://demotiles.maplibre.org/style.json',
    center: [-68.13734351262877, 45.137451890638886],
    zoom: 5
});
map.showTileBoundaries = true;

const rectangles = Array.from({length: 5}, (_, i) => ({
    id: i,
    x: -68.13 + (Math.random() - 0.5) * 5,
    y: 45.13 + (Math.random() - 0.5) * 5,
    vx: (Math.random() - 0.5) * 0.05,
    vy: (Math.random() - 0.5) * 0.05,
    w: 0.5 + Math.random(),
    h: 0.5 + Math.random(),
    rotation: Math.random() * 2 * Math.PI,
    rotationSpeed: (Math.random() - 0.5) * 0.1,
    color: '#008888',
    every: Math.round(Math.random() * 100 + 200)
}));

const getRandomColor = () => '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');

function getRectangleGeometry({x, y, w, h, rotation}) {
    const c = Math.cos(rotation), s = Math.sin(rotation);
    const hw = w / 2, hh = h / 2;
    const coords = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]]
        .map(([dx, dy]) => [x + dx * c - dy * s, y + dx * s + dy * c]);
    coords.push(coords[0]);
    return {type: 'Polygon', coordinates: [coords]};
}

map.on('load', () => {
    const features = rectangles.flatMap(rect => [
        {type: 'Feature', id: rect.id, properties: {color: rect.color}, geometry: getRectangleGeometry(rect)},
        {type: 'Feature', id: `${rect.id}_label`, properties: {label: `Zoom: ${Math.round(map.getZoom())}`}, geometry: {type: 'Point', coordinates: [rect.x, rect.y]}}
    ]);

    map.addSource('rectangles', {type: 'geojson', data: {type: 'FeatureCollection', features}});

    map.addLayer({
        id: 'rectangles', type: 'fill', source: 'rectangles',
        paint: {'fill-color': ['get', 'color'], 'fill-opacity': 0.8},
        filter: ['==', '$type', 'Polygon']
    });

    map.addLayer({
        id: 'rectangles-label', type: 'symbol', source: 'rectangles',
        layout: {'text-field': ['get', 'label'], 'text-size': 14, 'text-allow-overlap': true, 'text-ignore-placement': true},
        paint: {'text-color': '#ffffff'},
        filter: ['==', '$type', 'Point']
    });

    let count = 0;
    function animate() {
        const zoom = map.getZoom().toFixed(1);
        count++;

        const updates = rectangles.flatMap(rect => {
            rect.x += rect.vx; rect.y += rect.vy; rect.rotation += rect.rotationSpeed;
            if (rect.x < -75 || rect.x > -60) rect.vx *= -1;
            if (rect.y < 40 || rect.y > 50) rect.vy *= -1;
            if (count % rect.every === 0) rect.color = getRandomColor();

            return [
                {id: rect.id, newGeometry: getRectangleGeometry(rect), addOrUpdateProperties: [{key: 'color', value: rect.color}]},
                {id: `${rect.id}_label`, newGeometry: {type: 'Point', coordinates: [rect.x, rect.y]}, addOrUpdateProperties: [{key: 'label', value: zoom}]}
            ];
        });

        map.getSource('rectangles')?.updateData({update: updates});
        requestAnimationFrame(animate);
    }
    animate();
});

代码示例

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <title>Update GeoJSON polygons</title>
    <meta property="og:description" content="使用可更新的 GeoJSONVT 更新 GeoJSON 多边形" />
    <meta property="og:created" content="2026-03-01" />
    <meta charset='utf-8'>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel='stylesheet' href='https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.css' />
    <script src='https://unpkg.com/maplibre-gl@5.24.0/dist/maplibre-gl.js'></script>
    <style>
        body { margin: 0; padding: 0; }
        html, body, #map { height: 100%; }
    </style>
</head>
<body>
<div id="map"></div>
<script>
    const map = new maplibregl.Map({
        container: 'map',
        style: 'https://demotiles.maplibre.org/style.json',
        center: [-68.13734351262877, 45.137451890638886],
        zoom: 5
    });
    map.showTileBoundaries = true;

    const rectangles = Array.from({length: 5}, (_, i) => ({
        id: i,
        x: -68.13 + (Math.random() - 0.5) * 5,
        y: 45.13 + (Math.random() - 0.5) * 5,
        vx: (Math.random() - 0.5) * 0.05,
        vy: (Math.random() - 0.5) * 0.05,
        w: 0.5 + Math.random(),
        h: 0.5 + Math.random(),
        rotation: Math.random() * 2 * Math.PI,
        rotationSpeed: (Math.random() - 0.5) * 0.1,
        color: '#008888',
        every: Math.round(Math.random() * 100 + 200)
    }));

    const getRandomColor = () => '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');

    function getRectangleGeometry({x, y, w, h, rotation}) {
        const c = Math.cos(rotation), s = Math.sin(rotation);
        const hw = w / 2, hh = h / 2;
        const coords = [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]]
            .map(([dx, dy]) => [x + dx * c - dy * s, y + dx * s + dy * c]);
        coords.push(coords[0]);
        return {type: 'Polygon', coordinates: [coords]};
    }

    map.on('load', () => {
        const features = rectangles.flatMap(rect => [
            {type: 'Feature', id: rect.id, properties: {color: rect.color}, geometry: getRectangleGeometry(rect)},
            {type: 'Feature', id: `${rect.id}_label`, properties: {label: `Zoom: ${Math.round(map.getZoom())}`}, geometry: {type: 'Point', coordinates: [rect.x, rect.y]}}
        ]);

        map.addSource('rectangles', {type: 'geojson', data: {type: 'FeatureCollection', features}});

        map.addLayer({
            id: 'rectangles', type: 'fill', source: 'rectangles',
            paint: {'fill-color': ['get', 'color'], 'fill-opacity': 0.8},
            filter: ['==', '$type', 'Polygon']
        });

        map.addLayer({
            id: 'rectangles-label', type: 'symbol', source: 'rectangles',
            layout: {'text-field': ['get', 'label'], 'text-size': 14, 'text-allow-overlap': true, 'text-ignore-placement': true},
            paint: {'text-color': '#ffffff'},
            filter: ['==', '$type', 'Point']
        });

        let count = 0;
        function animate() {
            const zoom = map.getZoom().toFixed(1);
            count++;

            const updates = rectangles.flatMap(rect => {
                rect.x += rect.vx; rect.y += rect.vy; rect.rotation += rect.rotationSpeed;
                if (rect.x < -75 || rect.x > -60) rect.vx *= -1;
                if (rect.y < 40 || rect.y > 50) rect.vy *= -1;
                if (count % rect.every === 0) rect.color = getRandomColor();

                return [
                    {id: rect.id, newGeometry: getRectangleGeometry(rect), addOrUpdateProperties: [{key: 'color', value: rect.color}]},
                    {id: `${rect.id}_label`, newGeometry: {type: 'Point', coordinates: [rect.x, rect.y]}, addOrUpdateProperties: [{key: 'label', value: zoom}]}
                ];
            });

            map.getSource('rectangles')?.updateData({update: updates});
            requestAnimationFrame(animate);
        }
        animate();
    });
</script>
</body>
</html>

🔍 代码解析

1. 初始化地图

使用 new maplibregl.Map() 创建地图实例,配置基本参数。启用 showTileBoundaries 显示瓦片边界。

2. 创建矩形数据

生成5个随机矩形,包含位置、速度、尺寸、旋转角度等属性。

3. 几何计算函数

getRectangleGeometry() 函数根据矩形参数计算旋转后的坐标,使用三角函数进行旋转变换。

4. 添加数据源和图层

创建GeoJSON数据源,包含多边形和标签点要素,添加 fillsymbol 图层。

5. 动画循环

使用 requestAnimationFrame 实现平滑动画,通过 updateData() 方法高效更新要素。

6. 边界检测

矩形到达边界时反弹(反转速度方向),并随机改变颜色。

⚙️ 参数说明

参数 类型 必填 说明
container string 地图容器ID
style string 地图样式URL
center number, number 初始中心点,默认0, 0
zoom number 初始缩放级别,默认0

updateData 方法参数

参数 类型 说明
update array 要素更新数组
update\[\].id string/number 要素ID
update\[\].newGeometry object 新的几何对象
update\[\].addOrUpdateProperties array 属性更新数组

🎨 效果说明

运行代码后:

  • 地图显示美国缅因州区域(中心点 -68.14°W, 45.14°N)
  • 5个半透明矩形在地图上移动、旋转
  • 矩形到达边界时自动反弹
  • 矩形颜色随机变化
  • 每个矩形中心显示当前缩放级别标签
  • 瓦片边界可见(便于调试)

💡 常 见 问 题

Q1: updateData 和 setData 有什么区别?

A: updateData 是增量更新,只更新变化的要素;setData 是全量替换。

Q2: 如何暂停/恢复动画?

A: 可以使用 cancelAnimationFrame() 暂停,重新调用 animate() 恢复。

Q3: 旋转矩形的几何计算原理是什么?

A: 使用旋转变换矩阵:x' = x*cosθ - y*sinθ, y' = x*sinθ + y*cosθ

📝 练习任务

  1. 基础练习:修改矩形数量和初始颜色
  2. 进阶挑战:添加暂停/播放按钮控制动画
  3. 拓展思考:如何实现矩形碰撞检测?

🌟 最佳实践

  1. 增量更新 : 使用 updateData 替代 setData,提升性能
  2. 动画优化 : 使用 requestAnimationFrame 确保流畅的60fps动画
  3. 内存管理: 避免在动画循环中创建新对象
  4. 边界处理: 合理设置边界条件,避免要素移出可视区域

🔗 延伸阅读


本文是MapLibre GL JS实践课程系列的一部分,欢迎关注收藏

相关推荐
晴天161 小时前
前端 postMessage 使用场景
前端·javascript·网络
zhanghaha13142 小时前
HTML系列教程:15_标签 全称 + 简写 超详细讲解(新手专用)
前端·html
zhanghaha13142 小时前
HTML系列教程:14_HTML 表单与输入框 <form>、<input> 零基础详解
java·前端·javascript
IMPYLH2 小时前
HTML 的 <samp> 元素
前端·网络·html
IMPYLH2 小时前
HTML 的 <script> 元素
前端·html
weixin_493503677 小时前
Vue3 前端生成 PDF:会员证书与活动签到表的三种打印方案与踩坑记录
前端·pdf·状态模式
李少兄8 小时前
JavaScript 数据类型完全指南
开发语言·javascript·ecmascript
尾善爱看海10 小时前
前端算法与手写题集
前端·算法
徐小夕10 小时前
JitWord 4.0 万字分享:从协同工具到AI Word操作系统,聊聊3年产品创业史
前端·vue.js·后端
shmily麻瓜小菜鸡11 小时前
JS/TS 易踩坑知识点 — 模块化与工程化类
开发语言·javascript·ecmascript