Documentation (#569)

This commit is contained in:
Daniel Graf
2025-12-20 14:33:03 +01:00
committed by GitHub
parent d1369c9499
commit c39caa5025
7 changed files with 112 additions and 23 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 560 KiB

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 6.3 MiB

View File

@@ -38,7 +38,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
### Data Import & Integration
- **Multiple Import Formats**: Support for GPX files, Google Takeout JSON, Google Timeline Exports and GeoJSON files
- **Real-time Data Ingestion**: Live location updates via OwnTracks and GPSLogger mobile apps
- **Batch Processing**: Efficient handling of large location datasets with queue-based processing
- **Batch Processing**: Efficient handling of large location datasets with direct processing
- **API Integration**: RESTful API for programmatic data access and ingestion
### Photo Management
@@ -66,7 +66,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
### Privacy & Self-hosting
- **Complete Data Control**: Your location data never leaves your server
- **Self-hosted Solution**: Deploy on your own infrastructure
- **Asynchronous Processing**: Handle large datasets efficiently with RabbitMQ-based processing
- **Asynchronous Processing**: Handle large datasets efficiently with direct processing and RabbitMQ-based task scheduling
## Getting Started
@@ -76,7 +76,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
- Maven 3.6 or higher
- Docker and Docker Compose
- PostgreSQL database with spatial extensions (PostGIS)
- RabbitMQ for message processing
- RabbitMQ
- Redis for caching
### Quick Start with Docker
@@ -186,7 +186,7 @@ docker run -p 8080:8080 \
The included `docker-compose.yml` provides a complete setup with:
- PostgreSQL with PostGIS extensions
- RabbitMQ for message processing
- RabbitMQ for task scheduling
- Redis for caching and session storage
- Reitti application with proper networking
- Persistent data volumes
@@ -249,12 +249,7 @@ The included `docker-compose.yml` provides a complete setup with:
- Real-time mobile app integration (OwnTracks, GPSLogger)
- REST API endpoints
2. **Queue Processing**: Data is queued in RabbitMQ for asynchronous processing:
- Raw location points are validated and stored
- Processing jobs are distributed across workers
- Queue status is monitored in real-time
3. **Analysis & Detection**: Processing workers analyze the data to:
2. **Analysis & Detection**: The application directly processes the data to:
- Detect significant places where you spend time
- Identify trips between locations
- Determine transport modes (walking, cycling, driving)
@@ -265,7 +260,12 @@ The included `docker-compose.yml` provides a complete setup with:
- Temporal indexing for timeline operations
- User data isolation and security
5. **Visualization**: Web interface displays processed data as:
3. **Task Scheduling**: RabbitMQ is used for scheduling background tasks:
- Reverse geocoding requests
- User notifications
- Other asynchronous operations
4. **Visualization**: Web interface displays processed data as:
- Interactive timeline with visits and trips
- Map visualization with location markers
- Photo integration showing images taken at locations
@@ -450,13 +450,13 @@ To enable PKCE for the OIDC Client, you need to set `OIDC_AUTHENTICATION_METHOD`
- **Backup Requirements:**
- The PostGIS database needs to be backed up regularly. This database contains all user location data, analysis results, and other persistent information.
- The storage path used by Reitti needs to be backed up regularly. This contains uploaded files.
- **Stateless Services:** All other components (RabbitMQ, Redis, Photon, etc.) are stateless and do not store any important data. These can be redeployed or restarted without risk of data loss.
- **Stateless Services:** All other components (RabbitMQ for task scheduling, Redis, Photon, etc.) are stateless and do not store any important data. These can be redeployed or restarted without risk of data loss.
**Recommended Backup Strategy:**
- Use standard PostgreSQL backup tools (such as `pg_dump` or physical volume snapshots) to back up your database.
- Back up the entire storage directory/volume used by Reitti for file storage.
- Ensure backups are performed regularly and stored securely.
- No backup is needed for RabbitMQ, Redis, Photon.
- No backup is needed for RabbitMQ (task scheduling), Redis, Photon.
**Restore:**
- In case of disaster recovery, restore both the PostGIS database and the storage path to recover all user data and history.

View File

@@ -521,7 +521,7 @@ function updatePointsList() {
html += `
<div class="point-item" id="${pointId}" onclick="selectPoint(${trackIndex}, ${pointIndex})" style="border-left-color: ${speedColor}">
<div class="point-content">
<div class="point-coords">${point.lat.toFixed(5)}, ${point.lng.toFixed(5)}</div>
<div class="point-coords">${point.lat.toFixed(4)}, ${point.lng.toFixed(4)}</div>
<div class="point-time">${formatCompactTimestamp(point.timestamp)}</div>
<div class="point-speed" style="color: ${speedColor}">${speedText}</div>
</div>
@@ -596,7 +596,20 @@ function formatTimestamp(timestamp) {
}
function formatCompactTimestamp(timestamp) {
return timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const today = new Date();
const pointDate = new Date(timestamp);
// Check if it's the same date as today
const isToday = pointDate.toDateString() === today.toDateString();
if (isToday) {
return pointDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
} else {
// Include date for different days
const dateStr = pointDate.toLocaleDateString([], { month: '2-digit', day: '2-digit' });
const timeStr = pointDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return `${dateStr} ${timeStr}`;
}
}
function updateSpeedLegend() {
@@ -1508,3 +1521,62 @@ function handleMarkerContextMenu(e) {
}
}
// Time shifting functions
function shiftTrackTime(amount, unit) {
if (tracks.length === 0 || currentTrackIndex >= tracks.length) {
alert('No track selected to shift time.');
return;
}
const currentTrack = tracks[currentTrackIndex];
if (currentTrack.points.length === 0) {
alert('Current track has no points to shift.');
return;
}
// Calculate milliseconds to shift
let shiftMs = 0;
if (unit === 'hour') {
shiftMs = amount * 60 * 60 * 1000; // hours to milliseconds
} else if (unit === 'day') {
shiftMs = amount * 24 * 60 * 60 * 1000; // days to milliseconds
}
// Shift all points in the current track
currentTrack.points.forEach(point => {
point.timestamp = new Date(point.timestamp.getTime() + shiftMs);
});
// Update track start time
if (currentTrack.points.length > 0) {
currentTrack.startTime = new Date(currentTrack.points[0].timestamp);
}
// Update the datetime input to reflect the new time of the last point
if (currentTrack.points.length > 0) {
const lastPoint = currentTrack.points[currentTrack.points.length - 1];
const timeInterval = parseInt(document.getElementById('timeInterval').value);
const nextTimestamp = new Date(lastPoint.timestamp.getTime() + (timeInterval * 1000));
// Format for datetime-local input
const year = nextTimestamp.getFullYear();
const month = String(nextTimestamp.getMonth() + 1).padStart(2, '0');
const day = String(nextTimestamp.getDate()).padStart(2, '0');
const hours = String(nextTimestamp.getHours()).padStart(2, '0');
const minutes = String(nextTimestamp.getMinutes()).padStart(2, '0');
const seconds = String(nextTimestamp.getSeconds()).padStart(2, '0');
const datetimeString = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
document.getElementById('startDateTime').value = datetimeString;
}
// Update UI
updatePointsList();
updateStatus();
// Show confirmation
const unitText = unit === 'hour' ? 'hour' : 'day';
const direction = amount > 0 ? 'forward' : 'backward';
const absAmount = Math.abs(amount);
}

View File

@@ -542,16 +542,16 @@
}
.point-item {
padding: 8px 20px;
padding: 6px 20px;
border-bottom: 1px solid rgba(237, 242, 247, 0.8);
cursor: pointer;
transition: all 0.3s ease;
font-size: 12px;
font-size: 10px;
position: relative;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 32px;
min-height: 28px;
border-left: 4px solid #e2e8f0;
}
@@ -568,7 +568,7 @@
.point-content {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
flex: 1;
font-family: 'Monaco', 'Menlo', monospace;
}
@@ -576,15 +576,17 @@
.point-coords {
color: #4a5568;
font-weight: 500;
min-width: 120px;
}
.point-time {
color: #718096;
min-width: 80px;
}
.point-speed {
font-weight: 500;
min-width: 60px;
min-width: 45px;
text-align: right;
}
@@ -903,6 +905,21 @@
<button onclick="newTrack()" class="control-button"> New Track</button>
</div>
</div>
<div class="drawer-section">
<div class="drawer-section-title">Time Adjustment</div>
<div class="control-group">
<label class="control-label">Shift Current Track</label>
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
<button onclick="shiftTrackTime(-1, 'hour')" class="control-button" style="flex: 1; font-size: 11px;">-1h</button>
<button onclick="shiftTrackTime(1, 'hour')" class="control-button" style="flex: 1; font-size: 11px;">+1h</button>
</div>
<div style="display: flex; gap: 8px;">
<button onclick="shiftTrackTime(-1, 'day')" class="control-button" style="flex: 1; font-size: 11px;">-1d</button>
<button onclick="shiftTrackTime(1, 'day')" class="control-button" style="flex: 1; font-size: 11px;">+1d</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -498,8 +498,8 @@ public class ReittiIntegrationService {
(String) placeInfo.get("address"),
(String) placeInfo.get("city"),
(String) placeInfo.get("countryCode"),
getDoubleValue(placeInfo, "latitudeCentroid"),
getDoubleValue(placeInfo, "longitudeCentroid"),
getDoubleValue(placeInfo, "lat"),
getDoubleValue(placeInfo, "lng"),
(String) placeInfo.get("type")
);
@@ -515,7 +515,7 @@ public class ReittiIntegrationService {
.toList();
// Parse summary data
long totalDurationMs = getLongValue(placeData, "totalDurationSeconds") * 1000; // Convert to milliseconds
long totalDurationMs = getLongValue(placeData, "totalDurationMs"); // Convert to milliseconds
int visitCount = getIntValue(placeData, "visitCount");
String color = "#3388ff"; // Default color, could be extracted from response if available