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