vue+openlayers+nodejs+postgis實現(xiàn)軌跡運動效果
概要
使用openlayers實現(xiàn)軌跡運動
整體架構(gòu)流程
使用postgres(postgis)數(shù)據(jù)庫以及nodejs作為后臺,vue和openlayers做前端,openlayers使用http請求通過nodejs從postgres數(shù)據(jù)庫獲取數(shù)據(jù)。
技術(shù)名詞解釋
postgis:postgis是postgres的一個擴(kuò)展,提供空間對象的相關(guān)操作。
技術(shù)細(xì)節(jié)
nodejs直連數(shù)據(jù)庫,openlayers使用http服務(wù)通過nodejs轉(zhuǎn)為數(shù)據(jù)庫的查詢語句。
實現(xiàn)思路如下:每條數(shù)據(jù)表示一條船,每個船的軌跡關(guān)鍵點在數(shù)據(jù)庫存為MultiPointM的Geometry數(shù)據(jù),其中M分量為時間戳,然后前端傳入一個空間范圍和時間戳,空間范圍主要為了過濾范圍外要素加速渲染,時間戳則用來查詢所有船的軌跡點小于該時間戳的所有關(guān)鍵點,將其連成線,然后在時間戳所在的區(qū)間,使用線性插值插值出小船當(dāng)前位置,線和插值出的點有相同的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()); // 將范圍轉(zhuǎn)換為EPSG:4326坐標(biāo)系下的值 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 = {}; // 處理線要素數(shù)據(jù) //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 { // 否則創(chuàng)建新的要素并添加到圖層中 feature = new Feature({ // 設(shè)置要素信息 }); lineSource.addFeature(feature); existingLineFeatureIds[fid] = true; } }); // 處理點要素數(shù)據(jù) this.pointsData.forEach(point => { const fid = point.fid; let feature = pointSource.getFeatureById(fid); if (feature) { // 如果已存在具有相同 fid 的要素,則更新要素信息 // 更新要素信息 existingPointFeatureIds[fid] = true; } else { // 否則創(chuàng)建新的要素并添加到圖層中 feature = new Feature({ // 設(shè)置要素信息 }); pointSource.addFeature(feature); existingPointFeatureIds[fid] = true; } }); // 移除地圖上已存在但未在當(dāng)前數(shù)據(jù)中出現(xiàn)的線要素 lineSource.getFeatures().forEach(feature => { const fid = feature.getId(); if (!existingLineFeatureIds[fid]) { lineSource.removeFeature(feature); } }); // 移除地圖上已存在但未在當(dāng)前數(shù)據(jù)中出現(xiàn)的點要素 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; // 動畫持續(xù)時間,單位毫秒 const start = performance.now(); //console.log('startCoords', startCoords); const animate = (timestamp) => { const elapsed = timestamp - start; const progress = Math.min(elapsed / duration, 1); // 進(jìn)度百分比,范圍從0到1 // 線性插值計算當(dāng)前位置 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) { // 根據(jù)朝向創(chuàng)建點的樣式 return new Style({ image: new Icon({ src: require('@/assets/board.png'), scale: 0.1, rotation: -angle + (180 * Math.PI / 180), // 設(shè)置點的朝向 anchor: [0.5, 0.7], // 設(shè)置錨點位置 }), }); } }, }; </script> <style scoped> .map { width: 100%; height: 800px; } </style>
服務(wù)器代碼如下:
1、數(shù)據(jù)庫相關(guān):
// database.js const { Client } = require('pg'); const axios = require('axios'); const fs = require('fs').promises; const moment = require('moment-timezone'); // 配置數(shù)據(jù)庫連接 const client = new Client({ user: 'postgres', host: 'loaclhost', database: 'postgres', password: 'root', port: 4321, // 默認(rèn)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('數(shù)據(jù)插入成功'); } catch (err) { console.error('插入數(shù)據(jù)時發(fā)生錯誤:', 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 -- 隨機(jī)生成1到100之間的點數(shù) 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接口相關(guān):
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' }); } }); // 創(chuàng)建一個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 參數(shù)為數(shù)組 if (!timestamp) { return res.status(400).json({ error: 'Timestamp is required' }); } try { const points = await getPointsByTimestamp(timestamp, bbox); // 將 bbox 參數(shù)傳遞給函數(shù) 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 參數(shù)為數(shù)組 if (!timestamp) { return res.status(400).json({ error: 'Timestamp is required' }); } try { const lineGeometries = await getLineGeometries(timestamp, bbox); // 將 bbox 參數(shù)傳遞給函數(shù) res.json(lineGeometries); } catch (err) { console.error('Error fetching line geometries:', err.stack); res.status(500).json({ error: 'Internal Server Error' }); } }); // 啟動服務(wù)器 app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });
小結(jié)
當(dāng)顯示全球范圍性能會有明顯卡頓,可能需要改進(jìn)算法。
到此這篇關(guān)于vue+openlayers+nodejs+postgis實現(xiàn)軌跡運動的文章就介紹到這了,更多相關(guān)vue軌跡運動內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- 在Vue?3中使用OpenLayers加載GPX數(shù)據(jù)并顯示圖形效果
- Vue使用openlayers加載天地圖
- Vue+OpenLayers?創(chuàng)建地圖并顯示鼠標(biāo)所在經(jīng)緯度(完整代碼)
- vue?openlayers實現(xiàn)臺風(fēng)軌跡示例詳解
- vue利用openlayers實現(xiàn)動態(tài)軌跡
- Vue結(jié)合openlayers按照經(jīng)緯度坐標(biāo)實現(xiàn)錨地標(biāo)記及繪制多邊形區(qū)域
- Vue openLayers實現(xiàn)圖層數(shù)據(jù)切換與加載流程詳解
- Vue利用openlayers實現(xiàn)點擊彈窗的方法詳解
- Vue使用openlayers實現(xiàn)繪制圓形和多邊形
- 在Vue 3中使用OpenLayers讀取WKB數(shù)據(jù)并顯示圖形效果
相關(guān)文章
vue3響應(yīng)式Proxy與Reflect的理解及基本使用實例詳解
這篇文章主要為大家介紹了vue3響應(yīng)式Proxy與Reflect的理解及基本使用實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08vue element-ui實現(xiàn)input輸入框金額數(shù)字添加千分位
這篇文章主要介紹了vue element-ui實現(xiàn)input輸入框金額數(shù)字添加千分位,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12el-table實現(xiàn)嵌套表格的展示功能(完整代碼)
el-table中在嵌套一個el-table,這樣數(shù)據(jù)格式就沒問題了,主要就是樣式,將共同的列放到一列中,通過渲染自定義表頭render-header,將表頭按照合適的寬度渲染出來,本文給大家分享el-table實現(xiàn)嵌套表格的展示功能,感興趣的朋友一起看看吧2024-02-02element ui里dialog關(guān)閉后清除驗證條件方法
下面小編就為大家分享一篇element ui里dialog關(guān)閉后清除驗證條件方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02