console
function generateSpeedVectorBack(latitude, speed, course) {
var northSpeed = speed * Math.cos((course) * Math.PI / 180) / 60 / 3600;
var eastSpeed = speed * Math.sin((course) * Math.PI / 180) / 60 / 3600 * Math.abs(Math.sin(latitude * Math.PI / 180));
return [northSpeed, eastSpeed, 0]
}
function generateSpeedVector(speed, course) {
var northSpeed = speed / 3600 / 60 * Math.cos((course - 90) * Math.PI / 180);
var eastSpeed = speed / 3600 / 60 * Math.sin((course - 90) * Math.PI / 180);
return [northSpeed, eastSpeed, 0]
}
var MathFunc = {
add: function (a, b) {
return [
a[0] + b[0],
a[1] + b[1],
a[2] + b[2]
];
},
sub: function (a, b) {
return [
a[0] - b[0],
a[1] - b[1],
a[2] - b[2]
];
},
mulScalar: function (a, s) {
return [
a[0] * s,
a[1] * s,
a[2] * s
];
},
dot: function (a, b) {
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
},
lengthSquared: function (a) {
return a[0] * a[0] + a[1] * a[1] + a[2] * a[2];
}
};
function withMathFunc(mf) {
return {
getPositionByVeloAndTime: function (position, velocity, dt) {
return mf.add(position, mf.mulScalar(velocity, dt));
},
quadEquation: function (p, q) {
var wurzel = Math.sqrt((p * p / 4) - q);
var vorwurzel = (-p / 2);
var result = [];
if (wurzel > 0) {
result = [vorwurzel + wurzel, vorwurzel - wurzel];
}
else if (wurzel === 0) {
result = [vorwurzel];
}
return result;
},
calcCPATime: function (position1, velocity1, position2, velocity2) {
var posDiff = mf.sub(position2, position1);
var veloDiff = mf.sub(velocity2, velocity1);
var zaehler = -mf.dot(posDiff, veloDiff);
var nenner = mf.lengthSquared(veloDiff);
return nenner === 0.0 ? undefined : zaehler / nenner;
},
calcCPAPositionTarget1: function (position1, velocity1, position2, velocity2) {
var tcpa = this.calcCPATime(position1, velocity1, position2, velocity2);
if (tcpa === undefined)
return undefined;
return this.getPositionByVeloAndTime(position1, velocity1, tcpa);
},
calcCPAPositionTarget2: function (position1, velocity1, position2, velocity2) {
var tcpa = this.calcCPATime(position1, velocity1, position2, velocity2);
if (tcpa === undefined)
return undefined;
return this.getPositionByVeloAndTime(position2, velocity2, tcpa);
},
calcInterceptTime: function (myPos, myVelo, targetPos, targetVelo) {
var relTargetPos = mf.sub(targetPos, myPos);
var a = mf.lengthSquared(targetVelo) - myVelo * myVelo;
var b = 2.0 * mf.dot(targetVelo, relTargetPos);
var c = mf.lengthSquared(relTargetPos);
if (a === 0) {
if (b !== 0) {
var time = -c / b;
if (time > 0.0)
return time;
}
}
else {
var p = b / a;
var q = c / a;
var times = this.quadEquation(p, q);
if (times.length === 0)
return [];
if (times.length === 2) {
var icptTime = Math.min(times[0], times[1]);
if (icptTime < 0.0) {
icptTime = Math.max(times[0], times[1]);
}
return icptTime;
}
else if (times.length === 1) {
if (times[0] >= 0.0) {
return times[0];
}
}
}
return undefined;
},
calcInterceptPosition: function (myPos, myVelo, targetPos, targetVelo) {
var ticpt = this.calcInterceptTime(myPos, myVelo, targetPos, targetVelo);
if (ticpt === undefined)
return undefined;
return this.getPositionByVeloAndTime(targetPos, targetVelo, ticpt);
},
calcArrivalTime: function (myPos, myVelo, targetPos, targetVelo) {
var distance = Math.sqrt(mf.lengthSquared(mf.sub(targetPos, myPos)));
var approachSpeed = this.calcApproachSpeed(myPos, myVelo, targetPos, targetVelo);
if (approachSpeed > 0.0) {
return distance / approachSpeed;
}
else {
return undefined;
}
},
calcApproachSpeed: function (myPos, myVelo, targetPos, targetVelo) {
var posDiff = mf.sub(targetPos, myPos);
var veloDiff = mf.sub(targetVelo, myVelo);
var approachSpeed = mf.dot(posDiff, veloDiff);
var posDiffLength = Math.sqrt(mf.lengthSquared(posDiff));
if (posDiffLength <= 0.0)
return 0.0;
return -approachSpeed / posDiffLength;
}
};
};
var motionpredict = withMathFunc(MathFunc);
let lat = 39.38114242464665;
let lng = 121.26219749450685;
let mapUtils = null;
const map = L.map('map').setView([lat, lng], 11);
L.tileLayer(`http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=faed44eb8716fbc8bc9978d8e44ab7b4`, {
attribution: '© <a href="https://stadiamaps.com/" target="_blank">Stadia Maps</a> © <a href="https://stamen.com/" target="_blank">Stamen Design</a> © <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> © <a href="https://www.openstreetmap.org/about" target="_blank">OpenStreetMap</a> contributors',
minZoom: 2,
maxZoom: 18
}).addTo(map);
map.attributionControl.setPosition('bottomleft');
map.zoomControl.setPosition('bottomright');
map.on('click', function (e) {
console.log("Latitude: " + e.latlng.lat + ", Longitude: " + e.latlng.lng);
});
function disToPixeldistance(distance) {
var l2 = L.GeometryUtil.destination(map.getCenter(), 0, distance);
var p1 = map.latLngToContainerPoint(map.getCenter())
var p2 = map.latLngToContainerPoint(l2)
return p1.distanceTo(p2)
}
function pixelDistance(p1, p2) {
var l1 = mapUtils.layerPointToLatLng(p1);
var l2 = mapUtils.layerPointToLatLng(p2);
return l1.distanceTo(l2);
}
function calculateApolloniusCircle(x1, y1, x2, y2, k) {
const dx = x2 - x1;
const dy = y2 - y1;
const d = Math.sqrt(dx * dx + dy * dy);
const cx = x1 + dx * k / (k + 1);
const cy = y1 + dy * k / (k + 1);
const r = Math.abs(d * k / (k * k - 1));
const rx = dx * k / (k * k - 1);
const ry = dy * k / (k * k - 1);
const centerX = cx + rx
const centerY = cy + ry
return { centerX, centerY, radius: r, cx, cy };
}
function toRadians(degrees) {
return degrees * Math.PI / 180;
}
function speedToVector(speed, direction) {
if (typeof direction === 'number' && direction > 0 && direction < 360) {
direction = toRadians(direction);
}
let x = speed * Math.cos(direction);
let y = speed * Math.sin(direction);
return { x: x, y: y };
}
function shortestDistance(x1, y1, vx1, vy1, x2, y2, vx2, vy2) {
let a = (vx1 - vx2) ** 2 + (vy1 - vy2) ** 2;
let b = 2 * ((x1 - x2) * (vx1 - vx2) + (y1 - y2) * (vy1 - vy2));
let c = (x1 - x2) ** 2 + (y1 - y2) ** 2;
let t = -b / (2 * a);
let px1 = x1 + t * vx1;
let py1 = y1 + t * vy1;
let px2 = x2 + t * vx2;
let py2 = y2 + t * vy2;
let shortestDist = Math.sqrt((px1 - px2) ** 2 + (py1 - py2) ** 2);
return shortestDist;
}
var ships = [];
const duration = 60 * 60;
const timeScale = 10;
let frame = null;
let showAshiCirle = false;
let showTraceLine = true;
let firstDraw = true;
let prevZoom;
let start;
const animate = (timestamp) => {
const zoom = mapUtils.getMap().getZoom();
const container = mapUtils.getContainer();
const renderer = mapUtils.getRenderer();
const project = mapUtils.latLngToLayerPoint;
const scale = mapUtils.getScale();
if (start === null) start = timestamp;
const progress = (timestamp - start) / 1000 * timeScale;
let lambda = progress / duration;
if (lambda > 1) lambda = 1;
ships.forEach((ship, index) => {
var sSpeedV = generateSpeedVector(ship.info.speed, ship.info.angle);
let newX = ship.fromPixel.x + sSpeedV[0] * progress;
let newY = ship.fromPixel.y + sSpeedV[1] * progress;
let rad = Math.atan((ship.fromPixel.x - newY) / (ship.fromPixel.y - newX)) + 45 * (Math.PI / 180);
if (rad < 0) {
rad = Math.PI * 2 + rad;
}
ship.triangle.rotation = rad;
ship.triangle.x = newX;
ship.triangle.y = newY;
})
ships.forEach((aShip, aIndex) => {
ships.forEach((bShip, bIndex) => {
if (aIndex !== bIndex) {
aShip.lines[bIndex].clear();
aShip.lines[bIndex].lineStyle(1 / scale, 0x669920, 1);
aShip.lines[bIndex].moveTo(aShip.triangle.x, aShip.triangle.y);
aShip.lines[bIndex].lineTo(bShip.triangle.x, bShip.triangle.y);
let dis = pixelDistance(aShip.triangle, bShip.triangle);
dis = `${dis.toFixed(0)}m-${(progress).toFixed(0)}s`
aShip.lineTexts[bIndex].text = dis;
aShip.lineTexts[bIndex].x = (aShip.triangle.x + bShip.triangle.x) / 2;
aShip.lineTexts[bIndex].y = (aShip.triangle.y + bShip.triangle.y) / 2;
aShip.lineTexts[bIndex].zIndex = 1;
}
})
})
renderer.render(container);
if (progress < duration) {
frame = requestAnimationFrame(animate);
}
};
var shipInfos = [{
lat: 39.38114242464665,
lng: 121.26219749450685,
angle: -90,
top: 40,
left: 10,
right: 10,
bottom: 10,
minDis: 100,
maxDis: 500,
speed: 20000 * 1.852
}, {
lat: 39.38692224789837,
lng: 121.24580383300783,
angle: -120,
top: 160,
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 500,
speed: 10000 * 1.852
}, {
lat: 39.295516858108876,
lng: 121.22039794921876,
angle: -20,
top: 160,
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 200,
speed: 11000 * 1.852
}, {
lat: 39.27106874641232,
lng: 121.12495422363283,
angle: 40,
top: 160,
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 200,
speed: 12000 * 1.852
}, {
lat: 39.39428523176663,
lng: 121.01303100585939,
angle: 110,
top: 160,
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 200,
speed: 12300 * 1.852
}, {
lat: 39.45581202472926,
lng: 121.06521606445314,
angle: 150,
top: 160,
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 200,
speed: 12400 * 1.852
}, {
lat: 39.33376633431887,
lng: 121.04496002197267,
angle: 69, top: 160,
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 200,
speed: 12402 * 1.852
}];
const pixiOverlay = (() => {
shipInfos.forEach(info => {
var latlng = L.latLng(info.lat, info.lng);
var top = L.GeometryUtil.destination(latlng, 0 + info.angle, info.top)
var tmpDot1 = L.GeometryUtil.destination(top, 90 + info.angle, info.right)
tmpDot1 = L.GeometryUtil.destination(tmpDot1, 180 + info.angle, info.right / 2)
var tmpDot2 = L.GeometryUtil.destination(top, -90 + info.angle, info.left)
tmpDot2 = L.GeometryUtil.destination(tmpDot2, 180 + info.angle, info.left / 2)
var bottom = L.GeometryUtil.destination(latlng, 180 + info.angle, info.bottom)
var tmp2 = L.GeometryUtil.destination(bottom, 90 + info.angle, info.right)
var tmp4 = L.GeometryUtil.destination(bottom, 270 + info.angle, info.left)
var distance = info.speed / 3600 * duration
var to = L.GeometryUtil.destination(latlng, info.angle, distance);
ships.push({
info: info,
from: latlng,
fromPixel: null,
to: to,
toPixel: null,
centerRadius: 0,
minDisRadius: 0,
maxDisRadius: 0,
polygonLatLngs: [
[top.lat, top.lng],
[tmpDot1.lat, tmpDot1.lng],
[tmp2.lat, tmp2.lng],
[bottom.lat, bottom.lng],
[tmp4.lat, tmp4.lng],
[tmpDot2.lat, tmpDot2.lng],
[top.lat, top.lng]
],
projectedPolygon: []
})
});
const pixiContainer = new PIXI.Container();
ships.forEach(ship => {
ship.triangle = new PIXI.Graphics();
ship.ftLine = new PIXI.Graphics();
ship.lines = [];
ship.lineTexts = [];
ship.aShiCirles = [];
ship.disLines = [];
ship.disLineTexts = [];
for (var i = 0; i < ships.length; i++) {
var disLine = new PIXI.Graphics();
var line = new PIXI.Graphics()
var lineText = new PIXI.Text('', {
fontFamily: 'Arial',
fontSize: 10,
fill: 0xff1010,
align: 'center',
});
var disLineText = new PIXI.Text('', {
fontFamily: 'Arial',
fontSize: 10,
fill: 0x11f122,
align: 'center',
});
var aShiCirle = new PIXI.Graphics();
ship.aShiCirles.push(aShiCirle);
ship.lineTexts.push(lineText);
ship.disLineTexts.push(disLineText);
ship.disLines.push(disLine);
ship.lines.push(line);
pixiContainer.addChild(disLine, disLineText, line, lineText, aShiCirle);
}
ship.disPointOne = new PIXI.Graphics();
ship.disPointTwo = new PIXI.Graphics();
pixiContainer.addChild(ship.triangle, ship.ftLine, ship.disPointOne, ship.disPointTwo);
})
const doubleBuffering = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
return L.pixiOverlay((utils) => {
mapUtils = utils;
const zoom = utils.getMap().getZoom();
const container = utils.getContainer();
const renderer = utils.getRenderer();
const project = utils.latLngToLayerPoint;
const unproject = utils.layerPointToLatLng;
const scale = utils.getScale();
function draw(prevShip, ship, bIndex) {
ship.lines[bIndex].clear();
ship.lines[bIndex].lineStyle(1 / scale, 0x669920, 1);
ship.lines[bIndex].moveTo(prevShip.triangle.x, prevShip.triangle.y);
ship.lines[bIndex].lineTo(ship.triangle.x, ship.triangle.y);
ship.lines[bIndex].endFill();
ship.lineTexts[bIndex].scale.set(1 / scale);
ship.disLineTexts[bIndex].scale.set(1 / scale);
ship.aShiCirles[bIndex].clear();
if (showAshiCirle) {
const apolloniusCircle = calculateApolloniusCircle(prevShip.fromPixel.x, prevShip.fromPixel.y, ship.fromPixel.x, ship.fromPixel.y, prevShip.info.speed / ship.info.speed);
ship.aShiCirles[bIndex].lineStyle(1 / scale, 0xff0000, 1);
ship.aShiCirles[bIndex].x = apolloniusCircle.centerX;
ship.aShiCirles[bIndex].y = apolloniusCircle.centerY;
ship.aShiCirles[bIndex].drawCircle(0, 0, apolloniusCircle.radius);
}
var pSpeedV = generateSpeedVector(prevShip.info.speed, prevShip.info.angle);
var sSpeedV = generateSpeedVector(ship.info.speed, ship.info.angle);
var position1 = [prevShip.fromPixel.x, prevShip.fromPixel.y, 0];
var velocity1 = pSpeedV;
var position2 = [ship.fromPixel.x, ship.fromPixel.y, 0];
var velocity2 = sSpeedV;
var tcpa = motionpredict.calcCPATime(position1, velocity1, position2, velocity2);
var p1 = motionpredict.getPositionByVeloAndTime(position1, velocity1, tcpa)
var p2 = motionpredict.getPositionByVeloAndTime(position2, velocity2, tcpa)
var p3 = unproject({ x: p1[0], y: p1[1] }).distanceTo(unproject({ x: p2[0], y: p2[1] }));
ship.disLines[bIndex].clear();
ship.disLines[bIndex].lineStyle(1 / scale, 0x11f122, 1);
ship.disLines[bIndex].moveTo(p1[0], p1[1]);
ship.disLines[bIndex].lineTo(p2[0], p2[1]);
ship.disLines[bIndex].endFill();
ship.disLineTexts[bIndex].text = `${p3.toFixed(0)}m-${tcpa.toFixed(2)}s`;
ship.disLineTexts[bIndex].x = (p1[0] + p2[0]) / 2;
ship.disLineTexts[bIndex].y = (p1[1] + p2[1]) / 2;
ship.disLineTexts[bIndex].zIndex = 1;
}
if (prevZoom !== zoom) {
const getRenderer = utils.getRenderer;
const boundary = new PIXI.EventBoundary(container);
ships.forEach((ship, index) => {
ship.projectedPolygon = ship.polygonLatLngs.map((coords) => project(coords));
ship.fromPixel = project(ship.from);
ship.toPixel = project(ship.to);
ship.minDisRadius = disToPixeldistance(ship.info.top * 2 + ship.info.bottom * 2 + ship.info.speed / 3600 * 10);
ship.maxDisRadius = disToPixeldistance(ship.info.maxDis);
ship.centerRadius = disToPixeldistance(4);
ship.triangle.clear();
ship.triangle.lineStyle(1 / scale, 0x3388ff, 1);
ship.triangle.x = ship.fromPixel.x;
ship.triangle.y = ship.fromPixel.y;
ship.projectedPolygon.forEach((coords, index) => {
if (index == 0) ship.triangle.moveTo(coords.x - ship.fromPixel.x, coords.y - ship.fromPixel.y);
else ship.triangle.lineTo(coords.x - ship.fromPixel.x, coords.y - ship.fromPixel.y);
});
ship.triangle.drawCircle(0, 0, ship.minDisRadius / scale)
ship.ftLine.clear();
if (showTraceLine) {
ship.ftLine.lineStyle(1 / scale, 0xfff122, 1);
ship.ftLine.moveTo(ship.fromPixel.x, ship.fromPixel.y);
ship.ftLine.lineTo(ship.toPixel.x, ship.toPixel.y);
ship.ftLine.endFill();
}
})
ships.forEach((aShip, aIndex) => {
ships.forEach((bShip, bIndex) => {
if (aIndex !== bIndex) {
draw(aShip, bShip, aIndex);
}
})
})
}
firstDraw = false;
prevZoom = zoom;
renderer.render(container);
}, pixiContainer, {
doubleBuffering: doubleBuffering,
autoPreventDefault: false
});
});
const $startBtn = document.querySelector("#startBtn")
$startBtn.onclick = (e) => {
start = null;
frame = requestAnimationFrame(animate);
}
const $stopBtn = document.querySelector("#stopBtn")
$stopBtn.onclick = (e) => {
if (frame) {
cancelAnimationFrame(frame);
frame = null;
}
}
const $showBtn = document.querySelector("#showBtn")
$showBtn.onclick = (e) => {
showAshiCirle = !showAshiCirle;
mapUtils.getMap().setZoom(prevZoom + 1);
}
const $showTraceBtn = document.querySelector("#showTraceBtn")
$showTraceBtn.onclick = (e) => {
showTraceLine = !showTraceLine;
mapUtils.getMap().setZoom(prevZoom + 1);
}
const $addBtn = document.querySelector("#addBtn")
const $delBtn = document.querySelector("#delBtn")
function getRamdom(min, max) {
return parseFloat((Math.random() * (max - min) + min).toFixed(4));;
}
let overlay = pixiOverlay();
overlay.addTo(map);
$addBtn.onclick = (e) => {
map.removeLayer(overlay);
if (frame) {
cancelAnimationFrame(frame);
frame = null;
}
ships = [];
prevZoom = 0;
for (var i = 0; i < 1; i++) {
let test = {
lat: getRamdom(39.18708, 39.38708),
lng: getRamdom(121.187, 121.387),
angle: getRamdom(0, 360),
top: getRamdom(30, 300),
left: 20,
right: 20,
bottom: 40,
minDis: 100,
maxDis: 200,
speed: getRamdom(22404, 24404) * 1.852
}
shipInfos.push(test);
}
overlay = pixiOverlay();
overlay.addTo(map);
}
$delBtn.onclick = (e) => {
if (frame) {
cancelAnimationFrame(frame);
frame = null;
}
map.removeLayer(overlay);
ships = [];
prevZoom = 0;
shipInfos.pop();
overlay = pixiOverlay();
overlay.addTo(map);
}
<div id="map" style="height: 100%; width: 100%;position:absolute">
</div>
<button id="startBtn" style="position:absolute;z-index:1000;">开始动画</button>
<button id="stopBtn" style="position:absolute;z-index:1000;left:100px">停止动画</button>
<button id="showBtn" style="position:absolute;z-index:1000;left:200px">阿氏圆</button>
<button id="showTraceBtn" style="position:absolute;z-index:1000;left:300px">轨迹线</button>
<button id="addBtn" style="position:absolute;z-index:1000;left:400px">增加船</button>
<button id="delBtn" style="position:absolute;z-index:1000;left:500px">减少船</button>