OpenLayers 要素动画

105 阅读6分钟

前言

要素移动动画类似于轨迹动画,只不过要素移动动画是在地图上起点和终点之间来回移动。通过使用渲染后事件(postrender)和向量上下文沿直线设置标记特征,在路径上实现要素移动。

1. 数据准备

提前准备好路线GeoJSON数据,然后使用相应方法读取数据。通过fetch方法读取GeoJSON文件,再通过response.json()方法读取出目标数据。

// 获取路线JSON数据
fetch("../../data/geojson/route.json").then(response => {
  // 读取GeoJSON数据
  response.json().then(res => {
    // 路线数据
    const route = new ol.format.GeoJSON().readGeometry(res.geometry)
  })
)

不知道如何获取GeoJSON路线数据的同学,可以参考之前的文章:OpenLayers 绘制几何对象打开浏览器控制台,点击线按钮进行绘制,绘制完成后,可以看到控制台中输出的信息即为GeoJSON路线数据。

2. 添加要素图层

创建路径要素routeFeature、起始标注要素startMarker、结束标注要素endMarker以及移动要素geoMarker,然后创建矢量图层并将其添加到地图中。

// 路线数据
const route new ol.format.GeoJSON().readGeometry(res.geometry)

// 创建Feature对象
const routeFeature new ol.Feature({
    geometry: route,
    type"route"
})

//起始标注
const startMarker new ol.Feature({
    type"icon",
    geometrynew ol.geom.Point(route.getFirstCoordinate())
})
//结束标注
const endMarker new ol.Feature({
    type"icon",
    geometrynew ol.geom.Point(route.getLastCoordinate())
})
//复制起始坐标
const position = startMarker.getGeometry().clone()
const geoMarker new ol.Feature({
    type"geoMarker",
    geometry: position
})
const styles = {
    'route'new ol.style.Style({
        strokenew ol.style.Stroke({
            color: [23721200.8],
            width6
        })
    }),
    "icon"new ol.style.Style({
        imagenew ol.style.Icon({
            anchor: [0.51],
            src"../../image/icon.png"
        })
    }),
    "geoMarker"new ol.style.Style({
        imagenew ol.style.Circle({
            radius7,
            fillnew ol.style.Fill({
                color"black"
            }),
            strokenew ol.style.Stroke({
                color"white",
                width2
            })
        })
    })
}
// 创建矢量图层
const vectorLayer new ol.layer.Vector({
    sourcenew ol.source.Vector({
        features: [routeFeature, geoMarker, startMarker, endMarker]
    }),
    style: (feature) => {
        return styles[feature.get("type")]
    }
})
map.addLayer(vectorLayer)

3. 要素动画函数

在该方法中要实现在地图上移动一个地理标记(如图标等)以实现动画效果。首先需要获取到当前移动的速度值,计算当前帧的时间,并计算持续时间和移动距离。更新位置对象坐标,在地图上绘制移动要素。最后使用map.render()方法继续渲染动画,使动画持续进行

// 监听移动事件
function moveFeature(event) {
    const speed = Number(speedInput.value)
    // 获取当前帧的时间
    const time = event.frameState.time
    // 计算自上次渲染以来经过的时间
    const elapsedTime = time - lastTime
    // 根据速度和经过的时间更新距离,并取模2,使距离在0-2之间循环
    distance = (distance + (speed * elapsedTime) / 1e6) % 2
    // 更新上次渲染时间
    lastTime = time
    // 根据当前的距离计算出对应的坐标,当距离大于1时,使用2减去距离,以实现来回移动的效果
    const currentCoordinate = route.getCoordinateAt(
        distance > 1 ? 2 - distance : distance
    )
    // 更新位置对象的坐标
    position.setCoordinates(currentCoordinate)

    // 获取向量上下文对象,用于绘制图形
    const vectorContext = ol.render.getVectorContext(event)
    // 设置绘制样式
    vectorContext.setStyle(styles.geoMarker)
    // 绘制位置对象
    vectorContext.drawGeometry(position)

    // 继续渲染动画,使动画持续进行
    map.render()
}

4. 控制动画方法

创建开始动画函数startAnimation和结束动画函数stopAnimation。在开始动画函数中更改按钮显示文本为"停止动画",然后添加要素移动监听事件moveFeature,并且移除原来的标注。在结束动画函数中更改按钮显示文本为"开始动画",然后移除要素移动监听事件moveFeature,并且更新标注对象在动画停止位置。最后添加开始按钮监听事件,用于控制动画开始和停止。

// 开始动画函数
function startAnimation() {
    animating = true
    lastTime = Date.now()
    startButton.textContent = "停止动画"
    // 添加要素移动监听事件
    vectorLayer.on("postrender", moveFeature)
    // 移除标注
    geoMarker.setGeometry(null)
}
// 结束动画函数
function stopAnimation() {
    animating = false
    startButton.textContent = "开始动画"
    // 保持标注在动画停止位置
    geoMarker.setGeometry(position)
    // 移除要素移动监听事件
    vectorLayer.un("postrender", moveFeature)
}
// 开始按钮监听事件
startButton.addEventListener('click'evt => {
    if (animating) {
        stopAnimation()
    } else {
        startAnimation()
    }
})

5. 完整代码

其中libs文件夹下的包需要更换为自己下载的本地包或者引用在线资源。

<!DOCTYPE html>
  <html>

  <head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>OpenLayers 要素动画</title>
  <meta charset="utf-8" />

  <link rel="stylesheet" href="../../libs/css/ol9.2.4.css">

  <script src="../../js/config.js"></script>
  <script src="../../libs/js/ol9.2.4.js"></script>
  <style>
  * {
    padding0;
margin0;
font-size14px;
font-family'微软雅黑';
}

html,
  body {
  width100%;
  height100%;
}

#map {
  position: absolute;
top50px;
bottom0;
width100%;
}

#top-content {
  position: absolute;
  width100%;
  height50px;
  line-height50px;
  backgroundlinear-gradient(135deg#ff00cc#ffcc00#00ffcc#ff0066);
  color#fff;
  text-align: center;
  font-size32px;
}

#top-content span {
  font-size32px;
}

.state {
  position: absolute;
  bottom10px;
  left50%;
  transformtranslateX(-50%);
  width20%;
  height30px;
  line-height30px;
  display: flex;
  justify-content: center;
  justify-items: center;
  border-radius5px;
  backgroundlinear-gradient(135deg#ff00cc#ffcc00#00ffcc#ff0066);
  color#fff;
}

.state-item {
  margin-right10px;
  text-align: center;
  font-size16px;
  font-weight: bold;
}

.start-move {
  padding2px 5px;
  transition: background-color 10s ease-in-out 10s;
  text-align: center;
  font-size14px;
  color#fff;
  background#377d466e;
  border-radius5px;
  border1px solid #50505040;

}

.move-speed {
  vertical-align: text-bottom;
}

.start-move:hover {
  cursor: pointer;
}
</style>
  </head>

  <body>
  <div id="top-content">
  <span>OpenLayers 要素动画</span>
  </div>
  <div id="map" title="地图显示"></div>
  <div class="state">
  <div class="state-item">
  <label for="">移动速度:</label>
  <input class="move-speed" type="range" min="10" max="100" step="10" value="70"></input>
        </div>
        <div class="state-item">
            <span class="start-move">开始移动</span>
        </div>
    </div>
</body>

</html>

<script>
    //地图投影坐标系
    const projection = ol.proj.get('EPSG:3857');
    //==============================================================================//
    //============================天地图服务参数简单介绍==============================//
    //================================vec:矢量图层==================================//
    //================================img:影像图层==================================//
    //================================cva:注记图层==================================//
    //======================其中:_c表示经纬度投影,_w表示球面墨卡托投影================//
    //==============================================================================//
    const TDTImgLayer = new ol.layer.Tile({
        title"天地图影像图层",
        sourcenew ol.source.XYZ({
            url"http://t0.tianditu.com/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" + TDTTOKEN,
            attibutions"天地图影像图层",
            crossOrigin"anoymous",
            wrapXfalse
        })
    })
    const TDTImgCvaLayer = new ol.layer.Tile({
        title"天地图矢量注记图层",
        sourcenew ol.source.XYZ({
            url"http://t0.tianditu.com/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=" + TDTTOKEN,
            attibutions"天地图矢量注记",
            crossOrigin"anoymous",
            wrapXfalse
        })
    })
    const map = new ol.Map({
        target"map",
        loadTilesWhileInteractingtrue,
        viewnew ol.View({
            center: [102.6508851156569524.995626734327274],
            zoom13.5,
            worldsWrapfalse,
            minZoom1,
            maxZoom20,
            projection'EPSG:4326',
        }),
        layers: [TDTImgLayerTDTImgCvaLayer],
        // 地图默认控件
        controls: ol.control.defaults.defaults({
            zoomtrue,
            attributiontrue,
            rotatetrue
        })
    })

    // 获取路线JSON数据
    fetch("../../data/geojson/route.json").then(response => {

        response.json().then(res => {
            // 路线数据
            const route = new ol.format.GeoJSON().readGeometry(res.geometry)

            // 创建Feature对象
            const routeFeature = new ol.Feature({
                geometry: route,
                type"route"
            })

            //起始标注
            const startMarker = new ol.Feature({
                type"icon",
                geometrynew ol.geom.Point(route.getFirstCoordinate())
            })
            //结束标注
            const endMarker = new ol.Feature({
                type"icon",
                geometrynew ol.geom.Point(route.getLastCoordinate())
            })

            //复制起始坐标
            const position = startMarker.getGeometry().clone()
            const geoMarker = new ol.Feature({
                type"geoMarker",
                geometry: position
            })
            const styles = {
                'route'new ol.style.Style({
                    strokenew ol.style.Stroke({
                        color: [23721200.8],
                        width6
                    })
                }),
                "icon"new ol.style.Style({
                    imagenew ol.style.Icon({
                        anchor: [0.51],
                        src"../../image/icon.png"
                    })
                }),
                "geoMarker"new ol.style.Style({
                    imagenew ol.style.Circle({
                        radius7,
                        fillnew ol.style.Fill({
                            color"black"
                        }),
                        strokenew ol.style.Stroke({
                            color"white",
                            width2
                        })
                    })
                })
            }
            // 创建矢量图层
            const vectorLayer = new ol.layer.Vector({
                sourcenew ol.source.Vector({
                    features: [routeFeature, geoMarker, startMarker, endMarker]
                }),
                style(feature) => {
                    return styles[feature.get("type")]
                }
            })
            map.addLayer(vectorLayer)

            const speedInput = document.querySelector(".move-speed")
            const startButton = document.querySelector(".start-move")
            let animating = false
            let distance = 0
            let lastTime = 0

            // 监听移动事件
            function moveFeature(event) {
                const speed = Number(speedInput.value)
                // 获取当前帧的时间
                const time = event.frameState.time
                // 计算自上次渲染以来经过的时间
                const elapsedTime = time - lastTime
                // 根据速度和经过的时间更新距离,并取模2,使距离在0-2之间循环
                distance = (distance + (speed * elapsedTime) / 1e6) % 2
                // 更新上次渲染时间
                lastTime = time
                // 根据当前的距离计算出对应的坐标,当距离大于1时,使用2减去距离,以实现来回移动的效果
                const currentCoordinate = route.getCoordinateAt(
                    distance > 1 ? 2 - distance : distance
                )
                // 更新位置对象的坐标
                position.setCoordinates(currentCoordinate)

                // 获取向量上下文对象,用于绘制图形
                const vectorContext = ol.render.getVectorContext(event)
                // 设置绘制样式
                vectorContext.setStyle(styles.geoMarker)
                // 绘制位置对象
                vectorContext.drawGeometry(position)

                // 继续渲染动画,使动画持续进行
                map.render()
            }
            // 开始动画函数
            function startAnimation() {
                animating = true
                lastTime = Date.now()
                startButton.textContent = "停止动画"
                // 添加要素移动监听事件
                vectorLayer.on("postrender", moveFeature)
                // 移除标注
                geoMarker.setGeometry(null)
            }
            // 结束动画函数
            function stopAnimation() {
                animating = false
                startButton.textContent = "开始动画"
                // 保持标注在动画停止位置
                geoMarker.setGeometry(position)
                // 移除要素移动监听事件
                vectorLayer.un("postrender", moveFeature)
            }
            // 开始按钮监听事件
            startButton.addEventListener('click'evt => {
                if (animating) {
                    stopAnimation()
                } else {
                    startAnimation()
                }
            })

        })
    })
</script>

OpenLayers示例数据下载,请回复关键字:ol数据

全国信息化工程师-GIS 应用水平考试资料,请回复关键字:GIS考试

【GIS之路】 已经接入了智能助手,欢迎关注,欢迎提问。

欢迎访问我的博客网站-长谈GIShttp://shanhaitalk.com

都看到这了,不要忘记点赞、收藏 + 关注

本号不定时更新有关 GIS开发 相关内容,欢迎关注 !