dawarich/app/javascript/maps/privacy_zones.js
Evgenii Burmakin b1393ee674
0.36.0 (#1952)
* Implement OmniAuth GitHub authentication

* Fix omniauth GitHub scope to include user email access

* Remove margin-bottom

* Implement Google OAuth2 authentication

* Implement OIDC authentication for Dawarich using omniauth_openid_connect gem.

* Add patreon account linking and patron checking service

* Update docker-compose.yml to use boolean values instead of strings

* Add support for KML files

* Add tests

* Update changelog

* Remove patreon OAuth integration

* Move omniauthable to a concern

* Update an icon in integrations

* Update changelog

* Update app version

* Fix family location sharing toggle

* Move family location sharing to its own controller

* Update changelog

* Implement basic tagging functionality for places, allowing users to categorize and label places with custom tags.

* Add places management API and tags feature

* Add some changes related to places management feature

* Fix some tests

* Fix sometests

* Add places layer

* Update places layer to use Leaflet.Control.Layers.Tree for hierarchical layer control

* Rework tag form

* Add hashtag

* Add privacy zones to tags

* Add notes to places and manage place tags

* Update changelog

* Update e2e tests

* Extract tag serializer to its own file

* Fix some tests

* Fix tags request specs

* Fix some tests

* Fix rest of the tests

* Revert some changes

* Add missing specs

* Revert changes in place export/import code

* Fix some specs

* Fix PlaceFinder to only consider global places when finding existing places

* Fix few more specs

* Fix visits creator spec

* Fix last tests

* Update place creating modal

* Add home location based on "Home" tagged place

* Save enabled tag layers

* Some fixes

* Fix bug where enabling place tag layers would trigger saving enabled layers, overwriting with incomplete data

* Update migration to use disable_ddl_transaction! and add up/down methods

* Fix tag layers restoration and filtering logic

* Update OIDC auto-registration and email/password registration settings

* Fix potential xss
2025-11-24 19:45:09 +01:00

173 lines
4.9 KiB
JavaScript

// Privacy Zones Manager
// Handles filtering of map data (points, tracks) based on privacy zones defined by tags
import L from 'leaflet';
import { haversineDistance } from './helpers';
export class PrivacyZoneManager {
constructor(map, apiKey) {
this.map = map;
this.apiKey = apiKey;
this.zones = [];
this.visualLayers = L.layerGroup();
this.showCircles = false;
}
async loadPrivacyZones() {
try {
const response = await fetch('/api/v1/tags/privacy_zones', {
headers: { 'Authorization': `Bearer ${this.apiKey}` }
});
if (!response.ok) {
console.warn('Failed to load privacy zones:', response.status);
return;
}
this.zones = await response.json();
console.log(`[PrivacyZones] Loaded ${this.zones.length} privacy zones`);
} catch (error) {
console.error('Error loading privacy zones:', error);
this.zones = [];
}
}
isPointInPrivacyZone(lat, lng) {
if (!this.zones || this.zones.length === 0) return false;
return this.zones.some(zone =>
zone.places.some(place => {
const distanceKm = haversineDistance(lat, lng, place.latitude, place.longitude);
const distanceMeters = distanceKm * 1000;
return distanceMeters <= zone.radius_meters;
})
);
}
filterPoints(points) {
if (!this.zones || this.zones.length === 0) return points;
// Filter points and ensure polylines break at privacy zone boundaries
// We need to manipulate timestamps to force polyline breaks
const filteredPoints = [];
let lastWasPrivate = false;
let privacyZoneEncountered = false;
for (let i = 0; i < points.length; i++) {
const point = points[i];
const lat = point[0];
const lng = point[1];
const isPrivate = this.isPointInPrivacyZone(lat, lng);
if (!isPrivate) {
// Point is not in privacy zone, include it
const newPoint = [...point]; // Clone the point array
// If we just exited a privacy zone, force a polyline break by adding
// a large time gap that exceeds minutes_between_routes threshold
if (privacyZoneEncountered && filteredPoints.length > 0) {
// Add 2 hours (120 minutes) to timestamp to force a break
// This is larger than default minutes_between_routes (30 min)
const lastPoint = filteredPoints[filteredPoints.length - 1];
if (newPoint[4]) { // If timestamp exists (index 4)
newPoint[4] = lastPoint[4] + (120 * 60); // Add 120 minutes in seconds
}
privacyZoneEncountered = false;
}
filteredPoints.push(newPoint);
lastWasPrivate = false;
} else {
// Point is in privacy zone - skip it
if (!lastWasPrivate) {
privacyZoneEncountered = true;
}
lastWasPrivate = true;
}
}
return filteredPoints;
}
filterTracks(tracks) {
if (!this.zones || this.zones.length === 0) return tracks;
return tracks.map(track => {
const filteredPoints = track.points.filter(point => {
const lat = point[0];
const lng = point[1];
return !this.isPointInPrivacyZone(lat, lng);
});
return {
...track,
points: filteredPoints
};
}).filter(track => track.points.length > 0);
}
showPrivacyCircles() {
this.visualLayers.clearLayers();
if (!this.zones || this.zones.length === 0) return;
this.zones.forEach(zone => {
zone.places.forEach(place => {
const circle = L.circle([place.latitude, place.longitude], {
radius: zone.radius_meters,
color: zone.tag_color || '#ff4444',
fillColor: zone.tag_color || '#ff4444',
fillOpacity: 0.1,
dashArray: '10, 10',
weight: 2,
interactive: false,
className: 'privacy-zone-circle'
});
// Add popup with zone info
circle.bindPopup(`
<div class="privacy-zone-popup">
<strong>${zone.tag_icon || '🔒'} ${zone.tag_name}</strong><br>
<small>${place.name}</small><br>
<small>Privacy radius: ${zone.radius_meters}m</small>
</div>
`);
circle.addTo(this.visualLayers);
});
});
this.visualLayers.addTo(this.map);
this.showCircles = true;
}
hidePrivacyCircles() {
if (this.map.hasLayer(this.visualLayers)) {
this.map.removeLayer(this.visualLayers);
}
this.showCircles = false;
}
togglePrivacyCircles(show = null) {
const shouldShow = show !== null ? show : !this.showCircles;
if (shouldShow) {
this.showPrivacyCircles();
} else {
this.hidePrivacyCircles();
}
}
hasPrivacyZones() {
return this.zones && this.zones.length > 0;
}
getZoneCount() {
return this.zones ? this.zones.length : 0;
}
getTotalPlacesCount() {
if (!this.zones) return 0;
return this.zones.reduce((sum, zone) => sum + zone.places.length, 0);
}
}