mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -05:00
Rename hexagon_centers to h3_hex_ids and update related logic
This commit is contained in:
parent
440b031a0c
commit
2bd0390d1a
10 changed files with 541 additions and 81 deletions
|
|
@ -57,9 +57,9 @@ class Stat < ApplicationRecord
|
|||
end
|
||||
|
||||
def hexagons_available?
|
||||
hexagon_centers.present? &&
|
||||
hexagon_centers.is_a?(Array) &&
|
||||
hexagon_centers.any?
|
||||
h3_hex_ids.present? &&
|
||||
h3_hex_ids.is_a?(Hash) &&
|
||||
h3_hex_ids.any?
|
||||
end
|
||||
|
||||
def generate_new_sharing_uuid!
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ module Maps
|
|||
|
||||
def call
|
||||
return build_response_from_centers if pre_calculated_centers_available?
|
||||
return handle_legacy_area_too_large if legacy_area_too_large?
|
||||
|
||||
nil # No pre-calculated data available
|
||||
end
|
||||
|
|
@ -19,78 +18,59 @@ module Maps
|
|||
attr_reader :stat, :user
|
||||
|
||||
def pre_calculated_centers_available?
|
||||
return false if stat&.hexagon_centers.blank?
|
||||
return false if stat&.h3_hex_ids.blank?
|
||||
|
||||
# Handle legacy hash format
|
||||
if stat.hexagon_centers.is_a?(Hash)
|
||||
!stat.hexagon_centers['area_too_large']
|
||||
else
|
||||
# Handle array format (actual hexagon centers)
|
||||
stat.hexagon_centers.is_a?(Array) && stat.hexagon_centers.any?
|
||||
end
|
||||
end
|
||||
|
||||
def legacy_area_too_large?
|
||||
stat&.hexagon_centers.is_a?(Hash) && stat.hexagon_centers['area_too_large']
|
||||
stat.h3_hex_ids.is_a?(Hash) && stat.h3_hex_ids.any?
|
||||
end
|
||||
|
||||
def build_response_from_centers
|
||||
centers = stat.hexagon_centers
|
||||
Rails.logger.debug "Using pre-calculated hexagon centers: #{centers.size} centers"
|
||||
hex_ids = stat.h3_hex_ids
|
||||
Rails.logger.debug "Using pre-calculated H3 hex IDs: #{hex_ids.size} hexagons"
|
||||
|
||||
result = build_hexagons_from_centers(centers)
|
||||
result = build_hexagons_from_h3_ids(hex_ids)
|
||||
{ success: true, data: result, pre_calculated: true }
|
||||
end
|
||||
|
||||
def handle_legacy_area_too_large
|
||||
Rails.logger.info "Recalculating previously skipped large area hexagons for stat #{stat.id}"
|
||||
|
||||
new_centers = recalculate_hexagon_centers
|
||||
return nil unless new_centers.is_a?(Array)
|
||||
|
||||
update_stat_with_new_centers(new_centers)
|
||||
end
|
||||
|
||||
def recalculate_hexagon_centers
|
||||
def recalculate_h3_hex_ids
|
||||
service = Stats::CalculateMonth.new(user.id, stat.year, stat.month)
|
||||
service.send(:calculate_hexagon_centers)
|
||||
service.send(:calculate_h3_hex_ids)
|
||||
end
|
||||
|
||||
def update_stat_with_new_centers(new_centers)
|
||||
stat.update(hexagon_centers: new_centers)
|
||||
result = build_hexagons_from_centers(new_centers)
|
||||
Rails.logger.debug "Successfully recalculated hexagon centers: #{new_centers.size} centers"
|
||||
def update_stat_with_new_hex_ids(new_hex_ids)
|
||||
stat.update(h3_hex_ids: new_hex_ids)
|
||||
result = build_hexagons_from_h3_ids(new_hex_ids)
|
||||
Rails.logger.debug "Successfully recalculated H3 hex IDs: #{new_hex_ids.size} hexagons"
|
||||
{ success: true, data: result, pre_calculated: true }
|
||||
end
|
||||
|
||||
def build_hexagons_from_centers(centers)
|
||||
# Convert stored centers back to hexagon polygons
|
||||
hexagon_features = centers.map.with_index { |center, index| build_hexagon_feature(center, index) }
|
||||
def build_hexagons_from_h3_ids(hex_ids)
|
||||
# Convert stored H3 IDs back to hexagon polygons
|
||||
hexagon_features = hex_ids.map.with_index do |(h3_index, data), index|
|
||||
build_hexagon_feature_from_h3(h3_index, data, index)
|
||||
end
|
||||
|
||||
build_feature_collection(hexagon_features)
|
||||
end
|
||||
|
||||
def build_hexagon_feature(center, index)
|
||||
lng, lat, earliest, latest = center
|
||||
def build_hexagon_feature_from_h3(h3_index, data, index)
|
||||
count, earliest, latest = data
|
||||
|
||||
{
|
||||
'type' => 'Feature',
|
||||
'id' => index + 1,
|
||||
'geometry' => generate_hexagon_geometry(lng, lat),
|
||||
'properties' => build_hexagon_properties(index, earliest, latest)
|
||||
'geometry' => generate_hexagon_geometry_from_h3(h3_index),
|
||||
'properties' => build_hexagon_properties(index, count, earliest, latest)
|
||||
}
|
||||
end
|
||||
|
||||
def generate_hexagon_geometry(lng, lat)
|
||||
Maps::HexagonPolygonGenerator.new(
|
||||
center_lng: lng,
|
||||
center_lat: lat
|
||||
).call
|
||||
def generate_hexagon_geometry_from_h3(h3_index)
|
||||
Maps::HexagonPolygonGenerator.new(h3_index: h3_index).call
|
||||
end
|
||||
|
||||
def build_hexagon_properties(index, earliest, latest)
|
||||
def build_hexagon_properties(index, count, earliest, latest)
|
||||
{
|
||||
'hex_id' => index + 1,
|
||||
'point_count' => count,
|
||||
'earliest_point' => earliest ? Time.zone.at(earliest).iso8601 : nil,
|
||||
'latest_point' => latest ? Time.zone.at(latest).iso8601 : nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
module Maps
|
||||
class HexagonPolygonGenerator
|
||||
def initialize(center_lng:, center_lat:, h3_resolution: 5)
|
||||
def initialize(center_lng: nil, center_lat: nil, h3_resolution: 5, h3_index: nil)
|
||||
@center_lng = center_lng
|
||||
@center_lat = center_lat
|
||||
@h3_resolution = h3_resolution
|
||||
@h3_index = h3_index
|
||||
end
|
||||
|
||||
def call
|
||||
|
|
@ -14,7 +15,7 @@ module Maps
|
|||
|
||||
private
|
||||
|
||||
attr_reader :center_lng, :center_lat, :h3_resolution
|
||||
attr_reader :center_lng, :center_lat, :h3_resolution, :h3_index
|
||||
|
||||
def generate_h3_hexagon_polygon
|
||||
# Convert coordinates to H3 format [lat, lng]
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class Stats::CalculateMonth
|
|||
daily_distance: distance_by_day,
|
||||
distance: distance(distance_by_day),
|
||||
toponyms: toponyms,
|
||||
hexagon_centers: calculate_hexagon_centers
|
||||
h3_hex_ids: calculate_h3_hex_ids
|
||||
)
|
||||
stat.save
|
||||
end
|
||||
|
|
@ -132,22 +132,28 @@ class Stats::CalculateMonth
|
|||
Stat.where(year:, month:, user:).destroy_all
|
||||
end
|
||||
|
||||
def calculate_hexagon_centers
|
||||
return nil if points.empty?
|
||||
def calculate_h3_hex_ids
|
||||
return {} if points.empty?
|
||||
|
||||
begin
|
||||
result = calculate_h3_hexagon_centers
|
||||
|
||||
if result.empty?
|
||||
Rails.logger.info "No H3 hexagon centers calculated for user #{user.id}, #{year}-#{month} (no data)"
|
||||
return nil
|
||||
Rails.logger.info "No H3 hex IDs calculated for user #{user.id}, #{year}-#{month} (no data)"
|
||||
return {}
|
||||
end
|
||||
|
||||
Rails.logger.info "Pre-calculated #{result.size} H3 hexagon centers for user #{user.id}, #{year}-#{month}"
|
||||
result
|
||||
# Convert array format to hash format: { h3_index => [count, earliest, latest] }
|
||||
hex_hash = result.each_with_object({}) do |hex_data, hash|
|
||||
h3_index, count, earliest, latest = hex_data
|
||||
hash[h3_index] = [count, earliest, latest]
|
||||
end
|
||||
|
||||
Rails.logger.info "Pre-calculated #{hex_hash.size} H3 hex IDs for user #{user.id}, #{year}-#{month}"
|
||||
hex_hash
|
||||
rescue PostGISError => e
|
||||
Rails.logger.warn "H3 hexagon centers calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}"
|
||||
nil
|
||||
Rails.logger.warn "H3 hex IDs calculation failed for user #{user.id}, #{year}-#{month}: #{e.message}"
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddHexagonDataToStats < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :stats, :hexagon_data, :jsonb
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
class AddHexagonCentersToStats < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :stats, :hexagon_centers, :jsonb
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
class AddIndexToHexagonCenters < ActiveRecord::Migration[8.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def change
|
||||
add_index :stats, :hexagon_centers, using: :gin, where: "hexagon_centers IS NOT NULL", algorithm: :concurrently
|
||||
end
|
||||
end
|
||||
8
db/migrate/20250918215512_add_h3_hex_ids_to_stats.rb
Normal file
8
db/migrate/20250918215512_add_h3_hex_ids_to_stats.rb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddH3HexIdsToStats < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
add_column :stats, :h3_hex_ids, :jsonb, default: {}
|
||||
add_index :stats, :h3_hex_ids, using: :gin, where: "(h3_hex_ids IS NOT NULL AND h3_hex_ids != '{}'::jsonb)"
|
||||
end
|
||||
end
|
||||
5
db/schema.rb
generated
5
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_09_14_095157) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2025_09_10_224714) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
enable_extension "postgis"
|
||||
|
|
@ -222,10 +222,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_14_095157) do
|
|||
t.jsonb "daily_distance", default: {}
|
||||
t.jsonb "sharing_settings", default: {}
|
||||
t.uuid "sharing_uuid"
|
||||
t.jsonb "hexagon_data"
|
||||
t.jsonb "hexagon_centers"
|
||||
t.index ["distance"], name: "index_stats_on_distance"
|
||||
t.index ["hexagon_centers"], name: "index_stats_on_hexagon_centers", where: "(hexagon_centers IS NOT NULL)", using: :gin
|
||||
t.index ["month"], name: "index_stats_on_month"
|
||||
t.index ["sharing_uuid"], name: "index_stats_on_sharing_uuid", unique: true
|
||||
t.index ["user_id"], name: "index_stats_on_user_id"
|
||||
|
|
|
|||
487
docs/SHAREABLE_STATS_FEATURE.md
Normal file
487
docs/SHAREABLE_STATS_FEATURE.md
Normal file
|
|
@ -0,0 +1,487 @@
|
|||
# Shareable Stats Feature Documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The Shareable Stats feature allows Dawarich users to publicly share their monthly location statistics without requiring authentication. This system provides a secure, time-limited way to share location insights while maintaining user privacy through configurable expiration settings and unguessable UUID-based access.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Time-based expiration**: Share links can expire after 1 hour, 12 hours, 24 hours, or be permanent
|
||||
- **UUID-based security**: Each shared stat has a unique, unguessable UUID for secure access
|
||||
- **Public API access**: Hexagon map data can be accessed via API without authentication when sharing is enabled
|
||||
- **H3 Hexagon visualization**: Enhanced geographic data visualization using Uber's H3 hexagonal hierarchical spatial index
|
||||
- **Automatic expiration**: Expired shares are automatically inaccessible
|
||||
- **Privacy controls**: Users can enable/disable sharing and regenerate sharing URLs at any time
|
||||
|
||||
## Database Schema
|
||||
|
||||
### Stats Table Extensions
|
||||
|
||||
The sharing functionality extends the `stats` table with the following columns:
|
||||
|
||||
```sql
|
||||
-- Public sharing configuration
|
||||
sharing_settings JSONB DEFAULT {}
|
||||
sharing_uuid UUID
|
||||
|
||||
-- Pre-calculated H3 hexagon data for performance
|
||||
h3_hex_ids JSONB DEFAULT {}
|
||||
|
||||
-- Indexes for performance
|
||||
INDEX ON h3_hex_ids USING GIN WHERE (h3_hex_ids IS NOT NULL AND h3_hex_ids != '{}'::jsonb)
|
||||
```
|
||||
|
||||
### Sharing Settings Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"expiration": "24h", // "1h", "12h", "24h", or "permanent"
|
||||
"expires_at": "2024-01-15T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### H3 Hex IDs Data Format
|
||||
|
||||
The `h3_hex_ids` column stores pre-calculated H3 hexagon data as a hash:
|
||||
|
||||
```json
|
||||
{
|
||||
"8a1fb46622dffff": [15, 1640995200, 1640998800],
|
||||
"8a1fb46622e7fff": [8, 1640996400, 1640999200],
|
||||
// ... more H3 index entries
|
||||
// Format: { "h3_index_string": [point_count, earliest_timestamp, latest_timestamp] }
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture Components
|
||||
|
||||
### Models
|
||||
|
||||
#### Stat Model (`app/models/stat.rb`)
|
||||
|
||||
**Key Methods:**
|
||||
- `sharing_enabled?`: Checks if sharing is enabled
|
||||
- `sharing_expired?`: Validates expiration status
|
||||
- `public_accessible?`: Combined check for sharing availability
|
||||
- `hexagons_available?`: Verifies pre-calculated H3 hex data exists
|
||||
- `enable_sharing!(expiration:)`: Enables sharing with expiration
|
||||
- `disable_sharing!`: Disables sharing
|
||||
- `generate_new_sharing_uuid!`: Regenerates sharing UUID
|
||||
- `calculate_data_bounds`: Calculates geographic bounds for the month
|
||||
|
||||
### Controllers
|
||||
|
||||
#### Shared::StatsController (`app/controllers/shared/stats_controller.rb`)
|
||||
|
||||
Handles public sharing functionality:
|
||||
|
||||
**Routes:**
|
||||
- `GET /shared/stats/:uuid` - Public view of shared stats
|
||||
- `PATCH /stats/:year/:month/sharing` - Sharing management (authenticated)
|
||||
|
||||
**Key Methods:**
|
||||
- `show`: Renders public stats view without authentication
|
||||
- `update`: Manages sharing settings (enable/disable, expiration)
|
||||
|
||||
#### Api::V1::Maps::HexagonsController (`app/controllers/api/v1/maps/hexagons_controller.rb`)
|
||||
|
||||
Provides hexagon data for both authenticated and public access:
|
||||
|
||||
**Features:**
|
||||
- Skip authentication for public sharing requests (`uuid` parameter)
|
||||
- Context resolution for public vs. authenticated access
|
||||
- Error handling for missing or expired shares
|
||||
|
||||
```ruby
|
||||
# Public access via UUID
|
||||
GET /api/v1/maps/hexagons?uuid=SHARING_UUID
|
||||
|
||||
# Authenticated access
|
||||
GET /api/v1/maps/hexagons?start_date=2024-01-01&end_date=2024-01-31
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
#### Maps::HexagonRequestHandler (`app/services/maps/hexagon_request_handler.rb`)
|
||||
|
||||
Central service for processing hexagon requests:
|
||||
|
||||
**Workflow:**
|
||||
1. Attempts to find matching stat for the request
|
||||
2. Delegates to `HexagonCenterManager` for pre-calculated data
|
||||
3. Returns empty feature collection if no data available
|
||||
|
||||
#### Maps::HexagonCenterManager (`app/services/maps/hexagon_center_manager.rb`)
|
||||
|
||||
Manages pre-calculated H3 hexagon data:
|
||||
|
||||
**Responsibilities:**
|
||||
- Retrieves pre-calculated H3 hex IDs from database
|
||||
- Converts stored H3 indexes to GeoJSON polygons
|
||||
- Builds hexagon features with point counts and timestamps
|
||||
- Handles efficient polygon generation from H3 indexes
|
||||
|
||||
**Data Flow:**
|
||||
1. Check if pre-calculated H3 hex IDs are available
|
||||
2. Convert H3 indexes to hexagon polygons using `HexagonPolygonGenerator`
|
||||
3. Build GeoJSON FeatureCollection with metadata and point counts
|
||||
|
||||
#### Stats::CalculateMonth (`app/services/stats/calculate_month.rb`)
|
||||
|
||||
Responsible for calculating and storing hexagon data during stats processing:
|
||||
|
||||
**H3 Configuration:**
|
||||
- `DEFAULT_H3_RESOLUTION = 8`: Small hexagons for good detail
|
||||
- `MAX_HEXAGONS = 10_000`: Maximum to prevent memory issues
|
||||
|
||||
**Key Methods:**
|
||||
- `calculate_h3_hex_ids`: Main method for H3 calculation and storage
|
||||
- `calculate_h3_hexagon_centers`: Internal H3 calculation logic
|
||||
- `calculate_h3_indexes`: Groups points into H3 hexagons
|
||||
- `fetch_user_points_for_period`: Retrieves points for date range
|
||||
|
||||
**Algorithm:**
|
||||
1. Fetch user points for the specified month
|
||||
2. Convert each point to H3 index at specified resolution
|
||||
3. Aggregate points per hexagon with count and timestamp bounds
|
||||
4. Apply resolution reduction if hexagon count exceeds maximum
|
||||
5. Store as hash of { h3_index_string => [count, earliest, latest] }
|
||||
|
||||
#### Maps::HexagonPolygonGenerator (`app/services/maps/hexagon_polygon_generator.rb`)
|
||||
|
||||
Converts H3 indexes back to polygon geometry:
|
||||
|
||||
**Features:**
|
||||
- Uses H3 library for accurate hexagon boundaries
|
||||
- Converts coordinates to GeoJSON Polygon format
|
||||
- Supports both center-based and H3-index-based generation
|
||||
- Direct H3 index to polygon conversion for efficiency
|
||||
|
||||
**Usage Modes:**
|
||||
- **Center-based**: `new(center_lng: lng, center_lat: lat)`
|
||||
|
||||
## H3 Hexagon System
|
||||
|
||||
### What is H3?
|
||||
|
||||
H3 is Uber's Hexagonal Hierarchical Spatial Index that provides:
|
||||
- **Uniform coverage**: Earth divided into hexagonal cells
|
||||
- **Hierarchical resolution**: 16 levels from global to local
|
||||
- **Efficient indexing**: Fast spatial queries and aggregations
|
||||
- **Consistent shape**: Hexagons have uniform neighbors
|
||||
|
||||
### Resolution Levels
|
||||
|
||||
Dawarich uses H3 resolution 8 by default:
|
||||
- **Resolution 8**: ~737m average hexagon edge length
|
||||
- **Fallback mechanism**: Reduces resolution if too many hexagons
|
||||
- **Maximum limit**: 10,000 hexagons to prevent memory issues
|
||||
|
||||
### Performance Benefits
|
||||
|
||||
1. **Pre-calculation**: H3 hexagons calculated once during stats processing
|
||||
2. **Efficient storage**: Hash-based storage with H3 index as key
|
||||
3. **Fast retrieval**: Database lookup instead of real-time calculation
|
||||
4. **Reduced bandwidth**: Compact JSON hash format for API responses
|
||||
5. **Direct polygon generation**: H3 index directly converts to polygon boundaries
|
||||
|
||||
## Workflow
|
||||
|
||||
### 1. Stats Calculation Process
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Data Import] --> B[Stats::CalculateMonth Service]
|
||||
B --> C[Calculate H3 Hexagon Centers]
|
||||
C --> D[Store in hexagon_centers Column]
|
||||
D --> E[Stats Available for Sharing]
|
||||
```
|
||||
|
||||
**Detailed Steps:**
|
||||
1. User imports location data (GPX, JSON, etc.)
|
||||
2. Background job triggers `Stats::CalculateMonth`
|
||||
3. Service calculates monthly statistics including H3 hex IDs
|
||||
4. H3 indexes are calculated for all points in the month
|
||||
5. Results stored in `stats.h3_hex_ids` as JSON hash
|
||||
|
||||
### 2. Sharing Activation
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[User Visits Stats Page] --> B[Enable Sharing Toggle]
|
||||
B --> C[Select Expiration Duration]
|
||||
C --> D[PATCH /stats/:year/:month/sharing]
|
||||
D --> E[Generate/Update sharing_uuid]
|
||||
E --> F[Set sharing_settings]
|
||||
F --> G[Return Public URL]
|
||||
```
|
||||
|
||||
**Sharing Settings:**
|
||||
- **Expiration options**: 1h, 12h, 24h, permanent
|
||||
- **UUID generation**: Secure random UUID for each stat
|
||||
- **Expiration timestamp**: Calculated and stored in sharing_settings
|
||||
|
||||
### 3. Public Access Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Public User Visits Shared URL] --> B[Validate UUID & Expiration]
|
||||
B --> C{Valid & Not Expired?}
|
||||
C -->|Yes| D[Load Public Stats View]
|
||||
C -->|No| E[Redirect with Error]
|
||||
D --> F[Render Map with Hexagons]
|
||||
F --> G[Load Hexagon Data via API]
|
||||
G --> H[Display Interactive Map]
|
||||
```
|
||||
|
||||
**Security Checks:**
|
||||
1. Verify sharing UUID exists in database
|
||||
2. Check `sharing_settings.enabled = true`
|
||||
3. Validate expiration timestamp if not permanent
|
||||
4. Return 404 if any check fails
|
||||
|
||||
### 4. Hexagon Data Retrieval
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Map Requests Hexagon Data] --> B[GET /api/v1/maps/hexagons?uuid=UUID]
|
||||
B --> C[HexagonsController]
|
||||
C --> D[Skip Authentication for UUID Request]
|
||||
D --> E[HexagonRequestHandler]
|
||||
E --> F[Find Stat by UUID]
|
||||
F --> G[HexagonCenterManager]
|
||||
G --> H[Load Pre-calculated Centers]
|
||||
H --> I[Convert to GeoJSON Polygons]
|
||||
I --> J[Return FeatureCollection]
|
||||
```
|
||||
|
||||
**Data Transformation:**
|
||||
1. Retrieve stored H3 hex IDs hash from database
|
||||
2. Convert each H3 index to hexagon boundary coordinates
|
||||
3. Build GeoJSON Feature with properties (point count, timestamps)
|
||||
4. Return complete FeatureCollection for map rendering
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Public Sharing
|
||||
|
||||
#### View Shared Stats
|
||||
```http
|
||||
GET /shared/stats/:uuid
|
||||
```
|
||||
- **Authentication**: None required
|
||||
- **Response**: HTML page with public stats view
|
||||
- **Error Handling**: Redirects to root with alert if invalid/expired
|
||||
|
||||
#### Get Hexagon Data
|
||||
```http
|
||||
GET /api/v1/maps/hexagons?uuid=:uuid
|
||||
```
|
||||
- **Authentication**: None required for UUID access
|
||||
- **Response**: GeoJSON FeatureCollection
|
||||
- **Features**: Each feature represents one hexagon with point count and timestamps
|
||||
|
||||
### Authenticated Management
|
||||
|
||||
#### Toggle Sharing
|
||||
```http
|
||||
PATCH /stats/:year/:month/sharing
|
||||
```
|
||||
**Parameters:**
|
||||
- `enabled`: "1" to enable, "0" to disable
|
||||
- `expiration`: "1h", "12h", "24h", or "permanent" (when enabling)
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"sharing_url": "https://domain.com/shared/stats/uuid",
|
||||
"message": "Sharing enabled successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## Security Features
|
||||
|
||||
### UUID-based Access
|
||||
- **Unguessable URLs**: Uses secure random UUIDs
|
||||
- **No enumeration**: Can't guess valid sharing links
|
||||
- **Automatic generation**: New UUID created for each sharing activation
|
||||
|
||||
### Time-based Expiration
|
||||
- **Configurable duration**: Multiple expiration options
|
||||
- **Automatic enforcement**: Expired shares become inaccessible
|
||||
- **Precise timestamping**: ISO8601 format with timezone awareness
|
||||
|
||||
### Limited Data Exposure
|
||||
- **No user identification**: Public view doesn't expose user details
|
||||
- **Aggregated data only**: Only statistical summaries are shared
|
||||
- **No raw location points**: Individual coordinates not exposed
|
||||
|
||||
### Privacy Controls
|
||||
- **User control**: Users can enable/disable sharing at any time
|
||||
- **UUID regeneration**: Can generate new sharing URL to invalidate old ones
|
||||
- **Granular permissions**: Per-month sharing control
|
||||
|
||||
## Frontend Integration
|
||||
|
||||
### Public View Template (`app/views/stats/public_month.html.erb`)
|
||||
|
||||
**Features:**
|
||||
- **Responsive design**: Mobile-friendly layout with Tailwind CSS
|
||||
- **Monthly statistics**: Distance, active days, countries visited
|
||||
- **Interactive hexagon map**: Leaflet.js with H3 hexagon overlay
|
||||
- **Activity charts**: Daily distance visualization
|
||||
- **Location summary**: Countries and cities visited
|
||||
|
||||
**Map Integration:**
|
||||
```erb
|
||||
<div id="public-monthly-stats-map"
|
||||
data-controller="public-stat-map"
|
||||
data-public-stat-map-uuid-value="<%= @stat.sharing_uuid %>"
|
||||
data-public-stat-map-hexagons-available-value="<%= @hexagons_available.to_s %>">
|
||||
</div>
|
||||
```
|
||||
|
||||
### JavaScript Controller
|
||||
|
||||
**Stimulus Controller**: `public-stat-map`
|
||||
- **Leaflet initialization**: Sets up interactive map
|
||||
- **Hexagon layer**: Loads and renders hexagon data from API
|
||||
- **User interaction**: Click handlers, zoom controls
|
||||
- **Loading states**: Shows loading spinner during data fetch
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Pre-calculation Strategy
|
||||
- **Background processing**: Hexagons calculated during stats job
|
||||
- **Storage efficiency**: H3 indexes are compact
|
||||
- **Query optimization**: GIN index on hexagon_centers column
|
||||
- **Caching**: Pre-calculated data serves multiple requests
|
||||
|
||||
### Memory Management
|
||||
- **Hexagon limits**: Maximum 10,000 hexagons per month
|
||||
- **Resolution fallback**: Automatically reduces detail for large areas
|
||||
- **Lazy loading**: Only calculate when stats are processed
|
||||
- **Efficient formats**: JSON storage optimized for size
|
||||
|
||||
### Database Optimization
|
||||
```sql
|
||||
-- Optimized queries
|
||||
SELECT h3_hex_ids FROM stats
|
||||
WHERE sharing_uuid = ? AND sharing_settings->>'enabled' = 'true';
|
||||
|
||||
-- Index for performance
|
||||
CREATE INDEX index_stats_on_h3_hex_ids
|
||||
ON stats USING gin (h3_hex_ids)
|
||||
WHERE (h3_hex_ids IS NOT NULL AND h3_hex_ids != '{}'::jsonb);
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Validation Errors
|
||||
- **Missing UUID**: 404 response with user-friendly message
|
||||
- **Expired sharing**: Redirect with appropriate alert
|
||||
- **Invalid parameters**: Bad request with error details
|
||||
|
||||
### Service Errors
|
||||
- **H3 calculation failures**: Graceful degradation, logs warning
|
||||
- **Database errors**: Transaction rollback, user notification
|
||||
- **Memory issues**: Resolution reduction, retry mechanism
|
||||
|
||||
### Frontend Resilience
|
||||
- **Loading states**: User feedback during data fetching
|
||||
- **Fallback content**: Display stats even if hexagons fail
|
||||
- **Error messages**: Clear communication of issues
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# H3 hexagon settings (optional, defaults shown)
|
||||
H3_DEFAULT_RESOLUTION=8
|
||||
H3_MAX_HEXAGONS=10000
|
||||
|
||||
# Feature flags
|
||||
ENABLE_PUBLIC_SHARING=true
|
||||
```
|
||||
|
||||
### Runtime Configuration
|
||||
- **Resolution adaptation**: Automatic based on data size
|
||||
- **Expiration options**: Configurable in sharing settings
|
||||
- **Security headers**: CORS configuration for API access
|
||||
|
||||
## Monitoring and Analytics
|
||||
|
||||
### Logging
|
||||
- **Share creation**: Log when sharing is enabled
|
||||
- **Public access**: Log UUID-based requests (without exposing UUID)
|
||||
- **Performance metrics**: H3 calculation timing
|
||||
- **Error tracking**: Failed calculations and API errors
|
||||
|
||||
### Metrics
|
||||
- **Sharing adoption**: How many users enable sharing
|
||||
- **Expiration preferences**: Popular expiration durations
|
||||
- **Performance**: Hexagon calculation and rendering times
|
||||
- **Error rates**: Failed sharing requests
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### No Hexagons Displayed
|
||||
1. Check if `hexagons_available?` returns true
|
||||
2. Verify `h3_hex_ids` column has data
|
||||
3. Confirm H3 library is properly installed
|
||||
4. Check API endpoint returns valid GeoJSON
|
||||
|
||||
#### Sharing Link Not Working
|
||||
1. Verify UUID exists in database
|
||||
2. Check sharing_settings.enabled = true
|
||||
3. Validate expiration timestamp
|
||||
4. Confirm public routes are properly configured
|
||||
|
||||
#### Performance Issues
|
||||
1. Monitor hexagon count (should be < 10,000)
|
||||
2. Check if resolution is too high for large areas
|
||||
3. Verify database indexes are present
|
||||
4. Consider increasing H3_MAX_HEXAGONS if needed
|
||||
|
||||
### Debug Commands
|
||||
|
||||
```bash
|
||||
# Check sharing status for a stat
|
||||
rails runner "
|
||||
stat = Stat.find_by(sharing_uuid: 'UUID_HERE')
|
||||
puts stat.public_accessible?
|
||||
puts stat.hexagons_available?
|
||||
"
|
||||
|
||||
# Verify H3 hex data format
|
||||
rails runner "
|
||||
stat = Stat.first
|
||||
puts stat.h3_hex_ids.class
|
||||
puts stat.h3_hex_ids.first
|
||||
"
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
- **Social sharing**: Integration with social media platforms
|
||||
- **Embedding**: Iframe widgets for external sites
|
||||
- **Analytics**: View count and engagement metrics
|
||||
- **Custom styling**: User-configurable map themes
|
||||
|
||||
### Technical Improvements
|
||||
- **CDN integration**: Faster global access to shared stats
|
||||
- **Compression**: Further optimize H3 hex data storage
|
||||
- **Real-time updates**: Live sharing for ongoing activities
|
||||
- **API versioning**: Stable API contracts for external integration
|
||||
- **H3 resolution optimization**: Dynamic resolution based on geographic area
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Shareable Stats feature provides a robust, secure, and performant way for Dawarich users to share their location insights. The H3 hexagon system offers excellent visualization while maintaining privacy through aggregated data. The UUID-based security model ensures that only intended recipients can access shared statistics, while the configurable expiration system gives users complete control over data visibility.
|
||||
|
||||
The architecture is designed for scalability and performance, with pre-calculated data reducing server load and providing fast response times for public viewers. The comprehensive error handling and monitoring ensure reliable operation in production environments.
|
||||
Loading…
Reference in a new issue