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实践课程系列的一部分,欢迎关注收藏

相关推荐
铁皮饭盒9 分钟前
bun直接tsx,优雅!
javascript·后端
Csvn2 小时前
Monorepo 迁移血泪史:从 Multi-Repo 到 Turborepo,这 3 个坑我帮你踩完了
前端
星栈2 小时前
Dioxus 多页面怎么做:`dioxus-router`、嵌套路由、`Outlet` 和页面组织,一篇给你讲顺
前端·rust·前端框架
用户987409238872 小时前
用 Remotion + edge-tts 打造中文教学视频全自动流水线
前端
风骏时光牛马2 小时前
Less前端工程化实战:变量混合器与项目样式分层落地
前端
假如让我当三天老蒯2 小时前
Options API(选项式 API) 和 Composition API(组合式 API)
前端·vue.js·面试
SameX2 小时前
iOS 独立开发实践:用 MapKit + 像素渲染实现 Citywalk 轨迹地图 App「雁过留痕」
前端
_柳青杨2 小时前
一文吃透 Node.js 事件循环:从原理到 Node 20+ 重大变更
javascript·后端
skyey2 小时前
页面加载时,深色模式闪白的问题解决
前端
IT_陈寒3 小时前
Java 并行流把我坑惨了,这6小时加班值了
前端·人工智能·后端