|
@@ -0,0 +1,269 @@
|
|
|
+import * as format from "ol/format";
|
|
|
+import * as source from "ol/source";
|
|
|
+import * as layer from "ol/layer";
|
|
|
+import * as style from "ol/style";
|
|
|
+import * as extent from "ol/extent";
|
|
|
+import * as geom from "ol/geom";
|
|
|
+import * as ol from "ol";
|
|
|
+// @ts-ignore
|
|
|
+import DirectionImg from '../images/fangxiang.png'
|
|
|
+import {v4} from "uuid";
|
|
|
+class ShipTrack {
|
|
|
+ private uuid = v4()
|
|
|
+ private infos: any = {}
|
|
|
+ private map: any = null
|
|
|
+ private historyPoints: any = []
|
|
|
+ private realPoints: any = []
|
|
|
+ private color = ''
|
|
|
+ private trackLayer: any = null
|
|
|
+ private pointLayer: any = null
|
|
|
+ private directionLayer: any = null
|
|
|
+ private historyPointCoordinates: any = []
|
|
|
+ private realPointCoordinates: any = []
|
|
|
+ private pointMap: any = new Map()
|
|
|
+ private keyMapper: any = {}
|
|
|
+ private functions = {
|
|
|
+ zoom: () => {}
|
|
|
+ }
|
|
|
+ private zIndex
|
|
|
+ constructor({map, keys = {
|
|
|
+ lon: 'targetLongitude',
|
|
|
+ lat: 'targetLatitude',
|
|
|
+ }, color = '', infos = {}}: any, zIndex = 400) {
|
|
|
+ this.map = map
|
|
|
+ this.color = color
|
|
|
+ this.infos = infos
|
|
|
+ this.keyMapper = keys
|
|
|
+ this.zIndex = zIndex
|
|
|
+ }
|
|
|
+ init(historyPoints: any = [], realPoints: any = []) {
|
|
|
+ if (!this.color) {
|
|
|
+ this.color = randomColor(1)
|
|
|
+ }
|
|
|
+ historyPoints.forEach((v, i) => {
|
|
|
+ this.pointMap.set(`${v[this.keyMapper.lon]}_${v[this.keyMapper.lat]}`, v)
|
|
|
+ this.historyPointCoordinates.push([v[this.keyMapper.lon], v[this.keyMapper.lat]])
|
|
|
+ this.historyPoints.push(v)
|
|
|
+ });
|
|
|
+ realPoints.forEach((v, i) => {
|
|
|
+ this.pointMap.set(`${v[this.keyMapper.lon]}_${v[this.keyMapper.lat]}`, v)
|
|
|
+ this.realPointCoordinates.push([v[this.keyMapper.lon], v[this.keyMapper.lat]])
|
|
|
+ this.realPoints.push(v)
|
|
|
+ });
|
|
|
+ const lineF: any = new ol.Feature({
|
|
|
+ geometry: new geom.LineString([...this.historyPointCoordinates, ...this.realPointCoordinates]),
|
|
|
+ })
|
|
|
+ this.trackLayer = new layer.VectorImage({
|
|
|
+ source: new source.Vector({
|
|
|
+ features: [lineF],
|
|
|
+ wrapX: false,
|
|
|
+ }),
|
|
|
+ zIndex: this.zIndex,
|
|
|
+ });
|
|
|
+ this.trackLayer.set('layerName', this.uuid + '_track')
|
|
|
+ this.map.addLayer(this.trackLayer);
|
|
|
+ this.refreshStyle()
|
|
|
+ this.functions.zoom = debounce(e => {
|
|
|
+ this.refreshStyle()
|
|
|
+ }, 300)
|
|
|
+ this.map.getView().on('change:resolution', this.functions.zoom)
|
|
|
+ return this
|
|
|
+ }
|
|
|
+ focus() {
|
|
|
+ getShapeView(this.map, this.trackLayer.getSource().getFeatures()[0].getGeometry().getCoordinates())
|
|
|
+ }
|
|
|
+ show() {
|
|
|
+ this.trackLayer.setVisible(true)
|
|
|
+ this.pointLayer.setVisible(true)
|
|
|
+ this.directionLayer.setVisible(true)
|
|
|
+ }
|
|
|
+ hide() {
|
|
|
+ this.trackLayer.setVisible(false)
|
|
|
+ this.pointLayer.setVisible(false)
|
|
|
+ this.directionLayer.setVisible(false)
|
|
|
+ }
|
|
|
+ refreshStyle() {
|
|
|
+ this.map.getLayers().getArray().filter(v => [this.uuid + '_direction', this.uuid + '_point'].includes(v.get('layerName'))).forEach(v => {
|
|
|
+ this.map.removeLayer(v)
|
|
|
+ })
|
|
|
+ const resolution = this.map.getView().getResolution()
|
|
|
+ let radio = (30 * resolution);
|
|
|
+ if (this.map.getView().getZoom() == this.map.getView().getMaxZoom()) {
|
|
|
+ radio = 0
|
|
|
+ }
|
|
|
+ const simplifyGeom: any = new geom.LineString([...this.historyPointCoordinates, ...this.realPointCoordinates]).simplify(radio)
|
|
|
+ const simplifyCoor = simplifyGeom.getCoordinates()
|
|
|
+ const trackFeat = this.trackLayer.getSource().getFeatures()[0]
|
|
|
+ trackFeat.setGeometry(simplifyGeom)
|
|
|
+ trackFeat.setStyle(new style.Style({
|
|
|
+ stroke: new style.Stroke({
|
|
|
+ color: this.color,
|
|
|
+ width: 2,
|
|
|
+ }),
|
|
|
+ }))
|
|
|
+ const directionFeatures: any = []
|
|
|
+ const pointFeatures: any = []
|
|
|
+ simplifyCoor.forEach((v, i) => {
|
|
|
+ if (i > 0) {
|
|
|
+ const last = simplifyCoor[i - 1]
|
|
|
+ const dx = v[0] - last[0];
|
|
|
+ const dy = v[1] - last[1];
|
|
|
+ const rotation = Math.atan2(dy, dx) * -1;
|
|
|
+ const directFeat = new format.WKT().readFeature(`POINT(${(v[0] + last[0]) / 2} ${(v[1] + last[1]) / 2})`)
|
|
|
+ directFeat.set('rotation_', rotation)
|
|
|
+ directionFeatures.push(directFeat)
|
|
|
+ }
|
|
|
+ const d = this.pointMap.get(`${v[0]}_${v[1]}`)
|
|
|
+ const feat: any = new format.WKT().readFeature(`POINT(${v[0]} ${v[1]})`)
|
|
|
+ feat.set('data_', d)
|
|
|
+ feat.set('type_', 'track-point')
|
|
|
+ pointFeatures.push(feat)
|
|
|
+ });
|
|
|
+ this.pointLayer = new layer.WebGLPoints({
|
|
|
+ zIndex: this.zIndex + 2,
|
|
|
+ source: new source.Vector({
|
|
|
+ features: pointFeatures
|
|
|
+ }) as any,
|
|
|
+ style: {
|
|
|
+ 'circle-radius': ['match', ['get', 'hover_'], 1, 6, 4],
|
|
|
+ 'circle-fill-color': this.color,
|
|
|
+ 'circle-stroke-color': '#fff',
|
|
|
+ 'circle-stroke-width': 1,
|
|
|
+ },
|
|
|
+ })
|
|
|
+ this.pointLayer.set('layerName', this.uuid + '_point')
|
|
|
+ this.map.addLayer(this.pointLayer)
|
|
|
+ this.directionLayer = new layer.WebGLPoints({
|
|
|
+ zIndex: this.zIndex + 1,
|
|
|
+ source: new source.Vector({
|
|
|
+ features: directionFeatures
|
|
|
+ }) as any,
|
|
|
+ style: {
|
|
|
+ 'icon-src': DirectionImg,
|
|
|
+ 'icon-color': this.color,
|
|
|
+ 'icon-rotation': ['get', 'rotation_'],
|
|
|
+ 'icon-width': [
|
|
|
+ 'interpolate',
|
|
|
+ ['exponential', 2],
|
|
|
+ ['zoom'],
|
|
|
+ 0, 26,
|
|
|
+ 20, 16
|
|
|
+ ],
|
|
|
+ 'icon-height': [
|
|
|
+ 'interpolate',
|
|
|
+ ['exponential', 2],
|
|
|
+ ['zoom'],
|
|
|
+ 0, 26,
|
|
|
+ 20, 22
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ })
|
|
|
+ this.directionLayer.set('layerName', this.uuid + '_direction')
|
|
|
+ this.map.addLayer(this.directionLayer)
|
|
|
+ }
|
|
|
+ add(newPoints) {
|
|
|
+ newPoints.forEach((v, i) => {
|
|
|
+ this.pointMap.set(`${v[this.keyMapper.lon]}_${v[this.keyMapper.lat]}`, v)
|
|
|
+ this.historyPointCoordinates.push([v[this.keyMapper.lon], v[this.keyMapper.lat]])
|
|
|
+ this.historyPoints.push(v)
|
|
|
+ })
|
|
|
+ this.refreshStyle()
|
|
|
+ }
|
|
|
+ update(newPoints) {
|
|
|
+ newPoints.forEach((v, i) => {
|
|
|
+ this.pointMap.set(`${v[this.keyMapper.lon]}_${v[this.keyMapper.lat]}`, v)
|
|
|
+ this.realPointCoordinates.push([v[this.keyMapper.lon], v[this.keyMapper.lat]])
|
|
|
+ this.realPoints.push(v)
|
|
|
+ })
|
|
|
+ this.refreshStyle()
|
|
|
+ }
|
|
|
+ setColor(val) {
|
|
|
+ this.color = val
|
|
|
+ this.refreshStyle()
|
|
|
+ }
|
|
|
+ destroy() {
|
|
|
+ this.map.getView().un('change:resolution', this.functions.zoom)
|
|
|
+ this.map.getLayers().getArray().filter(v => [this.uuid + '_direction', this.uuid + '_point', this.uuid + '_track'].includes(v.get('layerName'))).forEach(v => {
|
|
|
+ this.map.removeLayer(v)
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+const randomColor = (opacity) => `rgba(${randomNum(0, 255)}, ${randomNum(0, 255)}, ${randomNum(0, 255)}, ${opacity ? opacity : randomNum(0.5, 1, 1)})`
|
|
|
+const randomNum = (min = 0, max = 0, decimal= 0) => {
|
|
|
+ // 获取数值的小数部分
|
|
|
+ const getDecimalNum = (data: number) => {
|
|
|
+ return Number(data.toString().split('.')[1]);
|
|
|
+ }
|
|
|
+ let min_z = Math.trunc(min); // 最小值的整数部分
|
|
|
+ let max_z = Math.trunc(max); // 最大值的整数部分
|
|
|
+ // 判断是否存在小数部分,不存在的话为0
|
|
|
+ let min_x = isNaN(getDecimalNum(min)) ? 0 : getDecimalNum(min); // 最小值的小数部分
|
|
|
+ let max_x = isNaN(getDecimalNum(max)) ? 0 : getDecimalNum(max); // 最大值的小数部分
|
|
|
+
|
|
|
+ // 区分有小数和没小数的情况
|
|
|
+ if (min_x > 0 || max_x > 0 || decimal > 0) {
|
|
|
+ // 整数部分随机数
|
|
|
+ let z = parseInt(String(Math.random() * (max_z - min_z + 1) + min_z), 10);
|
|
|
+ // 小数部分随机数
|
|
|
+ let x = 0;
|
|
|
+ // 小数部分随机数最大位数
|
|
|
+ let max_decimal = min_x.toString().length > max_x.toString().length ? min_x.toString().length : max_x.toString().length;
|
|
|
+ max_decimal = decimal > max_decimal ? decimal : max_decimal;
|
|
|
+ // 判断随机出的整数部分,是否等于最小值或者最大值
|
|
|
+ if(z == min_z || z == max_z){
|
|
|
+ if(z == min_z){
|
|
|
+ // 整数部分随机数等于最小值,那么应该从最小值的小数部分开始,到小数位数的最大值随机就可以
|
|
|
+ x = parseInt(String(Math.random() * (Math.pow(10, max_decimal) - min_x) + min_x), 10);
|
|
|
+ }else{
|
|
|
+ // 整数部分随机数等于最大值,那么应该从0开始,到最大值小数部分
|
|
|
+ x = parseInt(String(Math.random() * (max_x + 1)), 10);
|
|
|
+ }
|
|
|
+ }else{
|
|
|
+ // 整数部分在最大最小值区间的,就从0到小数位数的最大值随机就可以
|
|
|
+ x = parseInt(String(Math.random() * (Math.pow(10, max_decimal))), 10);
|
|
|
+ }
|
|
|
+ return Number(`${z}.${x}`);
|
|
|
+ } else {
|
|
|
+ return parseInt(String(Math.random() * (max_z - min_z + 1) + min_z), 10);
|
|
|
+ }
|
|
|
+}
|
|
|
+const getShapeView = (map, position, L = 600, defaultZoom = 12) => {
|
|
|
+ const center = extent.getCenter(extent.boundingExtent(position))
|
|
|
+ let x = 0
|
|
|
+ let y = 0
|
|
|
+ position.forEach(v => {
|
|
|
+ if (Math.abs(v[0] - center[0]) > x) {
|
|
|
+ x = Math.abs(v[0] - center[0])
|
|
|
+ }
|
|
|
+ if (Math.abs(v[1] - center[1]) > y) {
|
|
|
+ y = Math.abs(v[1] - center[1])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ const resolution = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2)) / (L / document.body.clientWidth * document.body.clientHeight)
|
|
|
+ if (map) {
|
|
|
+ if (position.length > 1) {
|
|
|
+ map.getView().animate({
|
|
|
+ center, resolution
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ map.getView().animate({
|
|
|
+ center, zoom: defaultZoom
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ center, resolution
|
|
|
+ }
|
|
|
+}
|
|
|
+const debounce = function (cb: any, ms = 0) {
|
|
|
+ let timer: any = null
|
|
|
+ return function () {
|
|
|
+ if (timer) clearTimeout(timer)
|
|
|
+ timer = setTimeout(() => {
|
|
|
+ // @ts-ignore
|
|
|
+ cb.apply(this, arguments)
|
|
|
+ timer = null
|
|
|
+ }, ms)
|
|
|
+ }
|
|
|
+}
|
|
|
+export default ShipTrack
|