vue+openlayers+nodejs+postgis实现轨迹运动效果
概要
使用openlayers实现轨迹运动
整体架构流程
使用postgres(postgis)数据库以及nodejs作为后台,vue和openlayers做前端,openlayers使用http请求通过nodejs从postgres数据库获取数据。
技术名词解释
postgis:postgis是postgres的一个扩展,提供空间对象的相关操作。
技术细节
nodejs直连数据库,openlayers使用http服务通过nodejs转为数据库的查询语句。
实现思路如下:每条数据表示一条船,每个船的轨迹关键点在数据库存为MultiPointM的Geometry数据,其中M分量为时间戳,然后前端传入一个空间范围和时间戳,空间范围主要为了过滤范围外要素加速渲染,时间戳则用来查询所有船的轨迹点小于该时间戳的所有关键点,将其连成线,然后在时间戳所在的区间,使用线性插值插值出小船当前位置,线和插值出的点有相同的fid,在前端通过fid将线和插值点连接并显示,就是船的实时轨迹。
效果如下:
前端代码如下:
<template> <div id="map" class="map"></div> </template> <script> import * as ol from 'ol'; import 'ol/ol.css'; import proj from 'ol/proj' import { fromLonLat } from 'ol/proj'; import Map from 'ol/Map'; import View from 'ol/View'; import TileLayer from 'ol/layer/Tile'; import XYZ from 'ol/source/XYZ'; import Feature from 'ol/Feature'; import Point from 'ol/geom/Point'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style'; import WKB from 'ol/format/WKB'; import Icon from 'ol/style/Icon'; import { transformExtent } from 'ol/proj'; export default { name: 'OpenLayersMap', data() { return { map: null, pointLayer: null, lineLayer: null, linesData: [], pointsData: [], iconImagePath: '../../board.png', lastPoint: {} }; }, mounted() { this.initializeMap(); this.timeRange(); }, methods: { initializeMap() { this.map = new Map({ target: 'map', layers: [ new TileLayer({ source: new XYZ({ url: 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png', }), }), ], view: new View({ center: [0, 0], zoom: 2, }), }); this.lineLayer = new VectorLayer({ source: new VectorSource(), }); this.map.addLayer(this.lineLayer); this.pointLayer = new VectorLayer({ source: new VectorSource(), style: new Style({ image: new CircleStyle({ radius: 6, fill: new Fill({ color: 'red' }), stroke: new Stroke({ color: 'white', width: 2 }), }), }), }); this.map.addLayer(this.pointLayer); }, timeRange() { fetch('http://localhost:4325/time-range') .then(response => response.json()) .then(data => { const { minTime, maxTime } = data; console.log('Time Range:', minTime, maxTime); this.fetchDataInRange(minTime, maxTime); }) .catch(error => console.error('Error fetching time range:', error)); }, fetchDataInRange(startTime, endTime) { let currentTime = startTime; const timerId = setInterval(() => { if (currentTime >= endTime) { this.fetchData(endTime); clearInterval(timerId); // Stop the timer when currentTime >= endTime return; } this.fetchData(currentTime); currentTime += 5; // Increment currentTime //console.log('Current Time:', currentTime); }, 200); }, fetchData(currentTime) { // 获取地图视图 const mapView = this.map.getView(); // 获取地图视图的范围 const extent = mapView.calculateExtent(this.map.getSize()); // 将范围转换为EPSG:4326坐标系下的值 const bbox = transformExtent(extent, mapView.getProjection(), 'EPSG:4326'); Promise.all([ fetch(`http://localhost:4325/line-geometries?timestamp=${currentTime}&bbox=${bbox.join(',')}`).then(response => response.json()), fetch(`http://localhost:4325/points?timestamp=${currentTime}&bbox=${bbox.join(',')}`).then(response => response.json()) ]).then(([linesData, pointsData]) => { this.linesData = linesData; this.pointsData = pointsData; this.processData(); }).catch(error => console.error('Error fetching data:', error)); }, processData() { const lineSource = this.lineLayer.getSource(); const pointSource = this.pointLayer.getSource(); const existingLineFeatureIds = {}; const existingPointFeatureIds = {}; // 处理线要素数据 //console.log('this.linesData', this.linesData) this.linesData.forEach(line => { const fid = line.fid; let feature = lineSource.getFeatureById(fid); if (feature) { // 如果已存在具有相同 fid 的要素,则更新要素信息 // 更新要素信息 existingLineFeatureIds[fid] = true; } else { // 否则创建新的要素并添加到图层中 feature = new Feature({ // 设置要素信息 }); lineSource.addFeature(feature); existingLineFeatureIds[fid] = true; } }); // 处理点要素数据 this.pointsData.forEach(point => { const fid = point.fid; let feature = pointSource.getFeatureById(fid); if (feature) { // 如果已存在具有相同 fid 的要素,则更新要素信息 // 更新要素信息 existingPointFeatureIds[fid] = true; } else { // 否则创建新的要素并添加到图层中 feature = new Feature({ // 设置要素信息 }); pointSource.addFeature(feature); existingPointFeatureIds[fid] = true; } }); // 移除地图上已存在但未在当前数据中出现的线要素 lineSource.getFeatures().forEach(feature => { const fid = feature.getId(); if (!existingLineFeatureIds[fid]) { lineSource.removeFeature(feature); } }); // 移除地图上已存在但未在当前数据中出现的点要素 pointSource.getFeatures().forEach(feature => { const fid = feature.getId(); if (!existingPointFeatureIds[fid]) { pointSource.removeFeature(feature); } }); // Create a mapping of fid to points const pointsMap = {}; this.pointsData.forEach(point => { if (!pointsMap[point.fid]) { pointsMap[point.fid] = []; } pointsMap[point.fid].push(point); }); // Process lines and append points if they exist this.linesData.forEach(line => { const format = new WKB(); const feature = format.readFeature(line.line_geom, { dataProjection: 'EPSG:4326', featureProjection: 'EPSG:3857' }); const geometry = feature.getGeometry(); if (geometry.getType() === 'LineString' && pointsMap[line.fid]) { const coordinates = geometry.getCoordinates(); pointsMap[line.fid].forEach(point => { const coord = fromLonLat([point.interpolated_longitude, point.interpolated_latitude]); coordinates.push(coord); }); geometry.setCoordinates(coordinates); } //feature.setId(line.fid); this.lineLayer.getSource().addFeature(feature); }); // Log for debugging //console.log('Processed Lines:', this.lineLayer.getSource().getFeatures()); //console.log('Processed Points:', this.pointLayer.getSource().getFeatures()); this.processPointLayer(); }, processPointLayer() { const tempLastPoint = {}; const lineFeatures = this.lineLayer.getSource().getFeatures(); lineFeatures.forEach(lineFeature => { const lineGeometry = lineFeature.getGeometry(); const lineCoordinates = lineGeometry.getCoordinates(); const numCoordinates = lineCoordinates.length; //const fid = lineFeature.getId(); //console.log('fid', fid); if (numCoordinates === 1) { const defaultAngle = 0; const lastPointCoords = lineCoordinates[0]; tempLastPoint[fid] = lastPointCoords; const pointFeature = new Feature({ geometry: new Point(lineCoordinates[0]), }); //pointFeature.setId(fid); const iconStyle = this.createPointStyle(defaultAngle); pointFeature.setStyle(iconStyle); this.pointLayer.getSource().addFeature(pointFeature); } else if (numCoordinates > 1) { const lastPointCoords = lineCoordinates[numCoordinates - 1]; //console.log('lastPointCoords', lastPointCoords); const penultimatePointCoords = lineCoordinates[numCoordinates - 2]; const dx = lastPointCoords[0] - penultimatePointCoords[0]; const dy = lastPointCoords[1] - penultimatePointCoords[1]; const angle = Math.atan2(dy, dx); const pointFeature = new Feature({ geometry: new Point(lastPointCoords), }); //pointFeature.setId(fid); const iconStyle = this.createPointStyle(angle); pointFeature.setStyle(iconStyle); this.pointLayer.getSource().addFeature(pointFeature); //const tempLastPointCoords = this.lastPoint[fid]; //console.log('tempLastPointCoords', tempLastPointCoords); //if (tempLastPointCoords) { //console.log('animate point', lineFeature.getId(), this.lastPoint[lineFeature.getId()], lastPointCoords); //this.animatePoint(pointFeature, tempLastPointCoords, lastPointCoords); //} //tempLastPoint[fid] = lastPointCoords; } }); //this.lastPoint = tempLastPoint; //console.log('lastPoint', this.lastPoint); //console.log('tempLastPoint', tempLastPoint); }, animatePoint(feature, startCoords, endCoords) { const duration = 800; // 动画持续时间,单位毫秒 const start = performance.now(); //console.log('startCoords', startCoords); const animate = (timestamp) => { const elapsed = timestamp - start; const progress = Math.min(elapsed / duration, 1); // 进度百分比,范围从0到1 // 线性插值计算当前位置 const currentCoords = [ startCoords[0] + (endCoords[0] - startCoords[0]) * progress, startCoords[1] + (endCoords[1] - startCoords[1]) * progress, ]; feature.setGeometry(new Point(currentCoords)); if (progress < 1) { requestAnimationFrame(animate); } }; requestAnimationFrame(animate); }, createPointStyle(angle) { // 根据朝向创建点的样式 return new Style({ image: new Icon({ src: require('@/assets/board.png'), scale: 0.1, rotation: -angle + (180 * Math.PI / 180), // 设置点的朝向 anchor: [0.5, 0.7], // 设置锚点位置 }), }); } }, }; </script> <style scoped> .map { width: 100%; height: 800px; } </style>
服务器代码如下:
1、数据库相关:
// database.js const { Client } = require('pg'); const axios = require('axios'); const fs = require('fs').promises; const moment = require('moment-timezone'); // 配置数据库连接 const client = new Client({ user: 'postgres', host: 'loaclhost', database: 'postgres', password: 'root', port: 4321, // 默认PostgreSQL端口 }); async function createTable() { const createTableQuery = ` CREATE TABLE IF NOT EXISTS track_board_test ( fid BIGINT PRIMARY KEY, id VARCHAR(255), name VARCHAR(255), mmsi VARCHAR(255), geom GEOMETRY(MultiPointM) ); CREATE INDEX IF NOT EXISTS geom_index ON track_board_test USING GIST (geom); `; try { await client.query(createTableQuery); console.log('Table created successfully'); } catch (err) { console.error('Error creating table:', err.stack); } } async function insertDataFromFile(filePath, isHttp) { try { let data; if (isHttp) { const response = await axios.get(filePath); data = response.data; } else { const rawData = await fs.readFile(filePath); data = JSON.parse(rawData); } for (const item of data.data) { const { id, mmsi, name, hisRecord } = item; let fid; if (id.startsWith("radar")) { fid = parseInt(id.substring("radar".length)); } else { fid = parseInt(id); } const points = hisRecord.map(record => { const utcTime = moment.tz(record.updateTime, "Asia/Shanghai").utc().format('YYYY-MM-DD HH:mm:ss'); return `ST_SetSRID(ST_MakePointM(${record.longitude}, ${record.latitude}, EXTRACT(EPOCH FROM TIMESTAMP '${utcTime}')), 4326)`; }).join(', '); const geom = `ST_Collect(ARRAY[${points}])`; const query = ` INSERT INTO track_board_test (id, name, mmsi, geom, fid) VALUES ($1, $2, $3, ${geom}, $4) ON CONFLICT (fid) DO UPDATE SET id = EXCLUDED.id, name = EXCLUDED.name, mmsi = EXCLUDED.mmsi, geom = EXCLUDED.geom, fid = EXCLUDED.fid; `; await client.query(query, [id, name, mmsi, fid]); } console.log('数据插入成功'); } catch (err) { console.error('插入数据时发生错误:', err); } } async function insertRandomData() { const insertRandomDataQuery = ` DO $$ DECLARE i INT; BEGIN FOR i IN 10010000..10015000 LOOP EXECUTE format( 'INSERT INTO track_board_test (id, geom, fid) VALUES (%L, (SELECT ST_Collect( ARRAY( WITH RECURSIVE points AS ( SELECT random() * 360 - 180 AS lon, random() * 180 - 90 AS lat, CAST(1716186468 + random() * 1000 AS INT) AS m, 1 AS iteration, CEIL(random() * 99 + 1) AS max_iterations -- 随机生成1到100之间的点数 UNION ALL SELECT lon + (0.01 + random() * 0.09) * (CASE WHEN random() < 0.5 THEN 1 ELSE -1 END) AS lon, lat + (0.01 + random() * 0.09) * (CASE WHEN random() < 0.5 THEN 1 ELSE -1 END) AS lat, CAST(m + random() * 400 AS INT) AS m, iteration + 1, max_iterations FROM points WHERE iteration < max_iterations ) SELECT ST_SetSRID(ST_MakePointM(lon, lat, m), 4326) FROM points ) )), %L ) ON CONFLICT (fid) DO NOTHING', 'radar_' || i, i ); END LOOP; END $$; `; try { await client.query(insertRandomDataQuery); console.log('Random data insert successfully'); } catch (err) { console.error('Error inserting random data:', err.stack); } } async function getAllData() { try { const query = ` SELECT fid, id, name, mmsi, ST_X(dp.geom) AS Lng, ST_Y(dp.geom) AS Lat, ST_M(dp.geom) AS time FROM track_board_test, LATERAL ST_DumpPoints(geom) AS dp; `; const result = await client.query(query); return result.rows; } catch (err) { console.error('Error fetching data:', err.stack); return []; } } async function getTimeRange() { try { const query = ` SELECT MAX(max_time) AS max_time, MIN(min_time) AS min_time FROM ( SELECT (SELECT MAX(ST_M(dp.geom)) FROM LATERAL ST_DumpPoints(track_board_test.geom) AS dp) AS max_time, (SELECT MIN(ST_M(dp.geom)) FROM LATERAL ST_DumpPoints(track_board_test.geom) AS dp) AS min_time FROM track_board_test ) AS subquery; `; const result = await client.query(query); const { max_time, min_time } = result.rows[0]; return { minTime: min_time, maxTime: max_time }; } catch (err) { console.error('Error executing query', err.stack); throw err; } } async function getPointsByTimestamp(timestamp, bbox) { try { const query = ` WITH extracted_points AS ( SELECT tbt.fid, (dp).geom AS point, ST_M((dp).geom) AS m_value FROM track_board_test tbt CROSS JOIN LATERAL ST_DumpPoints(tbt.geom) AS dp WHERE ST_Intersects(tbt.geom, ST_MakeEnvelope($1, $2, $3, $4, 4326)) -- Add bbox filter ORDER BY fid ), min_max_times AS ( SELECT fid, MAX(CASE WHEN m_value <= $5 THEN m_value END) AS min_time, MIN(CASE WHEN m_value > $5 THEN m_value END) AS max_time FROM extracted_points GROUP BY fid ), min_points AS ( SELECT ep.fid, ep.m_value AS min_time, ep.point AS min_point FROM extracted_points ep JOIN min_max_times mmt ON ep.fid = mmt.fid AND ep.m_value = mmt.min_time ), max_points AS ( SELECT ep.fid, ep.m_value AS max_time, ep.point AS max_point FROM extracted_points ep JOIN min_max_times mmt ON ep.fid = mmt.fid AND ep.m_value = mmt.max_time ) SELECT mmt.fid, ST_X(ST_LineInterpolatePoint(ST_MakeLine(mp.min_point, mx.max_point), ($5 - mmt.min_time) / (mmt.max_time - mmt.min_time))) AS interpolated_longitude, ST_Y(ST_LineInterpolatePoint(ST_MakeLine(mp.min_point, mx.max_point), ($5 - mmt.min_time) / (mmt.max_time - mmt.min_time))) AS interpolated_latitude FROM min_max_times mmt JOIN min_points mp ON mmt.fid = mp.fid JOIN max_points mx ON mmt.fid = mx.fid; `; const result = await client.query(query, [...bbox, timestamp]); return result.rows; } catch (err) { console.error('Error fetching interpolated points:', err.stack); return []; } } async function getLineGeometries(timestamp, bbox) { const query = ` WITH extracted_points AS ( SELECT fid, (ST_DumpPoints(geom)).geom AS point FROM track_board_test WHERE ST_Intersects(geom, ST_MakeEnvelope($1, $2, $3, $4, 4326)) -- Add bbox filter ), filtered_points AS ( SELECT fid, point, ST_M(point) AS m_value FROM extracted_points WHERE ST_M(point) <= $5 ), sorted_points AS ( SELECT fid, point FROM filtered_points ORDER BY fid, m_value ) SELECT fid, ST_MakeLine(point) AS line_geom FROM sorted_points GROUP BY fid; `; const result = await client.query(query, [...bbox, timestamp]); return result.rows; } module.exports = { client, createTable, insertDataFromFile, insertRandomData, getAllData, getTimeRange, getPointsByTimestamp, getLineGeometries };
http接口相关:
const express = require('express'); const cors = require('cors'); const { client, createTable, insertDataFromFile, insertRandomData, getAllData, getTimeRange, getPointsByTimestamp, getLineGeometries } = require('./database'); const app = express(); app.use(cors()); const port = 4325; client.connect() .then(() => console.log('Connected to the database')) .catch(err => console.error('Connection error', err.stack)); createTable(); const filePath = './test.json'; // 替换为你的文件路径 insertDataFromFile(filePath, false); insertRandomData(); app.get('/all-data', async (req, res) => { try { const data = await getAllData(); res.json(data); } catch (err) { res.status(500).json({ error: 'Internal Server Error' }); } }); // 创建一个API端点 app.get('/time-range', async (req, res) => { try { const { minTime, maxTime } = await getTimeRange(); res.json({ minTime, maxTime }); } catch (err) { console.error('Error fetching time range:', err.stack); res.status(500).json({ error: 'Internal Server Error' }); } }); app.get('/points', async (req, res) => { const timestamp = req.query.timestamp; const bbox = req.query.bbox.split(',').map(parseFloat); // 解析 bbox 参数为数组 if (!timestamp) { return res.status(400).json({ error: 'Timestamp is required' }); } try { const points = await getPointsByTimestamp(timestamp, bbox); // 将 bbox 参数传递给函数 res.json(points); } catch (err) { res.status(500).json({ error: 'Internal Server Error' }); } }); app.get('/line-geometries', async (req, res) => { const timestamp = req.query.timestamp; const bbox = req.query.bbox.split(',').map(parseFloat); // 解析 bbox 参数为数组 if (!timestamp) { return res.status(400).json({ error: 'Timestamp is required' }); } try { const lineGeometries = await getLineGeometries(timestamp, bbox); // 将 bbox 参数传递给函数 res.json(lineGeometries); } catch (err) { console.error('Error fetching line geometries:', err.stack); res.status(500).json({ error: 'Internal Server Error' }); } }); // 启动服务器 app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });
小结
当显示全球范围性能会有明显卡顿,可能需要改进算法。
到此这篇关于vue+openlayers+nodejs+postgis实现轨迹运动的文章就介绍到这了,更多相关vue轨迹运动内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Vue3全局属性app.config.globalProperties的实现
Vue3中的app.config.globalProperties是一个强大的全局配置功能,允许我们在应用级别设置和访问属性,本文主要介绍了Vue3全局属性app.config.globalProperties的实现,具有一定的参考价值,感兴趣的可以了解一下2024-01-01vue单页面打包文件大?首次加载慢?nginx带你飞,从7.5M到1.3M蜕变过程(推荐)
这篇文章主要介绍了vue单页面打包文件大?首次加载慢?nginx带你飞,从7.5M到1.3M蜕变过程,需要的朋友可以参考下2018-01-01element-ui 限制日期选择的方法(datepicker)
本篇文章主要介绍了element-ui 限制日期选择的方法(datepicker),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧2018-05-05
最新评论