From 27694a3a7d342db7f45941f606142a7b369d9b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D1=88=D0=BA=D0=B8=D0=BD=20=D0=A1=D0=B5=D1=80?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9?= Date: Wed, 26 Nov 2025 11:12:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D1=8E=20=D0=B2=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BA=D1=83.=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=202=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D1=8B=D0=B5=20js=20=D0=B1=D0=B8=D0=B1=D0=BB=D0=B8=D0=BE?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mainapp/source_with_points_map.html | 368 +++++++++++++- dbapp/mainapp/views/map.py | 6 +- .../leaflet-playback/leaflet-playback.js | 1 + .../leaflet.polylineDecorator.js | 478 ++++++++++++++++++ 4 files changed, 841 insertions(+), 12 deletions(-) create mode 100644 dbapp/static/leaflet-playback/leaflet-playback.js create mode 100644 dbapp/static/leaflet-polylineDecorator/leaflet.polylineDecorator.js diff --git a/dbapp/mainapp/templates/mainapp/source_with_points_map.html b/dbapp/mainapp/templates/mainapp/source_with_points_map.html index 9f714b6..8c3b711 100644 --- a/dbapp/mainapp/templates/mainapp/source_with_points_map.html +++ b/dbapp/mainapp/templates/mainapp/source_with_points_map.html @@ -7,6 +7,7 @@ + {% endblock %} {% block content %}
+ {% endblock content %} {% block extra_js %} @@ -69,7 +171,8 @@ - + + {% endblock extra_js %} diff --git a/dbapp/mainapp/views/map.py b/dbapp/mainapp/views/map.py index f9d1362..d7b6b5f 100644 --- a/dbapp/mainapp/views/map.py +++ b/dbapp/mainapp/views/map.py @@ -232,7 +232,7 @@ class ShowSourceWithPointsMapView(LoginRequiredMixin, View): "parameter_obj", "geo_obj" ).all() - # Собираем все точки ГЛ в одну группу + # Собираем все точки ГЛ в одну группу с сортировкой по времени all_gl_points = [] for obj in gl_points: if ( @@ -250,9 +250,13 @@ class ShowSourceWithPointsMapView(LoginRequiredMixin, View): "point": (obj.geo_obj.coords.x, obj.geo_obj.coords.y), "name": obj.name, "frequency": f"{param.frequency} [{param.freq_range}] МГц", + "timestamp": obj.geo_obj.timestamp.isoformat() if obj.geo_obj.timestamp else None, } ) + # Сортируем точки по времени (от старой к новой) + all_gl_points.sort(key=lambda x: x["timestamp"] if x["timestamp"] else "") + # Добавляем все точки ГЛ одним цветом (красный) if all_gl_points: groups.append( diff --git a/dbapp/static/leaflet-playback/leaflet-playback.js b/dbapp/static/leaflet-playback/leaflet-playback.js new file mode 100644 index 0000000..a69c299 --- /dev/null +++ b/dbapp/static/leaflet-playback/leaflet-playback.js @@ -0,0 +1 @@ +!function(a){var b;if("function"==typeof define&&define.amd)define(["leaflet"],a);else if("object"==typeof module&&"object"==typeof module.exports)b=require("leaflet"),module.exports=a(b);else{if(void 0===window.L)throw"Leaflet must be loaded first";a(window.L)}}(function(a){return a.Playback=a.Playback||{},a.Playback.Util=a.Class.extend({statics:{DateStr:function(a){return new Date(a).toDateString()},TimeStr:function(a){var b=new Date(a),c=b.getHours(),d=b.getMinutes(),e=b.getSeconds(),f=a/1e3,g=(f-Math.floor(f)).toFixed(2).slice(1),h="AM";return c>11&&(c%=12,h="PM"),0===c&&(c=12),d<10&&(d="0"+d),e<10&&(e="0"+e),c+":"+d+":"+e+g+" "+h},ParseGPX:function(a){for(var b={type:"FeatureCollection",features:[]},c=$.parseXML(a),d=$(c).find("trk"),e=0,f=d.length;e"+this.popupContent+"
":""},move:function(b,c){a.DomUtil.TRANSITION&&(this._icon&&(this._icon.style[a.DomUtil.TRANSITION]="all "+c+"ms linear",this._popup&&this._popup._wrapper&&(this._popup._wrapper.style[a.DomUtil.TRANSITION]="all "+c+"ms linear")),this._shadow&&(this._shadow.style[a.DomUtil.TRANSITION]="all "+c+"ms linear")),this.setLatLng(b),this._popup&&this._popup.setContent(this.getPopupContent()+this._latlng.toString())},_old__setPos:a.Marker.prototype._setPos,_updateImg:function(b,c,d){c=a.point(d).divideBy(2)._subtract(a.point(c));var e="";e+=" translate("+-c.x+"px, "+-c.y+"px)",e+=" rotate("+this.options.iconAngle+"deg)",e+=" translate("+c.x+"px, "+c.y+"px)",b.style[a.DomUtil.TRANSFORM]+=e},setIconAngle:function(a){this.options.iconAngle=a,this._map&&this.update()},_setPos:function(b){if(this._icon&&(this._icon.style[a.DomUtil.TRANSFORM]=""),this._shadow&&(this._shadow.style[a.DomUtil.TRANSFORM]=""),this._old__setPos.apply(this,[b]),this.options.iconAngle){var c,d=this.options.icon.options.iconAnchor,e=this.options.icon.options.iconSize;this._icon&&(c=this._icon,this._updateImg(c,d,e)),this._shadow&&(e=this.options.icon.options.shadowSize,c=this._shadow,this._updateImg(c,d,e))}}}),a.Playback=a.Playback||{},a.Playback.Track=a.Class.extend({initialize:function(a,b){b=b||{};var c=b.tickLen||250;this._staleTime=b.staleTime||36e5,this._fadeMarkersWhenStale=b.fadeMarkersWhenStale||!1,this._geoJSON=a,this._tickLen=c,this._ticks=[],this._marker=null,this._orientations=[];var d=a.properties.time;this._orientIcon=b.orientIcons;var e,f,g,h=a.geometry.coordinates,i=h[0],j=h[1],k=d[0],l=k,m=d[1],n=l%c;if(1===d.length)return 0!==n&&(l+=c-n),this._ticks[l]=h[0],this._orientations[l]=0,this._startTime=l,void(this._endTime=l);for(0!==n?(f=c-n,g=f/(m-k),l+=f,this._ticks[l]=this._interpolatePoint(i,j,g),this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):(this._ticks[l]=i,this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]),this._startTime=l,l+=c;lb.maxInterpolationTime?(this._ticks[l]=i,j?(this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):this._orientations[l]=e):(this._ticks[l]=this._interpolatePoint(i,j,g),j?(this._orientations[l]=this._directionOfPoint(i,j),e=this._orientations[l]):this._orientations[l]=e),l+=c;this._endTime=l-c,this._lastTick=this._ticks[this._endTime]},_interpolatePoint:function(a,b,c){try{var d=[b[0]-a[0],b[1]-a[1]],e=[d[0]*c,d[1]*c];return[a[0]+e[0],a[1]+e[1]]}catch(f){console.log("err: cant interpolate a point"),console.log(["start",a]),console.log(["end",b]),console.log(["ratio",c])}},_directionOfPoint:function(a,b){return this._getBearing(a[1],a[0],b[1],b[0])},_getBearing:function(a,b,c,d){a=this._radians(a),b=this._radians(b),c=this._radians(c),d=this._radians(d);var e=d-b,f=Math.log(Math.tan(c/2+Math.PI/4)/Math.tan(a/2+Math.PI/4));return Math.abs(e)>Math.PI&&(e=e>0?-(2*Math.PI-e):2*Math.PI+e),(this._degrees(Math.atan2(e,f))+360)%360},_radians:function(a){return a*(Math.PI/180)},_degrees:function(a){return a*(180/Math.PI)},getFirstTick:function(){return this._ticks[this._startTime]},getLastTick:function(){return this._ticks[this._endTime]},getStartTime:function(){return this._startTime},getEndTime:function(){return this._endTime},getTickMultiPoint:function(){for(var a=this.getStartTime(),b=this.getEndTime(),c=[],d=[];a<=b;)d.push(a),c.push(this.tick(a)),a+=this._tickLen;return{type:"Feature",geometry:{type:"MultiPoint",coordinates:c},properties:{time:d}}},trackPresentAtTick:function(a){return a>=this._startTime},trackStaleAtTick:function(a){return this._endTime+this._staleTime<=a},tick:function(a){return a>this._endTime&&(a=this._endTime),athis._endTime&&(a=this._endTime),a0;){var a=this._tracks.pop(),b=a.getMarker();b&&this._map.removeLayer(b)}},setTracks:function(a){this.clearTracks(),this.addTracks(a)},addTracks:function(a){if(a)if(a instanceof Array)for(var b=0,c=a.length;b0){a=this._tracks[0].getStartTime();for(var b=1,c=this._tracks.length;b0){a=this._tracks[0].getEndTime();for(var b=1,c=this._tracks.length;ba&&(a=d)}}return a},getTracks:function(){return this._tracks}}),a.Playback=a.Playback||{},a.Playback.Clock=a.Class.extend({initialize:function(b,c,d){this._trackController=b,this._callbacksArry=[],c&&this.addCallback(c),a.setOptions(this,d),this._speed=this.options.speed,this._tickLen=this.options.tickLen,this._cursor=b.getStartTime(),this._transitionTime=this._tickLen/this._speed},_tick:function(a){if(a._cursor>a._trackController.getEndTime())return void clearInterval(a._intervalID);a._trackController.tock(a._cursor,a._transitionTime),a._callbacks(a._cursor),a._cursor+=a._tickLen},_callbacks:function(a){for(var b=this._callbacksArry,c=0,d=b.length;c 0 + }; +} + +var pointsEqual = function pointsEqual(a, b) { + return a.x === b.x && a.y === b.y; +}; + +function pointsToSegments(pts) { + return pts.reduce(function (segments, b, idx, points) { + // this test skips same adjacent points + if (idx > 0 && !pointsEqual(b, points[idx - 1])) { + var a = points[idx - 1]; + var distA = segments.length > 0 ? segments[segments.length - 1].distB : 0; + var distAB = pointDistance(a, b); + segments.push({ + a: a, + b: b, + distA: distA, + distB: distA + distAB, + heading: computeSegmentHeading(a, b) + }); + } + return segments; + }, []); +} + +function projectPatternOnPointPath(pts, pattern) { + // 1. split the path into segment infos + var segments = pointsToSegments(pts); + var nbSegments = segments.length; + if (nbSegments === 0) { + return []; + } + + var totalPathLength = segments[nbSegments - 1].distB; + + var offset = asRatioToPathLength(pattern.offset, totalPathLength); + var endOffset = asRatioToPathLength(pattern.endOffset, totalPathLength); + var repeat = asRatioToPathLength(pattern.repeat, totalPathLength); + + var repeatIntervalPixels = totalPathLength * repeat; + var startOffsetPixels = offset > 0 ? totalPathLength * offset : 0; + var endOffsetPixels = endOffset > 0 ? totalPathLength * endOffset : 0; + + // 2. generate the positions of the pattern as offsets from the path start + var positionOffsets = []; + var positionOffset = startOffsetPixels; + do { + positionOffsets.push(positionOffset); + positionOffset += repeatIntervalPixels; + } while (repeatIntervalPixels > 0 && positionOffset < totalPathLength - endOffsetPixels); + + // 3. projects offsets to segments + var segmentIndex = 0; + var segment = segments[0]; + return positionOffsets.map(function (positionOffset) { + // find the segment matching the offset, + // starting from the previous one as offsets are ordered + while (positionOffset > segment.distB && segmentIndex < nbSegments - 1) { + segmentIndex++; + segment = segments[segmentIndex]; + } + + var segmentRatio = (positionOffset - segment.distA) / (segment.distB - segment.distA); + return { + pt: interpolateBetweenPoints(segment.a, segment.b, segmentRatio), + heading: segment.heading + }; + }); +} + +/** +* Finds the point which lies on the segment defined by points A and B, +* at the given ratio of the distance from A to B, by linear interpolation. +*/ +function interpolateBetweenPoints(ptA, ptB, ratio) { + if (ptB.x !== ptA.x) { + return { + x: ptA.x + ratio * (ptB.x - ptA.x), + y: ptA.y + ratio * (ptB.y - ptA.y) + }; + } + // special case where points lie on the same vertical axis + return { + x: ptA.x, + y: ptA.y + (ptB.y - ptA.y) * ratio + }; +} + +(function() { + // save these original methods before they are overwritten + var proto_initIcon = L.Marker.prototype._initIcon; + var proto_setPos = L.Marker.prototype._setPos; + + var oldIE = (L.DomUtil.TRANSFORM === 'msTransform'); + + L.Marker.addInitHook(function () { + var iconOptions = this.options.icon && this.options.icon.options; + var iconAnchor = iconOptions && this.options.icon.options.iconAnchor; + if (iconAnchor) { + iconAnchor = (iconAnchor[0] + 'px ' + iconAnchor[1] + 'px'); + } + this.options.rotationOrigin = this.options.rotationOrigin || iconAnchor || 'center bottom' ; + this.options.rotationAngle = this.options.rotationAngle || 0; + + // Ensure marker keeps rotated during dragging + this.on('drag', function(e) { e.target._applyRotation(); }); + }); + + L.Marker.include({ + _initIcon: function() { + proto_initIcon.call(this); + }, + + _setPos: function (pos) { + proto_setPos.call(this, pos); + this._applyRotation(); + }, + + _applyRotation: function () { + if(this.options.rotationAngle) { + this._icon.style[L.DomUtil.TRANSFORM+'Origin'] = this.options.rotationOrigin; + + if(oldIE) { + // for IE 9, use the 2D rotation + this._icon.style[L.DomUtil.TRANSFORM] = 'rotate(' + this.options.rotationAngle + 'deg)'; + } else { + // for modern browsers, prefer the 3D accelerated version + this._icon.style[L.DomUtil.TRANSFORM] += ' rotateZ(' + this.options.rotationAngle + 'deg)'; + } + } + }, + + setRotationAngle: function(angle) { + this.options.rotationAngle = angle; + this.update(); + return this; + }, + + setRotationOrigin: function(origin) { + this.options.rotationOrigin = origin; + this.update(); + return this; + } + }); +})(); + +L$1.Symbol = L$1.Symbol || {}; + +/** +* A simple dash symbol, drawn as a Polyline. +* Can also be used for dots, if 'pixelSize' option is given the 0 value. +*/ +L$1.Symbol.Dash = L$1.Class.extend({ + options: { + pixelSize: 10, + pathOptions: {} + }, + + initialize: function initialize(options) { + L$1.Util.setOptions(this, options); + this.options.pathOptions.clickable = false; + }, + + buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) { + var opts = this.options; + var d2r = Math.PI / 180; + + // for a dot, nothing more to compute + if (opts.pixelSize <= 1) { + return L$1.polyline([dirPoint.latLng, dirPoint.latLng], opts.pathOptions); + } + + var midPoint = map.project(dirPoint.latLng); + var angle = -(dirPoint.heading - 90) * d2r; + var a = L$1.point(midPoint.x + opts.pixelSize * Math.cos(angle + Math.PI) / 2, midPoint.y + opts.pixelSize * Math.sin(angle) / 2); + // compute second point by central symmetry to avoid unecessary cos/sin + var b = midPoint.add(midPoint.subtract(a)); + return L$1.polyline([map.unproject(a), map.unproject(b)], opts.pathOptions); + } +}); + +L$1.Symbol.dash = function (options) { + return new L$1.Symbol.Dash(options); +}; + +L$1.Symbol.ArrowHead = L$1.Class.extend({ + options: { + polygon: true, + pixelSize: 10, + headAngle: 60, + pathOptions: { + stroke: false, + weight: 2 + } + }, + + initialize: function initialize(options) { + L$1.Util.setOptions(this, options); + this.options.pathOptions.clickable = false; + }, + + buildSymbol: function buildSymbol(dirPoint, latLngs, map, index, total) { + return this.options.polygon ? L$1.polygon(this._buildArrowPath(dirPoint, map), this.options.pathOptions) : L$1.polyline(this._buildArrowPath(dirPoint, map), this.options.pathOptions); + }, + + _buildArrowPath: function _buildArrowPath(dirPoint, map) { + var d2r = Math.PI / 180; + var tipPoint = map.project(dirPoint.latLng); + var direction = -(dirPoint.heading - 90) * d2r; + var radianArrowAngle = this.options.headAngle / 2 * d2r; + + var headAngle1 = direction + radianArrowAngle; + var headAngle2 = direction - radianArrowAngle; + var arrowHead1 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle1), tipPoint.y + this.options.pixelSize * Math.sin(headAngle1)); + var arrowHead2 = L$1.point(tipPoint.x - this.options.pixelSize * Math.cos(headAngle2), tipPoint.y + this.options.pixelSize * Math.sin(headAngle2)); + + return [map.unproject(arrowHead1), dirPoint.latLng, map.unproject(arrowHead2)]; + } +}); + +L$1.Symbol.arrowHead = function (options) { + return new L$1.Symbol.ArrowHead(options); +}; + +L$1.Symbol.Marker = L$1.Class.extend({ + options: { + markerOptions: {}, + rotate: false + }, + + initialize: function initialize(options) { + L$1.Util.setOptions(this, options); + this.options.markerOptions.clickable = false; + this.options.markerOptions.draggable = false; + }, + + buildSymbol: function buildSymbol(directionPoint, latLngs, map, index, total) { + if (this.options.rotate) { + this.options.markerOptions.rotationAngle = directionPoint.heading + (this.options.angleCorrection || 0); + } + return L$1.marker(directionPoint.latLng, this.options.markerOptions); + } +}); + +L$1.Symbol.marker = function (options) { + return new L$1.Symbol.Marker(options); +}; + +var isCoord = function isCoord(c) { + return c instanceof L$1.LatLng || Array.isArray(c) && c.length === 2 && typeof c[0] === 'number'; +}; + +var isCoordArray = function isCoordArray(ll) { + return Array.isArray(ll) && isCoord(ll[0]); +}; + +L$1.PolylineDecorator = L$1.FeatureGroup.extend({ + options: { + patterns: [] + }, + + initialize: function initialize(paths, options) { + L$1.FeatureGroup.prototype.initialize.call(this); + L$1.Util.setOptions(this, options); + this._map = null; + this._paths = this._initPaths(paths); + this._bounds = this._initBounds(); + this._patterns = this._initPatterns(this.options.patterns); + }, + + /** + * Deals with all the different cases. input can be one of these types: + * array of LatLng, array of 2-number arrays, Polyline, Polygon, + * array of one of the previous. + */ + _initPaths: function _initPaths(input, isPolygon) { + var _this = this; + + if (isCoordArray(input)) { + // Leaflet Polygons don't need the first point to be repeated, but we do + var coords = isPolygon ? input.concat([input[0]]) : input; + return [coords]; + } + if (input instanceof L$1.Polyline) { + // we need some recursivity to support multi-poly* + return this._initPaths(input.getLatLngs(), input instanceof L$1.Polygon); + } + if (Array.isArray(input)) { + // flatten everything, we just need coordinate lists to apply patterns + return input.reduce(function (flatArray, p) { + return flatArray.concat(_this._initPaths(p, isPolygon)); + }, []); + } + return []; + }, + + // parse pattern definitions and precompute some values + _initPatterns: function _initPatterns(patternDefs) { + return patternDefs.map(this._parsePatternDef); + }, + + /** + * Changes the patterns used by this decorator + * and redraws the new one. + */ + setPatterns: function setPatterns(patterns) { + this.options.patterns = patterns; + this._patterns = this._initPatterns(this.options.patterns); + this.redraw(); + }, + + /** + * Changes the patterns used by this decorator + * and redraws the new one. + */ + setPaths: function setPaths(paths) { + this._paths = this._initPaths(paths); + this._bounds = this._initBounds(); + this.redraw(); + }, + + /** + * Parse the pattern definition + */ + _parsePatternDef: function _parsePatternDef(patternDef, latLngs) { + return { + symbolFactory: patternDef.symbol, + // Parse offset and repeat values, managing the two cases: + // absolute (in pixels) or relative (in percentage of the polyline length) + offset: parseRelativeOrAbsoluteValue(patternDef.offset), + endOffset: parseRelativeOrAbsoluteValue(patternDef.endOffset), + repeat: parseRelativeOrAbsoluteValue(patternDef.repeat) + }; + }, + + onAdd: function onAdd(map) { + this._map = map; + this._draw(); + this._map.on('moveend', this.redraw, this); + }, + + onRemove: function onRemove(map) { + this._map.off('moveend', this.redraw, this); + this._map = null; + L$1.FeatureGroup.prototype.onRemove.call(this, map); + }, + + /** + * As real pattern bounds depends on map zoom and bounds, + * we just compute the total bounds of all paths decorated by this instance. + */ + _initBounds: function _initBounds() { + var allPathCoords = this._paths.reduce(function (acc, path) { + return acc.concat(path); + }, []); + return L$1.latLngBounds(allPathCoords); + }, + + getBounds: function getBounds() { + return this._bounds; + }, + + /** + * Returns an array of ILayers object + */ + _buildSymbols: function _buildSymbols(latLngs, symbolFactory, directionPoints) { + var _this2 = this; + + return directionPoints.map(function (directionPoint, i) { + return symbolFactory.buildSymbol(directionPoint, latLngs, _this2._map, i, directionPoints.length); + }); + }, + + /** + * Compute pairs of LatLng and heading angle, + * that define positions and directions of the symbols on the path + */ + _getDirectionPoints: function _getDirectionPoints(latLngs, pattern) { + var _this3 = this; + + if (latLngs.length < 2) { + return []; + } + var pathAsPoints = latLngs.map(function (latLng) { + return _this3._map.project(latLng); + }); + return projectPatternOnPointPath(pathAsPoints, pattern).map(function (point) { + return { + latLng: _this3._map.unproject(L$1.point(point.pt)), + heading: point.heading + }; + }); + }, + + redraw: function redraw() { + if (!this._map) { + return; + } + this.clearLayers(); + this._draw(); + }, + + /** + * Returns all symbols for a given pattern as an array of FeatureGroup + */ + _getPatternLayers: function _getPatternLayers(pattern) { + var _this4 = this; + + var mapBounds = this._map.getBounds().pad(0.1); + return this._paths.map(function (path) { + var directionPoints = _this4._getDirectionPoints(path, pattern) + // filter out invisible points + .filter(function (point) { + return mapBounds.contains(point.latLng); + }); + return L$1.featureGroup(_this4._buildSymbols(path, pattern.symbolFactory, directionPoints)); + }); + }, + + /** + * Draw all patterns + */ + _draw: function _draw() { + var _this5 = this; + + this._patterns.map(function (pattern) { + return _this5._getPatternLayers(pattern); + }).forEach(function (layers) { + _this5.addLayer(L$1.featureGroup(layers)); + }); + } +}); +/* + * Allows compact syntax to be used + */ +L$1.polylineDecorator = function (paths, options) { + return new L$1.PolylineDecorator(paths, options); +}; + +}))); \ No newline at end of file