Geographical Location Tracking Requirements
Geographical Location Tracking Requirements
Overview
This document specifies the technical requirements for implementing geographical location tracking (LAT/LON) in the Plings object creation system. The feature integrates GPS coordinates capture and location-based discovery into the existing CreateObjectModal workflow to enable “near me” functionality for outdoor, mobile, and uncontained objects.
Integration with Existing System
Current Implementation Status
✅ Foundation Ready:
- CreateObjectModal with extensible architecture
- Non-blocking user experience patterns
- State management for complex modal workflows
- Mobile-responsive design infrastructure
- Existing ScanEvent location tracking in Neo4j
- Comprehensive spatial relationships system (IN, ON, LEFT_OF, etc.)
🔄 Extensions Required:
- GPS/location API integration
- Location permission management
- Geographical coordinates state management
- Location-based search and discovery
- Map integration components
- Privacy and security controls
Technical Architecture
Frontend Component Extensions
1. Enhanced Location State Management
Extend existing modal state with location tracking:
interface LocationEnhancedCreateState extends CreateObjectState {
location: {
// Location Detection State
detectionStatus: 'idle' | 'requesting' | 'detecting' | 'success' | 'denied' | 'error';
permissionStatus: 'prompt' | 'granted' | 'denied';
// Location Data
coordinates: GeographicalCoordinates | null;
address: string | null;
source: LocationSource;
// User Preferences
preferences: LocationPreferences;
// UI State
manualEntryMode: boolean;
mapPickerVisible: boolean;
addressSearchVisible: boolean;
};
}
interface GeographicalCoordinates {
latitude: number; // -90 to +90
longitude: number; // -180 to +180
accuracy: number; // meters
altitude?: number; // meters above sea level
heading?: number; // degrees from north
speed?: number; // meters per second
timestamp: number; // Unix timestamp
}
interface LocationPreferences {
autoDetectEnabled: boolean;
askEachTime: boolean;
saveForFutureUse: boolean;
shareWithOrganization: boolean;
precisionLevel: 'precise' | 'approximate' | 'city' | 'region';
inheritFromParent: boolean;
}
enum LocationSource {
GPS = 'gps',
NETWORK = 'network',
MANUAL = 'manual',
MAP_PICKER = 'map',
ADDRESS_SEARCH = 'address',
INHERITED = 'inherited',
CACHED = 'cached'
}
2. Location Components Architecture
Create new React components for location handling:
src/components/objects/creation/location/
├── LocationProvider.tsx # Context provider for location state
├── LocationSection.tsx # Main location section in modal
├── LocationDetectionButton.tsx # GPS detection trigger
├── LocationStatusIndicator.tsx # Current location display
├── LocationPermissionDialog.tsx # Permission request UI
├── LocationDeniedFallback.tsx # Fallback when location denied
├── ManualLocationEntry.tsx # Coordinate/address input
├── LocationMapPicker.tsx # Interactive map selection
├── LocationAddressSearch.tsx # Address-based location search
├── LocationPrivacySettings.tsx # Privacy controls
├── LocationAccuracyIndicator.tsx # GPS accuracy display
├── LocationInheritanceSelector.tsx # Inherit from parent object
└── hooks/
├── useLocationDetection.tsx # GPS detection logic
├── useLocationPermissions.tsx # Permission management
├── useLocationPreferences.tsx # User preference management
├── useLocationValidation.tsx # Coordinate validation
├── useLocationSearch.tsx # Address search integration
└── useLocationInheritance.tsx # Parent location inheritance
3. Enhanced CreateObjectModal Integration
Location section integrated into modal workflow:
const CreateObjectModal = () => {
const { location, updateLocation } = useLocationState();
const { hasLocationPermission } = useLocationPermissions();
return (
<Modal>
{/* Existing image upload section */}
<ImageUploadSection onImageUpload={handleImageUpload} />
{/* Location Section - NEW */}
<LocationSection>
<LocationProvider>
<LocationDetectionButton />
<LocationStatusIndicator />
{location.detectionStatus === 'denied' && (
<LocationDeniedFallback />
)}
{location.manualEntryMode && (
<ManualLocationEntry />
)}
{location.mapPickerVisible && (
<LocationMapPicker />
)}
<LocationPrivacySettings />
</LocationProvider>
</LocationSection>
{/* Enhanced existing sections with location context */}
<ObjectDetailsForm
locationContext={location.coordinates}
addressContext={location.address}
/>
{/* Existing organization and other sections */}
</Modal>
);
};
Backend Extensions
1. Database Schema Extensions
Neo4j ObjectInstance node enhancements:
// Enhanced ObjectInstance with geographical properties
CREATE (obj:ObjectInstance {
id: $objectId,
name: $name,
description: $description,
statuses: $statuses,
owner: $owner,
// Geographical Properties - NEW
latitude: $latitude,
longitude: $longitude,
location_accuracy: $accuracy,
location_altitude: $altitude,
location_heading: $heading,
location_speed: $speed,
location_timestamp: $timestamp,
location_source: $source,
location_address: $address,
location_precision_level: $precisionLevel,
location_inherited_from: $inheritedFromId
})
// Spatial index for proximity queries
CREATE INDEX location_spatial_idx FOR (n:ObjectInstance)
ON (n.latitude, n.longitude);
Supabase/PostgreSQL schema extensions:
-- Architecture Note: Following Plings database rules, location data for queries
-- lives in Neo4j while PostgreSQL maintains audit trail and history
-- Create location_events table for location history and audit
CREATE TABLE location_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
object_instance_id UUID REFERENCES object_instances(id) NOT NULL,
event_type VARCHAR(20) NOT NULL CHECK (event_type IN ('created', 'updated', 'manual_override', 'inherited', 'permission_denied')),
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
accuracy INTEGER, -- meters
altitude INTEGER, -- meters above sea level
heading INTEGER, -- degrees from north
speed DECIMAL(5, 2), -- meters per second
source VARCHAR(20) NOT NULL CHECK (source IN ('gps', 'network', 'manual', 'map', 'address', 'inherited', 'cached')),
address TEXT,
precision_level VARCHAR(20) CHECK (precision_level IN ('precise', 'approximate', 'city', 'region')),
inherited_from UUID REFERENCES object_instances(id),
captured_by UUID REFERENCES profiles(id) NOT NULL,
captured_at TIMESTAMPTZ DEFAULT now(),
device_info JSONB, -- browser, OS, device type
permission_status VARCHAR(10) CHECK (permission_status IN ('granted', 'denied', 'prompt'))
);
-- Indexes for performance
CREATE INDEX idx_location_events_object ON location_events(object_instance_id);
CREATE INDEX idx_location_events_captured_at ON location_events(captured_at DESC);
CREATE INDEX idx_location_events_event_type ON location_events(event_type);
-- RLS policy for location_events
CREATE POLICY location_events_access ON location_events
FOR ALL USING (
object_instance_id IN (
SELECT id FROM object_instances
-- Inherits RLS from object_instances
)
);
-- User location preferences table
CREATE TABLE user_location_preferences (
user_id UUID PRIMARY KEY REFERENCES profiles(id),
auto_detect_enabled BOOLEAN DEFAULT false,
ask_each_time BOOLEAN DEFAULT true,
share_with_organization BOOLEAN DEFAULT false,
precision_level VARCHAR(20) DEFAULT 'precise' CHECK (precision_level IN ('precise', 'approximate', 'city', 'region')),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- RLS for user preferences
CREATE POLICY user_location_preferences_access ON user_location_preferences
FOR ALL USING (user_id = auth.uid());
2. GraphQL Schema Extensions
New types and operations for location handling:
# Location Types
type GeographicalLocation {
latitude: Float!
longitude: Float!
accuracy: Int
altitude: Int
heading: Int
speed: Float
timestamp: String!
source: LocationSource!
address: String
precisionLevel: LocationPrecisionLevel!
inheritedFrom: ObjectInstance
}
enum LocationSource {
GPS
NETWORK
MANUAL
MAP_PICKER
ADDRESS_SEARCH
INHERITED
CACHED
}
enum LocationPrecisionLevel {
PRECISE
APPROXIMATE
CITY
REGION
}
# Location-based Input Types
input GeographicalLocationInput {
latitude: Float!
longitude: Float!
accuracy: Int
altitude: Int
heading: Int
speed: Float
source: LocationSource!
address: String
precisionLevel: LocationPrecisionLevel!
inheritedFromId: ID
}
input LocationBoundsInput {
northEast: LatLngInput!
southWest: LatLngInput!
}
input LatLngInput {
latitude: Float!
longitude: Float!
}
# Enhanced Object Creation
input CreateObjectInput {
name: String!
description: String
ownerOrgId: ID!
statuses: [String!]
photos: [Upload!]
location: GeographicalLocationInput # NEW
spatialParentId: ID
spatialRelationship: String
}
# Location-based Queries
extend type Query {
# Find objects within radius of coordinates
objectsNearLocation(
center: LatLngInput!
radiusKm: Float!
filters: ObjectFilters
organizationId: ID
): [ObjectInstanceWithDistance!]!
# Find objects within bounding box
objectsInBounds(
bounds: LocationBoundsInput!
filters: ObjectFilters
organizationId: ID
): [ObjectInstance!]!
# Get location suggestions based on address
locationSuggestions(
query: String!
organizationContext: ID
): [LocationSuggestion!]!
# Get reverse geocoding for coordinates
reverseGeocode(
latitude: Float!
longitude: Float!
): LocationAddress
}
# Location-based Mutations
extend type Mutation {
# Update object location
updateObjectLocation(
objectId: ID!
location: GeographicalLocationInput!
): ObjectInstance!
# Batch update locations for multiple objects
updateObjectLocations(
updates: [ObjectLocationUpdateInput!]!
): [ObjectInstance!]!
# Update user location preferences
updateLocationPreferences(
preferences: LocationPreferencesInput!
): UserLocationPreferences!
}
# Enhanced Types
type ObjectInstanceWithDistance {
object: ObjectInstance!
distance: Float! # Distance in meters
bearing: Float # Bearing in degrees
estimatedTravelTime: Int # Estimated travel time in minutes
}
type LocationSuggestion {
address: String!
coordinates: LatLngInput!
confidence: Float!
components: AddressComponents!
}
type LocationAddress {
formatted: String!
components: AddressComponents!
accuracy: LocationAccuracy!
}
type AddressComponents {
street: String
city: String
region: String
country: String
postalCode: String
}
type LocationAccuracy {
level: String! # 'street', 'city', 'region', 'country'
radius: Int # Accuracy radius in meters
}
# Enhanced ObjectInstance with location
extend type ObjectInstance {
location: GeographicalLocation
nearbyObjects(radiusKm: Float = 1.0): [ObjectInstanceWithDistance!]!
spatialAndGeographical: SpatialLocationContext!
}
type SpatialLocationContext {
# Combines spatial relationships with geographical context
spatialParent: ObjectInstance
spatialRelationship: String
geographicalLocation: GeographicalLocation
inheritedLocation: Boolean!
locationSource: LocationSource!
}
# User Location Preferences
type UserLocationPreferences {
autoDetectEnabled: Boolean!
askEachTime: Boolean!
saveForFutureUse: Boolean!
shareWithOrganization: Boolean!
precisionLevel: LocationPrecisionLevel!
inheritFromParent: Boolean!
}
input LocationPreferencesInput {
autoDetectEnabled: Boolean
askEachTime: Boolean
saveForFutureUse: Boolean
shareWithOrganization: Boolean
precisionLevel: LocationPrecisionLevel
inheritFromParent: Boolean
}
input ObjectLocationUpdateInput {
objectId: ID!
location: GeographicalLocationInput!
}
3. Location Service Backend Integration
Geocoding and reverse geocoding services:
class LocationService:
def __init__(self):
self.geocoding_client = GeocodingClient()
self.reverse_geocoding_client = ReverseGeocodingClient()
async def geocode_address(
self,
address: str,
organization_context: Optional[str] = None
) -> List[LocationSuggestion]:
"""
Convert address to coordinates with organization context
"""
# Use Google Maps API, OpenStreetMap Nominatim, or similar
results = await self.geocoding_client.geocode(
address,
bias_location=organization_context
)
return [
LocationSuggestion(
address=result.formatted_address,
coordinates=LatLng(result.lat, result.lng),
confidence=result.confidence,
components=result.address_components
)
for result in results
]
async def reverse_geocode(
self,
latitude: float,
longitude: float
) -> LocationAddress:
"""
Convert coordinates to human-readable address
"""
result = await self.reverse_geocoding_client.reverse_geocode(
latitude, longitude
)
return LocationAddress(
formatted=result.formatted_address,
components=result.address_components,
accuracy=result.accuracy
)
async def find_objects_near_location(
self,
center_lat: float,
center_lng: float,
radius_km: float,
organization_id: Optional[str] = None,
filters: Optional[ObjectFilters] = None
) -> List[ObjectInstanceWithDistance]:
"""
Find objects within specified radius using Neo4j spatial queries
Architecture Note: Location data lives in Neo4j per Plings data storage rules
"""
# Neo4j Cypher query for proximity search
neo4j_query = """
MATCH (obj:ObjectInstance)
WHERE obj.latitude IS NOT NULL AND obj.longitude IS NOT NULL
WITH obj,
point.distance(
point({latitude: $center_lat, longitude: $center_lng}),
point({latitude: obj.latitude, longitude: obj.longitude})
) AS distance
WHERE distance <= $radius_meters
RETURN obj.id AS id,
obj.name AS name,
obj.latitude AS latitude,
obj.longitude AS longitude,
obj.location_accuracy AS accuracy,
obj.location_source AS source,
obj.location_address AS address,
distance,
degrees(
point.azimuth(
point({latitude: $center_lat, longitude: $center_lng}),
point({latitude: obj.latitude, longitude: obj.longitude})
)
) AS bearing
ORDER BY distance
LIMIT 100
"""
radius_meters = radius_km * 1000
# Execute Neo4j query
async with self.neo4j_driver.session() as session:
result = await session.run(
neo4j_query,
center_lat=center_lat,
center_lng=center_lng,
radius_meters=radius_meters
)
neo4j_objects = [dict(record) async for record in result]
# Enrich with PostgreSQL metadata if needed
if neo4j_objects:
object_ids = [obj['id'] for obj in neo4j_objects]
pg_query = """
SELECT id, name, description, owner_organization_id,
main_image_url, status, created_at
FROM object_instances
WHERE id = ANY($1)
"""
# Apply organization filter if provided
if organization_id:
pg_query += " AND owner_organization_id = $2"
pg_objects = await self.db.fetch(pg_query, object_ids, organization_id)
else:
pg_objects = await self.db.fetch(pg_query, object_ids)
# Merge Neo4j location data with PostgreSQL metadata
pg_dict = {str(obj['id']): obj for obj in pg_objects}
results = []
for neo4j_obj in neo4j_objects:
obj_id = neo4j_obj['id']
if obj_id in pg_dict:
pg_obj = pg_dict[obj_id]
results.append(
ObjectInstanceWithDistance(
object=ObjectInstance(
id=obj_id,
name=pg_obj['name'],
description=pg_obj['description'],
status=pg_obj['status'],
location=GeographicalLocation(
latitude=neo4j_obj['latitude'],
longitude=neo4j_obj['longitude'],
accuracy=neo4j_obj['accuracy'],
source=neo4j_obj['source'],
address=neo4j_obj['address']
)
),
distance=neo4j_obj['distance'],
bearing=neo4j_obj['bearing'],
estimatedTravelTime=self._estimate_travel_time(neo4j_obj['distance'])
)
)
return results
return []
def _estimate_travel_time(self, distance_meters: float) -> int:
"""
Estimate walking time based on distance
"""
# Assume 5 km/h walking speed
walking_speed_ms = 5000 / 3600 # 5 km/h in m/s
time_seconds = distance_meters / walking_speed_ms
return int(time_seconds / 60) # Convert to minutes
User Interface Specifications
1. Location Detection Button Component
Component: LocationDetectionButton.tsx
const LocationDetectionButton = ({
onLocationDetected,
onLocationDenied,
detectionStatus
}: LocationDetectionButtonProps) => {
const [isDetecting, setIsDetecting] = useState(false);
const { requestLocation } = useLocationDetection();
const handleDetection = async () => {
setIsDetecting(true);
try {
const position = await requestLocation();
onLocationDetected({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
heading: position.coords.heading,
speed: position.coords.speed,
timestamp: Date.now(),
source: LocationSource.GPS
});
} catch (error) {
onLocationDenied(error);
} finally {
setIsDetecting(false);
}
};
return (
<div className="location-detection-section">
<div className="location-header">
<MapPinIcon />
<h3>Object Location</h3>
<Badge variant="secondary">Optional</Badge>
</div>
<div className="location-options">
<Button
onClick={handleDetection}
disabled={isDetecting}
variant="primary"
className="location-detect-button"
>
{isDetecting ? (
<>
<Spinner className="animate-spin" />
Detecting location...
</>
) : (
<>
<GpsIcon />
Use current location
</>
)}
</Button>
<Button
onClick={() => setManualMode(true)}
variant="outline"
>
<MapIcon />
Choose on map
</Button>
<Button
onClick={() => setAddressMode(true)}
variant="outline"
>
<SearchIcon />
Search address
</Button>
<Button
onClick={() => setSkipLocation(true)}
variant="ghost"
>
<XIcon />
Skip location
</Button>
</div>
</div>
);
};
2. Location Status Indicator Component
Component: LocationStatusIndicator.tsx
const LocationStatusIndicator = ({
coordinates,
address,
accuracy,
source,
onEdit,
onRemove
}: LocationStatusIndicatorProps) => {
const [showDetails, setShowDetails] = useState(false);
if (!coordinates) return null;
return (
<Card className="location-status-indicator">
<CardHeader>
<div className="location-header">
<MapPinIcon className="text-green-600" />
<span className="location-title">Location Set</span>
<LocationAccuracyBadge accuracy={accuracy} />
</div>
</CardHeader>
<CardContent>
<div className="location-details">
<div className="coordinates">
<span className="label">Coordinates:</span>
<span className="value">
{formatCoordinates(coordinates.latitude, coordinates.longitude)}
</span>
</div>
{address && (
<div className="address">
<span className="label">Address:</span>
<span className="value">{address}</span>
</div>
)}
<div className="accuracy">
<span className="label">Accuracy:</span>
<span className="value">±{accuracy}m</span>
</div>
<div className="source">
<span className="label">Source:</span>
<LocationSourceBadge source={source} />
</div>
{showDetails && (
<div className="additional-details">
{coordinates.altitude && (
<div className="altitude">
<span className="label">Altitude:</span>
<span className="value">{coordinates.altitude}m</span>
</div>
)}
{coordinates.heading && (
<div className="heading">
<span className="label">Heading:</span>
<span className="value">{coordinates.heading}°</span>
</div>
)}
<div className="timestamp">
<span className="label">Captured:</span>
<span className="value">
{formatTimestamp(coordinates.timestamp)}
</span>
</div>
</div>
)}
</div>
</CardContent>
<CardActions>
<Button
onClick={() => setShowDetails(!showDetails)}
variant="ghost"
size="sm"
>
{showDetails ? 'Hide Details' : 'Show Details'}
</Button>
<Button
onClick={onEdit}
variant="outline"
size="sm"
>
<EditIcon />
Edit
</Button>
<Button
onClick={onRemove}
variant="outline"
size="sm"
>
<XIcon />
Remove
</Button>
</CardActions>
</Card>
);
};
3. Location Permission Dialog Component
Component: LocationPermissionDialog.tsx
const LocationPermissionDialog = ({
isOpen,
onAllow,
onDeny,
onAskLater
}: LocationPermissionDialogProps) => {
return (
<Dialog open={isOpen} onOpenChange={onAskLater}>
<DialogContent className="location-permission-dialog">
<DialogHeader>
<DialogTitle>
<MapPinIcon />
Location Access
</DialogTitle>
<DialogDescription>
Plings would like to access your location to help you track where
objects are created and discover items near you.
</DialogDescription>
</DialogHeader>
<div className="permission-explanation">
<div className="benefit-list">
<div className="benefit-item">
<CheckIcon className="text-green-600" />
<span>Find objects near your current location</span>
</div>
<div className="benefit-item">
<CheckIcon className="text-green-600" />
<span>Automatically tag objects with their location</span>
</div>
<div className="benefit-item">
<CheckIcon className="text-green-600" />
<span>Track equipment across outdoor areas</span>
</div>
</div>
<div className="privacy-note">
<ShieldIcon className="text-blue-600" />
<span>
Your location is only used for object tracking and is never
shared outside your organization.
</span>
</div>
</div>
<DialogFooter>
<Button
onClick={onDeny}
variant="outline"
>
Deny
</Button>
<Button
onClick={onAskLater}
variant="outline"
>
Ask Each Time
</Button>
<Button
onClick={onAllow}
variant="primary"
>
Allow Location Access
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
4. Manual Location Entry Component
Component: ManualLocationEntry.tsx
const ManualLocationEntry = ({
onLocationSet,
onCancel,
initialCoordinates
}: ManualLocationEntryProps) => {
const [latitude, setLatitude] = useState(initialCoordinates?.latitude || '');
const [longitude, setLongitude] = useState(initialCoordinates?.longitude || '');
const [address, setAddress] = useState('');
const [mode, setMode] = useState<'coordinates' | 'address'>('coordinates');
const [isValidating, setIsValidating] = useState(false);
const { validateCoordinates } = useLocationValidation();
const { searchLocation } = useLocationSearch();
const handleCoordinateSubmit = async () => {
setIsValidating(true);
try {
const valid = await validateCoordinates(
parseFloat(latitude),
parseFloat(longitude)
);
if (valid) {
onLocationSet({
latitude: parseFloat(latitude),
longitude: parseFloat(longitude),
accuracy: 1000, // Manual entry has lower accuracy
timestamp: Date.now(),
source: LocationSource.MANUAL
});
}
} catch (error) {
// Handle validation error
} finally {
setIsValidating(false);
}
};
const handleAddressSubmit = async () => {
setIsValidating(true);
try {
const results = await searchLocation(address);
if (results.length > 0) {
const result = results[0];
onLocationSet({
latitude: result.coordinates.latitude,
longitude: result.coordinates.longitude,
accuracy: result.accuracy || 1000,
timestamp: Date.now(),
source: LocationSource.ADDRESS_SEARCH,
address: result.address
});
}
} catch (error) {
// Handle search error
} finally {
setIsValidating(false);
}
};
return (
<Card className="manual-location-entry">
<CardHeader>
<CardTitle>Enter Location</CardTitle>
<Tabs value={mode} onValueChange={setMode}>
<TabsList>
<TabsTrigger value="coordinates">Coordinates</TabsTrigger>
<TabsTrigger value="address">Address</TabsTrigger>
</TabsList>
</Tabs>
</CardHeader>
<CardContent>
{mode === 'coordinates' ? (
<div className="coordinate-entry">
<div className="input-group">
<Label htmlFor="latitude">Latitude</Label>
<Input
id="latitude"
type="number"
step="any"
min="-90"
max="90"
value={latitude}
onChange={(e) => setLatitude(e.target.value)}
placeholder="59.3293"
/>
</div>
<div className="input-group">
<Label htmlFor="longitude">Longitude</Label>
<Input
id="longitude"
type="number"
step="any"
min="-180"
max="180"
value={longitude}
onChange={(e) => setLongitude(e.target.value)}
placeholder="18.0686"
/>
</div>
<div className="coordinate-format-help">
<InfoIcon />
<span>
Enter coordinates in decimal degrees format (e.g., 59.3293, 18.0686)
</span>
</div>
</div>
) : (
<div className="address-entry">
<div className="input-group">
<Label htmlFor="address">Address</Label>
<Input
id="address"
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder="123 Main St, City, Country"
/>
</div>
<div className="address-format-help">
<InfoIcon />
<span>
Enter a complete address for best results
</span>
</div>
</div>
)}
</CardContent>
<CardActions>
<Button
onClick={onCancel}
variant="outline"
>
Cancel
</Button>
<Button
onClick={mode === 'coordinates' ? handleCoordinateSubmit : handleAddressSubmit}
disabled={isValidating || (mode === 'coordinates' && (!latitude || !longitude)) || (mode === 'address' && !address)}
variant="primary"
>
{isValidating ? (
<>
<Spinner className="animate-spin" />
Validating...
</>
) : (
'Set Location'
)}
</Button>
</CardActions>
</Card>
);
};
5. Location Map Picker Component
Component: LocationMapPicker.tsx
const LocationMapPicker = ({
initialLocation,
onLocationSelected,
onCancel
}: LocationMapPickerProps) => {
const [selectedLocation, setSelectedLocation] = useState(initialLocation);
const [mapCenter, setMapCenter] = useState(
initialLocation || { latitude: 59.3293, longitude: 18.0686 }
);
const handleMapClick = (event: MapMouseEvent) => {
const { lat, lng } = event.latlng;
setSelectedLocation({
latitude: lat,
longitude: lng,
accuracy: 100, // Map selection has moderate accuracy
timestamp: Date.now(),
source: LocationSource.MAP_PICKER
});
};
return (
<Dialog open={true} onOpenChange={onCancel}>
<DialogContent className="location-map-picker max-w-4xl">
<DialogHeader>
<DialogTitle>Select Location on Map</DialogTitle>
<DialogDescription>
Click on the map to select the precise location for this object.
</DialogDescription>
</DialogHeader>
<div className="map-container">
<MapContainer
center={[mapCenter.latitude, mapCenter.longitude]}
zoom={13}
className="location-picker-map"
onClick={handleMapClick}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{selectedLocation && (
<Marker
position={[selectedLocation.latitude, selectedLocation.longitude]}
draggable={true}
eventHandlers={{
dragend: (e) => {
const { lat, lng } = e.target.getLatLng();
setSelectedLocation({
...selectedLocation,
latitude: lat,
longitude: lng
});
}
}}
>
<Popup>
<div className="marker-popup">
<h4>Selected Location</h4>
<p>
{formatCoordinates(selectedLocation.latitude, selectedLocation.longitude)}
</p>
<p className="popup-hint">
Drag marker to adjust position
</p>
</div>
</Popup>
</Marker>
)}
</MapContainer>
</div>
{selectedLocation && (
<div className="selected-location-details">
<div className="location-info">
<MapPinIcon />
<span>
{formatCoordinates(selectedLocation.latitude, selectedLocation.longitude)}
</span>
</div>
</div>
)}
<DialogFooter>
<Button
onClick={onCancel}
variant="outline"
>
Cancel
</Button>
<Button
onClick={() => onLocationSelected(selectedLocation)}
disabled={!selectedLocation}
variant="primary"
>
Use This Location
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
6. Location Privacy Settings Component
Component: LocationPrivacySettings.tsx
const LocationPrivacySettings = ({
preferences,
onPreferencesChange
}: LocationPrivacySettingsProps) => {
return (
<Card className="location-privacy-settings">
<CardHeader>
<CardTitle>
<ShieldIcon />
Location Privacy
</CardTitle>
<CardDescription>
Control how your location information is used and shared.
</CardDescription>
</CardHeader>
<CardContent>
<div className="privacy-settings-grid">
<div className="setting-item">
<div className="setting-label">
<Label htmlFor="auto-detect">Auto-detect location</Label>
<span className="setting-description">
Automatically detect location when creating objects
</span>
</div>
<Switch
id="auto-detect"
checked={preferences.autoDetectEnabled}
onCheckedChange={(checked) =>
onPreferencesChange({
...preferences,
autoDetectEnabled: checked
})
}
/>
</div>
<div className="setting-item">
<div className="setting-label">
<Label htmlFor="ask-each-time">Ask each time</Label>
<span className="setting-description">
Always ask for permission before detecting location
</span>
</div>
<Switch
id="ask-each-time"
checked={preferences.askEachTime}
onCheckedChange={(checked) =>
onPreferencesChange({
...preferences,
askEachTime: checked
})
}
/>
</div>
<div className="setting-item">
<div className="setting-label">
<Label htmlFor="share-org">Share with organization</Label>
<span className="setting-description">
Allow organization members to see object locations
</span>
</div>
<Switch
id="share-org"
checked={preferences.shareWithOrganization}
onCheckedChange={(checked) =>
onPreferencesChange({
...preferences,
shareWithOrganization: checked
})
}
/>
</div>
<div className="setting-item">
<div className="setting-label">
<Label htmlFor="precision-level">Location precision</Label>
<span className="setting-description">
Control how precise location information is stored
</span>
</div>
<Select
value={preferences.precisionLevel}
onValueChange={(value) =>
onPreferencesChange({
...preferences,
precisionLevel: value as LocationPrecisionLevel
})
}
>
<SelectTrigger>
<SelectValue placeholder="Select precision" />
</SelectTrigger>
<SelectContent>
<SelectItem value="precise">Precise (±10m)</SelectItem>
<SelectItem value="approximate">Approximate (±100m)</SelectItem>
<SelectItem value="city">City level</SelectItem>
<SelectItem value="region">Region level</SelectItem>
</SelectContent>
</Select>
</div>
<div className="setting-item">
<div className="setting-label">
<Label htmlFor="inherit-parent">Inherit from parent</Label>
<span className="setting-description">
Use parent object's location for contained items
</span>
</div>
<Switch
id="inherit-parent"
checked={preferences.inheritFromParent}
onCheckedChange={(checked) =>
onPreferencesChange({
...preferences,
inheritFromParent: checked
})
}
/>
</div>
</div>
</CardContent>
</Card>
);
};
Performance Requirements
Location Detection Performance
- GPS Acquisition: <10 seconds for initial location fix
- Location Accuracy: Target ±10m for GPS, ±100m for network location
- Battery Optimization: Minimize GPS usage through intelligent caching
- Fallback Speed: <3 seconds to fallback to network location or manual entry
- Cache Efficiency: Reuse location for 5 minutes to avoid repeated GPS requests
Location-Based Search Performance
- Proximity Query: <500ms for “near me” searches within 5km radius
- Bounding Box Query: <1s for map-based searches with up to 1000 objects
- Geocoding: <2s for address-to-coordinates conversion
- Reverse Geocoding: <2s for coordinates-to-address conversion
- Map Rendering: <3s for initial map load with object markers
Frontend Performance
- State Management: Efficient location state updates without re-renders
- Map Integration: Lazy loading of map components and tile data
- Memory Usage: Optimize coordinate storage and map marker handling
- Network Optimization: Batch location-related API calls
- Offline Support: Cache location data for offline object creation
Backend Performance
- Spatial Indexing: PostGIS indexes for sub-100ms proximity queries
- Concurrent Requests: Handle 100+ concurrent location-based searches
- Database Optimization: Efficient spatial queries with proper indexing
- Caching Strategy: Cache geocoding results and frequent location searches
- Rate Limiting: Protect against abuse while maintaining performance
Error Handling and Fallbacks
Location Permission Errors
Comprehensive error handling strategy:
interface LocationErrorHandling {
// Permission denied by user
onPermissionDenied: () => void; // Show manual entry options
// Location unavailable (GPS disabled, no signal)
onLocationUnavailable: () => void; // Fallback to network location
// Location timeout (GPS taking too long)
onLocationTimeout: () => void; // Offer cached location or manual entry
// Position acquisition error
onPositionError: (error: GeolocationPositionError) => void;
// Network location failure
onNetworkLocationFailure: () => void; // Show manual entry only
// Geocoding service failure
onGeocodingFailure: () => void; // Continue with coordinates only
}
Graceful Degradation
- No Location Permission: Continue with manual entry and map picker
- GPS Unavailable: Fallback to network-based location
- Location Services Disabled: Show clear instructions for enabling
- Geocoding Failure: Store coordinates without address resolution
- Map Service Unavailable: Fallback to coordinate input only
User Experience During Errors
- Clear Error Messages: Explain why location detection failed
- Alternative Options: Always provide manual entry as backup
- Retry Mechanisms: Allow users to retry location detection
- Error Recovery: Gracefully handle partial failures
- Progressive Enhancement: Core functionality works without location
Security and Privacy
Location Data Protection
- Encrypted Storage: All location data encrypted at rest
- Secure Transmission: HTTPS-only for all location API calls
- Access Control: Organization-level location access controls
- Audit Logging: Log all location data access and modifications
- Data Retention: Configurable retention policies for location history
Privacy Controls
- Granular Permissions: Per-object location sharing controls
- Organization Settings: Admin control over location features
- User Consent: Clear consent for location collection and use
- Data Minimization: Only collect necessary location precision
- Anonymization: Option to anonymize location data in exports
Compliance Requirements
- GDPR Compliance: Right to deletion and data portability for location data
- Location Privacy: Clear privacy policy for location information
- Consent Management: Proper consent mechanisms for location tracking
- Data Subject Rights: Tools for users to control their location data
- Cross-Border Data: Comply with location data transfer regulations
Testing Requirements
Location Integration Testing
describe('Location Integration', () => {
test('should request location permission on first use', async () => {
// Test permission request flow
});
test('should detect GPS location successfully', async () => {
// Test GPS detection with mocked coordinates
});
test('should fallback to manual entry when GPS denied', async () => {
// Test graceful fallback to manual entry
});
test('should validate coordinate input correctly', async () => {
// Test coordinate validation logic
});
test('should save location with object creation', async () => {
// Test location data persistence
});
test('should find nearby objects accurately', async () => {
// Test proximity search functionality
});
test('should handle geocoding errors gracefully', async () => {
// Test geocoding service failure handling
});
});
Performance Testing
- GPS Acquisition Time: Test location detection speed across devices
- Location Query Performance: Stress test proximity searches
- Memory Usage: Monitor location state management efficiency
- Battery Impact: Measure GPS usage impact on mobile devices
- Network Efficiency: Test location API call optimization
User Experience Testing
- Permission Flow: Test location permission request UX
- Error Handling: Test all error scenarios and fallbacks
- Accessibility: Test location features with assistive technologies
- Mobile Usability: Test location features on mobile devices
- Cross-Platform: Test location functionality across browsers
Security Testing
- Data Encryption: Verify location data encryption at rest and in transit
- Access Control: Test organization-level location access controls
- Permission Validation: Test location permission enforcement
- Privacy Settings: Test privacy control functionality
- Audit Logging: Verify location access logging
Deployment and Configuration
Environment Configuration
# Location Service Configuration
location_services:
gps_detection:
enabled: true
timeout: 10000 # 10 second timeout
high_accuracy: true # Request high accuracy GPS
max_age: 300000 # 5 minute cache duration
geocoding:
provider: "google" # google, openstreetmap, mapbox
api_key: "${GEOCODING_API_KEY}"
timeout: 5000
rate_limit: 100 # requests per minute
reverse_geocoding:
enabled: true
provider: "google"
cache_duration: "1h"
location_features:
enabled: true
default_precision: "precise"
auto_detect_default: false
require_permission: true
max_accuracy_radius: 10000 # meters
# Privacy and Security
location_privacy:
encryption_at_rest: true
require_user_consent: true
data_retention_days: 365
allow_export: true
anonymize_exports: false
Feature Flags
interface LocationFeatureFlags {
locationTrackingEnabled: boolean;
gpsDetectionEnabled: boolean;
manualEntryEnabled: boolean;
mapPickerEnabled: boolean;
addressSearchEnabled: boolean;
proximitySearchEnabled: boolean;
locationInheritanceEnabled: boolean;
privacyControlsEnabled: boolean;
auditLoggingEnabled: boolean;
}
Database Migration
-- Migration: Add location tracking to objects
-- Version: 2025.07.01_location_tracking
-- Add location columns
ALTER TABLE object_instances
ADD COLUMN IF NOT EXISTS latitude DECIMAL(10, 8),
ADD COLUMN IF NOT EXISTS longitude DECIMAL(11, 8),
ADD COLUMN IF NOT EXISTS location_accuracy INTEGER,
ADD COLUMN IF NOT EXISTS location_altitude INTEGER,
ADD COLUMN IF NOT EXISTS location_heading INTEGER,
ADD COLUMN IF NOT EXISTS location_speed DECIMAL(5, 2),
ADD COLUMN IF NOT EXISTS location_timestamp BIGINT,
ADD COLUMN IF NOT EXISTS location_source VARCHAR(20),
ADD COLUMN IF NOT EXISTS location_address TEXT,
ADD COLUMN IF NOT EXISTS location_precision_level VARCHAR(20),
ADD COLUMN IF NOT EXISTS location_inherited_from UUID;
-- Add constraints
ALTER TABLE object_instances
ADD CONSTRAINT IF NOT EXISTS chk_location_source
CHECK (location_source IN ('gps', 'network', 'manual', 'map', 'address', 'inherited', 'cached'));
ALTER TABLE object_instances
ADD CONSTRAINT IF NOT EXISTS chk_location_precision
CHECK (location_precision_level IN ('precise', 'approximate', 'city', 'region'));
ALTER TABLE object_instances
ADD CONSTRAINT IF NOT EXISTS chk_latitude_range
CHECK (latitude >= -90 AND latitude <= 90);
ALTER TABLE object_instances
ADD CONSTRAINT IF NOT EXISTS chk_longitude_range
CHECK (longitude >= -180 AND longitude <= 180);
-- Add foreign key for location inheritance
ALTER TABLE object_instances
ADD CONSTRAINT IF NOT EXISTS fk_location_inherited_from
FOREIGN KEY (location_inherited_from) REFERENCES object_instances(id);
-- Enable PostGIS and add spatial column
CREATE EXTENSION IF NOT EXISTS postgis;
ALTER TABLE object_instances ADD COLUMN IF NOT EXISTS location_point GEOGRAPHY(POINT, 4326);
-- Create spatial index
CREATE INDEX IF NOT EXISTS idx_object_instances_location_point
ON object_instances USING GIST (location_point);
-- Create composite index for organization + location queries
CREATE INDEX IF NOT EXISTS idx_object_instances_org_location
ON object_instances (owner_org_id, location_point)
WHERE location_point IS NOT NULL;
-- Function to update location_point when lat/lon changes
CREATE OR REPLACE FUNCTION update_location_point()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.latitude IS NOT NULL AND NEW.longitude IS NOT NULL THEN
NEW.location_point = ST_MakePoint(NEW.longitude, NEW.latitude)::geography;
ELSE
NEW.location_point = NULL;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger to automatically update location_point
DROP TRIGGER IF EXISTS trigger_update_location_point ON object_instances;
CREATE TRIGGER trigger_update_location_point
BEFORE INSERT OR UPDATE ON object_instances
FOR EACH ROW EXECUTE FUNCTION update_location_point();
-- Create user location preferences table
CREATE TABLE IF NOT EXISTS user_location_preferences (
user_id UUID PRIMARY KEY,
auto_detect_enabled BOOLEAN DEFAULT false,
ask_each_time BOOLEAN DEFAULT true,
save_for_future_use BOOLEAN DEFAULT false,
share_with_organization BOOLEAN DEFAULT true,
precision_level VARCHAR(20) DEFAULT 'precise',
inherit_from_parent BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Add constraint for precision level
ALTER TABLE user_location_preferences
ADD CONSTRAINT IF NOT EXISTS chk_user_precision_level
CHECK (precision_level IN ('precise', 'approximate', 'city', 'region'));
-- Create location access audit log
CREATE TABLE IF NOT EXISTS location_access_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
object_id UUID,
action VARCHAR(50) NOT NULL,
coordinates GEOGRAPHY(POINT, 4326),
timestamp TIMESTAMPTZ DEFAULT now(),
ip_address INET,
user_agent TEXT
);
-- Index for audit log queries
CREATE INDEX IF NOT EXISTS idx_location_access_log_user_timestamp
ON location_access_log (user_id, timestamp);
CREATE INDEX IF NOT EXISTS idx_location_access_log_object_timestamp
ON location_access_log (object_id, timestamp);
Monitoring and Analytics
- Location Usage Metrics: Track location detection success rates
- GPS Performance: Monitor location acquisition times and accuracy
- Privacy Compliance: Track consent rates and privacy setting usage
- Search Performance: Monitor proximity search performance and usage
- Error Rates: Track location-related errors and fallback usage
- Battery Impact: Monitor GPS usage impact on mobile devices
Related Documentation
Integration References
- Use Case: geographical-location-tracking.md - Business requirements and user stories
- Current System: spatial-relationships.md - Existing spatial positioning system
- Object Creation: object-creation-requirements.md - Base modal architecture
- ScanEvent Location: neo4j-core-schema.md - Existing location tracking in scan events
Implementation Dependencies
- Frontend Components: Location detection, map integration, privacy controls
- Backend Services: Geocoding, reverse geocoding, proximity search
- Database Schema: Location storage, spatial indexing, privacy preferences
- API Extensions: GraphQL schema for location operations
- Privacy Framework: Consent management, data protection, audit logging
Requirements Document Created: Mån 7 Jul 2025 11:19:07 CEST - Technical specifications for Geographical Location Tracking integration