/**
* @mixin
* @memberof module:geoflo
* @name Pinning
* @description This module provides the pinning functionality for the Geoflo application. It allows users to pin features to the map by creating a buffer around the feature and snapping to nearby features.
* @param {Object} mode - The mode object containing the type of mode.
* @returns {Object} Returns the Pinning object.
*/
const Pinning = function (mode) {
const geoflo = this.geoflo;
this.type = mode.type;
this.coldFeatures = [];
this.pinableFeatures = [];
this.pinnedFeatures = [];
/**
* @function
* @memberof module:geoflo.Pinning
* @name activate
* @description Activates the feature by setting the enabled flag to true and enabling pinning in the options.
* @params {void} None
* @returns {void}
*/
/**
* @function activate
* @memberof _MEMBER_OF_
* @description - This function activates the pinning functionality by setting the enabled flag to true and enabling pinning in the options.
*
* @returns {void} No return value.
*
*/
this.activate = function () {
this.coldFeatures = [];
this.pinableFeatures = [];
this.pinnedFeatures = [];
this.enabled = true;
geoflo.options['pinning'].enable = true;
geoflo.map.getSource(geoflo.statics.constants.sources.PIN).setData(turf.featureCollection([]));
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name deactivate
* @description This function deactivates pinning by setting enabled to false, disabling pinning in options, clearing buffer, pinableFeatures, and pinningFeatures, and resetting coldFeatures.
*/
this.deactivate = function () {
this.enabled = false;
geoflo.options['pinning'].enable = false;
this.resetFeatures();
delete this.buffer;
this.coldFeatures = [];
this.pinableFeatures = [];
this.pinnedFeatures = [];
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name getFeatures
* @description Retrieves the features from the pinnedFeatures array in the context object.
* @returns {Array} An array of features extracted from the pinnedFeatures array.
*/
this.getFeatures = function () {
return this.pinnedFeatures.map(function (feature) { return geoflo.Utilities.cloneDeep(feature) });
}
this.saveFeatures = function () {
const features = this.pinnedFeatures.map(function (feature) { return geoflo.Utilities.cloneDeep(feature) });
geoflo.addFeatures(features, true);
geoflo.map.getSource(geoflo.statics.constants.sources.PIN).setData(turf.featureCollection([]));
this.coldFeatures = [];
this.pinableFeatures = [];
this.pinnedFeatures = [];
return features;
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name setBuffer
* @description This function creates a buffer around the provided coordinates based on the pinning buffer option.
* @param {Array<number>} coords - The coordinates [longitude, latitude] to create the buffer around.
* @returns {Object|boolean} Returns the buffer object containing the feature, radius, and coordinates if successful, otherwise false.
*/
this.setBuffer = function (coords) {
delete this.buffer;
if (!this.enabled) return false;
if (!coords || !geoflo.options.pinning.buffer) return false;
var buffer = turf.buffer(turf.point(coords), geoflo.options.pinning.buffer);
var radius = turf.polygon(buffer.geometry.coordinates);
this.buffer = {
feature: buffer,
radius: radius,
coords: coords
}
return this.buffer;
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name setFeatures
* @description Sets the pinable features based on the provided coordinates and fires an event.
* @param {Object} coords - The coordinates to determine nearby features.
* @returns {Array} - An array of pinable features.
*/
this.setFeatures = function (coords) {
if (!this.enabled || !coords) return false;
this.pinableFeatures = this.getNearByFeatures(coords);
geoflo.fire('pinning.add', { features: this.pinableFeatures, buffer: this.buffer });
return this.pinableFeatures;
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name resetFeatures
* @description Resets the updated features by adding them to the canvas context.
* @returns {boolean} Returns false if there are no updated features to reset.
*/
this.resetFeatures = function () {
if (!this.coldFeatures.length) return false;
geoflo.map.getSource(geoflo.statics.constants.sources.PIN).setData(turf.featureCollection([]));
geoflo.addFeatures(this.coldFeatures, true);
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name updateFeatures
* @description This function updates the features if the pinning functionality is enabled. It updates the pinable features, pinned features, and triggers events accordingly.
* @returns {boolean} Returns false if the pinning functionality is not enabled, otherwise returns the updated pinning features.
*/
this.updateFeatures = function (point) {
if (!this.enabled || !point || !this.pinableFeatures.length) return false;
updateFeatures.call(this, this.pinableFeatures, point.geometry.coordinates);
geoflo.hideFeatures(this.coldFeatures.map(function (feature) { return feature.id }));
geoflo.map.getSource(geoflo.statics.constants.sources.PIN).setData(turf.featureCollection(this.pinnedFeatures));
geoflo.pinnedFeatures = geoflo.Utilities.cloneDeep(this.pinableFeatures);
geoflo.fire('pinning.update', { feature: geoflo.hotFeature, point: point, pinned: this.pinnedFeatures });
return this.pinnedFeatures;
}
/**
* @function
* @memberof module:geoflo.Pinning
* @name getNearByFeatures
* @description This function calculates the radius based on the map zoom level and retrieves nearby features within that radius.
* @param {Array<number>} coords - The coordinates [longitude, latitude] to find nearby features.
* @returns {Array<Object>} An array of nearby features with their IDs, types, indices, and feature objects.
*/
this.getNearByFeatures = function (coords) {
if (!this.enabled || !coords) return false;
const hotFeatureId = geoflo.hotFeature ? geoflo.hotFeature.id : null;
const calculatedRadius = geoflo.options.snapping.distance * Math.pow(2, Math.max(1, 19 - geoflo.map.getZoom()));
const radiusInKm = calculatedRadius / 100000;
const buffer = this.setBuffer(coords);
if (!buffer) return false;
const features = geoflo.getRenderedDrawnFeatures({ lng: coords[0], lat: coords[1] }, radiusInKm);
const nearby = [];
// Precompute point for faster comparisons
const point = turf.point(coords);
features.forEach((feature) => {
if (hotFeatureId === feature.id) return; // Skip if it's the active feature
// Check all coordinates in one loop instead of using `turf.coordEach`
const coordsArray = feature.geometry.coordinates.flat(Infinity); // Flattens to avoid nested looping
for (let index = 0; index < coordsArray.length; index += 2) {
const coord = [coordsArray[index], coordsArray[index + 1]];
// Fast checks for nearby conditions
if (
(buffer.radius && turf.booleanWithin(turf.point(coord), buffer.radius)) ||
(buffer.coords && geoflo.Utilities.isPointEqual(coord, buffer.coords))
) {
nearby.push({
id: feature.id || feature.properties.id,
type: feature.properties.type,
index: Math.floor(index / 2), // Convert flattened index back to original index
feature: feature
});
break; // Stop checking once a valid nearby point is found
}
}
});
return nearby;
};
if (geoflo.options['pinning'].enable) this.activate();
function updateFeatures(features, coords) {
if (!features || !features.length || !coords) return false;
const coldFeatureIds = new Set(this.coldFeatures.map(f => f.id));
const updatedFeatureIds = new Set(this.pinnedFeatures.map(f => f.id));
features.forEach((feature) => {
const id = feature.id;
const feat = feature.feature;
const index = feature.index;
if (!coldFeatureIds.has(id)) {
this.coldFeatures.push(geoflo.Utilities.cloneDeep(feat));
coldFeatureIds.add(id);
}
const updated = updatedFeatureIds.has(id) ? this.pinnedFeatures.find(f => f.id === id) : feat;
if (updated.geometry.type === 'Point') {
updated.geometry.coordinates = coords;
} else if (updated.geometry.type === 'Polygon') {
updated.geometry.coordinates[0][index] = coords;
} else if (updated.geometry.type === 'LineString') {
updated.geometry.coordinates[index] = coords;
}
if (!updatedFeatureIds.has(id)) {
this.pinnedFeatures.push(geoflo.Utilities.cloneDeep(updated));
updatedFeatureIds.add(id);
}
});
}
};
export default Pinning;