mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
* 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
173 lines
4.9 KiB
JavaScript
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);
|
|
}
|
|
}
|