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='&copy; <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

Integration References

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