Files
whats-on-my-laptop/static/leaflet-editable-polyline.js
2023-04-01 18:44:31 +02:00

590 lines
23 KiB
JavaScript

L.Polyline.polylineEditor = L.Polyline.extend({
_prepareMapIfNeeded: function() {
var that = this;
that._changed = false;
if(this._map._editablePolylines != null) {
return
}
// Container for all editable polylines on this map:
this._map._editablePolylines = [];
this._map._editablePolylinesEnabled = true;
// Click anywhere on map to add a new point-polyline:
if(this._options.newPolylines) {
console.log('click na map');
that._map.on('dblclick', function(event) {
console.log('click, target=' + (event.target == that._map) + ' type=' + event.type);
if(that._map.isEditablePolylinesBusy())
return;
var latLng = event.latlng;
if(that._options.newPolylineConfirmMessage)
if(!confirm(that._options.newPolylineConfirmMessage))
return
var contexts = [{'originalPolylineNo': null, 'originalPointNo': null}];
L.Polyline.PolylineEditor([latLng], that._options, contexts).addTo(that._map);
that._showBoundMarkers();
that._changed = true;
});
}
/**
* Check if there is *any* busy editable polyline on this map.
*/
this._map.isEditablePolylinesBusy = function() {
var map = this;
for(var i = 0; i < map._editablePolylines.length; i++)
if(map._editablePolylines[i]._isBusy())
return true;
return false;
};
/**
* Enable/disable editing.
*/
this._map.setEditablePolylinesEnabled = function(enabled) {
var map = this;
map._editablePolylinesEnabled = enabled;
for(var i = 0; i < map._editablePolylines.length; i++) {
var polyline = map._editablePolylines[i];
if(enabled) {
polyline._showBoundMarkers();
} else {
polyline._hideAll();
}
}
};
/*
* Utility method added to this map to retreive editable
* polylines.
*/
this._map.getEditablePolylines = function() {
var map = this;
return map._editablePolylines;
}
this._map.fixAroundEditablePoint = function(marker) {
var map = this;
for(var i = 0; i < map._editablePolylines.length; i++) {
var polyline = map._editablePolylines[i];
polyline._reloadPolyline(marker);
}
}
},
/**
* Will add all needed methods to this polyline.
*/
_addMethods: function() {
var that = this;
this._init = function(options, contexts) {
this._prepareMapIfNeeded();
/**
* Since all point editing is done by marker events, markers
* will be the main holder of the polyline points locations.
* Every marker contains a reference to the newPointMarker
* *before* him (=> the first marker has newPointMarker=null).
*/
this._parseOptions(options);
this._markers = [];
var points = this.getLatLngs();
var length = points.length;
for(var i = 0; i < length; i++) {
var marker = this._addMarkers(i, points[i]);
if(! ('context' in marker)) {
marker.context = {}
if(that._contexts != null) {
marker.context = contexts[i];
}
}
if(marker.context && ! ('originalPointNo' in marker.context))
marker.context.originalPointNo = i;
if(marker.context && ! ('originalPolylineNo' in marker.context))
marker.context.originalPolylineNo = that._map._editablePolylines.length;
}
// Map move => show different editable markers:
var map = this._map;
this._map.on("zoomend", function(e) {
that._showBoundMarkers();
});
this._map.on("moveend", function(e) {
that._showBoundMarkers();
});
if(this._desiredPolylineNo && this._desiredPolylineNo != null) {
this._map._editablePolylines.splice(this._desiredPolylineNo, 0, this);
} else {
this._map._editablePolylines.push(this);
}
};
/**
* Check if is busy adding/moving new nodes. Note, there may be
* *other* editable polylines on the same map which *are* busy.
*/
this._isBusy = function() {
return that._busy;
};
this._setBusy = function(busy) {
that._busy = busy;
};
/**
* Get markers for this polyline.
*/
this.getPoints = function() {
return this._markers;
};
this.isChanged = function() {
return this._changed;
}
this._parseOptions = function(options) {
if(!options)
options = {};
// Do not show edit markers if more than maxMarkers would be shown:
if(!('maxMarkers' in options))
options.maxMarkers = 100;
if(!('newPolylines' in options))
options.newPolylines = false;
if(!('newPolylineConfirmMessage' in options))
options.newPolylineConfirmMessage = '';
if(!('addFirstLastPointEvent' in options))
options.addFirstLastPointEvent = 'click';
if(!('customPointListeners' in options))
options.customPointListeners = {};
if(!('customNewPointListeners' in options))
options.customNewPointListeners = {};
this._options = options;
// Icons:
if(!options.pointIcon)
this._options.pointIcon = L.icon({ iconUrl: 'editmarker.png', iconSize: [11, 11], iconAnchor: [6, 6] });
if(!options.newPointIcon)
this._options.newPointIcon = L.icon({ iconUrl: 'editmarker2.png', iconSize: [11, 11], iconAnchor: [6, 6] });
};
/**
* Show only markers in current map bounds *is* there are only a certain
* number of markers. This method is called on eventy that change map
* bounds.
*/
this._showBoundMarkers = function() {
if (!that._map) {
return;
}
this._setBusy(false);
if(!that._map._editablePolylinesEnabled) {
console.log('Do not show because editing is disabled');
return;
}
var bounds = that._map.getBounds();
var found = 0;
for(var polylineNo in that._map._editablePolylines) {
var polyline = that._map._editablePolylines[polylineNo];
for(var markerNo in polyline._markers) {
var marker = polyline._markers[markerNo];
if(bounds.contains(marker.getLatLng()))
found += 1;
}
}
for(var polylineNo in that._map._editablePolylines) {
var polyline = that._map._editablePolylines[polylineNo];
for(var markerNo in polyline._markers) {
var marker = polyline._markers[markerNo];
if(found < that._options.maxMarkers) {
that._setMarkerVisible(marker, bounds.contains(marker.getLatLng()));
that._setMarkerVisible(marker.newPointMarker, markerNo > 0 && bounds.contains(marker.getLatLng()));
} else {
that._setMarkerVisible(marker, false);
that._setMarkerVisible(marker.newPointMarker, false);
}
}
}
};
/**
* Used when adding/moving points in order to disable the user to mess
* with other markers (+ easier to decide where to put the point
* without too many markers).
*/
this._hideAll = function(except) {
this._setBusy(true);
for(var polylineNo in that._map._editablePolylines) {
console.log("hide " + polylineNo + " markers");
var polyline = that._map._editablePolylines[polylineNo];
for(var markerNo in polyline._markers) {
var marker = polyline._markers[markerNo];
if(except == null || except != marker)
polyline._setMarkerVisible(marker, false);
if(except == null || except != marker.newPointMarker)
polyline._setMarkerVisible(marker.newPointMarker, false);
}
}
}
/**
* Show/hide marker.
*/
this._setMarkerVisible = function(marker, show) {
if(!marker)
return;
var map = this._map;
if(show) {
if(!marker._visible) {
if(!marker._map) { // First show for this marker:
marker.addTo(map);
} else { // Marker was already shown and hidden:
map.addLayer(marker);
}
marker._map = map;
}
marker._visible = true;
} else {
if(marker._visible) {
map.removeLayer(marker);
}
marker._visible = false;
}
};
/**
* Reload polyline. If it is busy, then the bound markers will not be
* shown.
*/
this._reloadPolyline = function(fixAroundPointNo) {
that.setLatLngs(that._getMarkerLatLngs());
if(fixAroundPointNo != null)
that._fixAround(fixAroundPointNo);
that._showBoundMarkers();
that._changed = true;
}
/**
* Add two markers (a point marker and his newPointMarker) for a
* single point.
*
* Markers are not added on the map here, the marker.addTo(map) is called
* only later when needed first time because of performance issues.
*/
this._addMarkers = function(pointNo, latLng, fixNeighbourPositions) {
var that = this;
var points = this.getLatLngs();
var marker = L.marker(latLng, {draggable: true, icon: this._options.pointIcon});
marker.newPointMarker = null;
marker.on('dragstart', function(event) {
var pointNo = that._getPointNo(event.target);
var previousPoint = pointNo && pointNo > 0 ? that._markers[pointNo - 1].getLatLng() : null;
var nextPoint = pointNo < that._markers.length - 1 ? that._markers[pointNo + 1].getLatLng() : null;
that._setupDragLines(marker, previousPoint, nextPoint);
that._hideAll(marker);
});
marker.on('dragend', function(event) {
var marker = event.target;
var pointNo = that._getPointNo(event.target);
setTimeout(function() {
that._reloadPolyline(pointNo);
}, 25);
});
marker.on('contextmenu', function(event) {
var marker = event.target;
var pointNo = that._getPointNo(event.target);
that._map.removeLayer(marker);
that._map.removeLayer(newPointMarker);
that._markers.splice(pointNo, 1);
that._reloadPolyline(pointNo);
});
marker.on(that._options.addFirstLastPointEvent, function(event) {
console.log('click on marker');
var marker = event.target;
var pointNo = that._getPointNo(event.target);
console.log('pointNo=' + pointNo + ' that._markers.length=' + that._markers.length);
event.dont;
if(pointNo == 0 || pointNo == that._markers.length - 1) {
console.log('first or last');
that._prepareForNewPoint(marker, pointNo == 0 ? 0 : pointNo + 1);
} else {
console.log('not first or last');
}
});
var previousPoint = points[pointNo == 0 ? pointNo : pointNo - 1];
var newPointMarker = L.marker([(latLng.lat + previousPoint.lat) / 2.,
(latLng.lng + previousPoint.lng) / 2.],
{draggable: true, icon: this._options.newPointIcon});
marker.newPointMarker = newPointMarker;
newPointMarker.on('dragstart', function(event) {
var pointNo = that._getPointNo(event.target);
var previousPoint = that._markers[pointNo - 1].getLatLng();
var nextPoint = that._markers[pointNo].getLatLng();
that._setupDragLines(marker.newPointMarker, previousPoint, nextPoint);
that._hideAll(marker.newPointMarker);
});
newPointMarker.on('dragend', function(event) {
var marker = event.target;
var pointNo = that._getPointNo(event.target);
that._addMarkers(pointNo, marker.getLatLng(), true);
setTimeout(function() {
that._reloadPolyline();
}, 25);
});
newPointMarker.on('contextmenu', function(event) {
// 1. Remove this polyline from map
var marker = event.target;
var pointNo = that._getPointNo(marker);
var markers = that.getPoints();
that._hideAll();
var secondPartMarkers = that._markers.slice(pointNo, pointNo.length);
that._markers.splice(pointNo, that._markers.length - pointNo);
that._reloadPolyline();
var points = [];
var contexts = [];
for(var i = 0; i < secondPartMarkers.length; i++) {
var marker = secondPartMarkers[i];
points.push(marker.getLatLng());
contexts.push(marker.context);
}
console.log('points:' + points);
console.log('contexts:' + contexts);
// Need to know the current polyline order numbers, because
// the splitted one need to be inserted immediately after:
var originalPolylineNo = that._map._editablePolylines.indexOf(that);
L.Polyline.PolylineEditor(points, that._options, contexts, originalPolylineNo + 1)
.addTo(that._map);
that._showBoundMarkers();
});
this._markers.splice(pointNo, 0, marker);
// User-defined custom event listeners:
if(that._options.customPointListeners)
for(var eventName in that._options.customPointListeners)
marker.on(eventName, that._options.customPointListeners[eventName]);
if(that._options.customNewPointListeners)
for(var eventName in that._options.customNewPointListeners)
newPointMarker.on(eventName, that._options.customNewPointListeners[eventName]);
if(fixNeighbourPositions) {
this._fixAround(pointNo);
}
return marker;
};
/**
* Event handlers for first and last point.
*/
this._prepareForNewPoint = function(marker, pointNo) {
// This is slightly delayed to prevent the same propagated event
// to be catched here:
setTimeout(
function() {
that._hideAll();
that._setupDragLines(marker, marker.getLatLng());
that._map.once('click', function(event) {
if(that._markers.length == 1) {
pointNo += 1;
}
console.log('dodajemo na ' + pointNo + ' - ' + event.latlng);
that._addMarkers(pointNo, event.latlng, true);
that._reloadPolyline();
});
},
100
);
};
/**
* Fix nearby new point markers when the new point is created.
*/
this._fixAround = function(pointNoOrMarker) {
if((typeof pointNoOrMarker) == 'number')
var pointNo = pointNoOrMarker;
else
var pointNo = that._markers.indexOf(pointNoOrMarker);
if(pointNo < 0)
return;
var previousMarker = pointNo == 0 ? null : that._markers[pointNo - 1];
var marker = that._markers[pointNo];
var nextMarker = pointNo < that._markers.length - 1 ? that._markers[pointNo + 1] : null;
if(marker && previousMarker) {
marker.newPointMarker.setLatLng([(previousMarker.getLatLng().lat + marker.getLatLng().lat) / 2.,
(previousMarker.getLatLng().lng + marker.getLatLng().lng) / 2.]);
}
if(marker && nextMarker) {
nextMarker.newPointMarker.setLatLng([(marker.getLatLng().lat + nextMarker.getLatLng().lat) / 2.,
(marker.getLatLng().lng + nextMarker.getLatLng().lng) / 2.]);
}
};
/**
* Find the order number of the marker.
*/
this._getPointNo = function(marker) {
for(var i = 0; i < this._markers.length; i++) {
if(marker == this._markers[i] || marker == this._markers[i].newPointMarker) {
return i;
}
}
return -1;
};
/**
* Get polyline latLngs based on marker positions.
*/
this._getMarkerLatLngs = function() {
var result = [];
for(var i = 0; i < this._markers.length; i++)
result.push(this._markers[i].getLatLng());
return result;
};
this._setupDragLines = function(marker, point1, point2) {
var line1 = null;
var line2 = null;
if(point1) line1 = L.polyline([marker.getLatLng(), point1], {dasharray: "5,1", weight: 1})
.addTo(that._map);
if(point2) line2 = L.polyline([marker.getLatLng(), point2], {dasharray: "5,1", weight: 1})
.addTo(that._map);
var moveHandler = function(event) {
if(line1)
line1.setLatLngs([event.latlng, point1]);
if(line2)
line2.setLatLngs([event.latlng, point2]);
};
var stopHandler = function(event) {
if (that._map) {
that._map.off('mousemove', moveHandler);
marker.off('dragend', stopHandler);
if(line1) that._map.removeLayer(line1);
if(line2) that._map.removeLayer(line2);
console.log('STOPPED');
if(event.target != that._map) {
that._map.fire('click', event);
}
}
};
that._map.on('mousemove', moveHandler);
marker.on('dragend', stopHandler);
that._map.once('click', stopHandler);
marker.once('click', stopHandler);
if(line1) line1.once('click', stopHandler);
if(line2) line2.once('click', stopHandler);
}
}
});
L.Polyline.polylineEditor.addInitHook(function () {
this.on('add', function(event) {
this._map = event.target._map;
this._addMethods();
/**
* When addint a new point we must disable the user to mess with other
* markers. One way is to check everywhere if the user is busy. The
* other is to just remove other markers when the user is doing
* somethinng.
*
* TODO: Decide the right way to do this and then leave only _busy or
* _hideAll().
*/
this._busy = false;
this._initialized = false;
this._init(this._options, this._contexts);
this._initialized = true;
return this;
});
this.on('remove', function(event) {
var polyline = event.target;
var map = polyline._map;
var polylines = map.getEditablePolylines();
var index = polylines.indexOf(polyline);
if (index > -1) {
polylines[index]._markers.forEach(function(marker) {
map.removeLayer(marker);
if(marker.newPointMarker)
map.removeLayer(marker.newPointMarker);
});
polylines.splice(index, 1);
}
});
});
/**
* Construct a new editable polyline.
*
* latlngs ... a list of points (or two-element tuples with coordinates)
* options ... polyline options
* contexts ... custom contexts for every point in the polyline. Must have the
* same number of elements as latlngs and this data will be
* preserved when new points are added or polylines splitted.
* polylineNo ... insert this polyline in a specific order (used when splitting).
*
* More about contexts:
* This is an array of objects that will be kept as "context" for every
* point. Marker will keep this value as marker.context. New markers will
* have context set to null.
*
* Contexts must be the same size as the polyline size!
*
* By default, even without calling this method -- every marker will have
* context with one value: marker.context.originalPointNo with the
* original order number of this point. The order may change if some
* markers before this one are delted or new added.
*/
L.Polyline.PolylineEditor = function(latlngs, options, contexts, polylineNo) {
// Since the app code may not be able to explicitly call the
// initialization of all editable polylines (if the user created a new
// one by splitting an existing), with this method you can control the
// options for new polylines:
if(options.prepareOptions) {
options.prepareOptions(options);
}
var result = new L.Polyline.polylineEditor(latlngs, options);
result._options = options;
result._contexts = contexts;
result._desiredPolylineNo = polylineNo
return result;
};