OpenLayers 要素动画

关注我,带你一起学GIS ^

注:当前使用的是 ol [9.2.4] 版本,天地图使用的key请到天地图官网申请,并替换为自己的key

前言

要素移动动画类似于轨迹动画,只不过要素移动动画是在地图上起点和终点之间来回移动。通过使用渲染后事件(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",
    geometry: new ol.geom.Point(route.getFirstCoordinate())
})
//结束标注
const endMarker = new ol.Feature({
    type"icon",
    geometry: new 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({
        stroke: new ol.style.Stroke({
            color: [237, 212, 0, 0.8],
            width: 6
        })
    }),
    "icon": new ol.style.Style({
        image: new ol.style.Icon({
            anchor: [0.5, 1],
            src: "../../image/icon.png"
        })
    }),
    "geoMarker": new ol.style.Style({
        image: new ol.style.Circle({
            radius: 7,
            fill: new ol.style.Fill({
                color: "black"
            }),
            stroke: new ol.style.Stroke({
                color: "white",
                width: 2
            })
        })
    })
}
// 创建矢量图层
const vectorLayer = new ol.layer.Vector({
    source: new 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>
  * {
    padding: 0;
margin: 0;
font-size: 14px;
font-family: '微软雅黑';
}

html,
  body {
  width: 100%;
  height: 100%;
}

#map {
  position: absolute;
top: 50px;
bottom: 0;
width: 100%;
}

#top-content {
  position: absolute;
  width: 100%;
  height: 50px;
  line-height: 50px;
  background: linear-gradient(135deg, #ff00cc, #ffcc00, #00ffcc, #ff0066);
  color: #fff;
  text-align: center;
  font-size: 32px;
}

#top-content span {
  font-size: 32px;
}

.state {
  position: absolute;
  bottom: 10px;
  left: 50%;
  transform: translateX(-50%);
  width: 20%;
  height: 30px;
  line-height: 30px;
  display: flex;
  justify-content: center;
  justify-items: center;
  border-radius: 5px;
  background: linear-gradient(135deg, #ff00cc, #ffcc00, #00ffcc, #ff0066);
  color: #fff;
}

.state-item {
  margin-right: 10px;
  text-align: center;
  font-size: 16px;
  font-weight: bold;
}

.start-move {
  padding: 2px 5px;
  transition: background-color 10s ease-in-out 10s;
  text-align: center;
  font-size: 14px;
  color: #fff;
  background: #377d466e;
  border-radius: 5px;
  border: 1px 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: "天地图影像图层",
        source: new ol.source.XYZ({
            url: "http://t0.tianditu.com/DataServer?T=img_w&x={x}&y={y}&l={z}&tk=" + TDTTOKEN,
            attibutions: "天地图影像图层",
            crossOrigin: "anoymous",
            wrapX: false
        })
    })
    const TDTImgCvaLayer = new ol.layer.Tile({
        title: "天地图矢量注记图层",
        source: new ol.source.XYZ({
            url: "http://t0.tianditu.com/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk=" + TDTTOKEN,
            attibutions: "天地图矢量注记",
            crossOrigin: "anoymous",
            wrapX: false
        })
    })
    const map = new ol.Map({
        target: "map",
        loadTilesWhileInteracting: true,
        view: new ol.View({
            center: [102.65088511565695, 24.995626734327274],
            zoom: 13.5,
            worldsWrap: false,
            minZoom: 1,
            maxZoom: 20,
            projection: 'EPSG:4326',
        }),
        layers: [TDTImgLayer, TDTImgCvaLayer],
        // 地图默认控件
        controls: ol.control.defaults.defaults({
            zoom: true,
            attribution: true,
            rotate: true
        })
    })

    // 获取路线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",
                geometry: new ol.geom.Point(route.getFirstCoordinate())
            })
            //结束标注
            const endMarker = new ol.Feature({
                type"icon",
                geometry: new 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({
                    stroke: new ol.style.Stroke({
                        color: [237, 212, 0, 0.8],
                        width: 6
                    })
                }),
                "icon": new ol.style.Style({
                    image: new ol.style.Icon({
                        anchor: [0.5, 1],
                        src: "../../image/icon.png"
                    })
                }),
                "geoMarker": new ol.style.Style({
                    image: new ol.style.Circle({
                        radius: 7,
                        fill: new ol.style.Fill({
                            color: "black"
                        }),
                        stroke: new ol.style.Stroke({
                            color: "white",
                            width: 2
                        })
                    })
                })
            }
            // 创建矢量图层
            const vectorLayer = new ol.layer.Vector({
                source: new 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开发  相关内容,欢迎关注 

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部