mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-09 17:37:57 -05:00
Documentation (#569)
This commit is contained in:
BIN
.github/screenshots/livemode.png
vendored
BIN
.github/screenshots/livemode.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 560 KiB After Width: | Height: | Size: 5.6 MiB |
BIN
.github/screenshots/main.png
vendored
BIN
.github/screenshots/main.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 6.9 MiB |
BIN
.github/screenshots/multiple-users.png
vendored
BIN
.github/screenshots/multiple-users.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 6.3 MiB |
26
README.md
26
README.md
@@ -38,7 +38,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
|
|||||||
### Data Import & Integration
|
### Data Import & Integration
|
||||||
- **Multiple Import Formats**: Support for GPX files, Google Takeout JSON, Google Timeline Exports and GeoJSON files
|
- **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
|
- **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
|
- **API Integration**: RESTful API for programmatic data access and ingestion
|
||||||
|
|
||||||
### Photo Management
|
### Photo Management
|
||||||
@@ -66,7 +66,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
|
|||||||
### Privacy & Self-hosting
|
### Privacy & Self-hosting
|
||||||
- **Complete Data Control**: Your location data never leaves your server
|
- **Complete Data Control**: Your location data never leaves your server
|
||||||
- **Self-hosted Solution**: Deploy on your own infrastructure
|
- **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
|
## Getting Started
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
|
|||||||
- Maven 3.6 or higher
|
- Maven 3.6 or higher
|
||||||
- Docker and Docker Compose
|
- Docker and Docker Compose
|
||||||
- PostgreSQL database with spatial extensions (PostGIS)
|
- PostgreSQL database with spatial extensions (PostGIS)
|
||||||
- RabbitMQ for message processing
|
- RabbitMQ
|
||||||
- Redis for caching
|
- Redis for caching
|
||||||
|
|
||||||
### Quick Start with Docker
|
### Quick Start with Docker
|
||||||
@@ -186,7 +186,7 @@ docker run -p 8080:8080 \
|
|||||||
|
|
||||||
The included `docker-compose.yml` provides a complete setup with:
|
The included `docker-compose.yml` provides a complete setup with:
|
||||||
- PostgreSQL with PostGIS extensions
|
- PostgreSQL with PostGIS extensions
|
||||||
- RabbitMQ for message processing
|
- RabbitMQ for task scheduling
|
||||||
- Redis for caching and session storage
|
- Redis for caching and session storage
|
||||||
- Reitti application with proper networking
|
- Reitti application with proper networking
|
||||||
- Persistent data volumes
|
- Persistent data volumes
|
||||||
@@ -249,12 +249,7 @@ The included `docker-compose.yml` provides a complete setup with:
|
|||||||
- Real-time mobile app integration (OwnTracks, GPSLogger)
|
- Real-time mobile app integration (OwnTracks, GPSLogger)
|
||||||
- REST API endpoints
|
- REST API endpoints
|
||||||
|
|
||||||
2. **Queue Processing**: Data is queued in RabbitMQ for asynchronous processing:
|
2. **Analysis & Detection**: The application directly processes the data to:
|
||||||
- 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:
|
|
||||||
- Detect significant places where you spend time
|
- Detect significant places where you spend time
|
||||||
- Identify trips between locations
|
- Identify trips between locations
|
||||||
- Determine transport modes (walking, cycling, driving)
|
- 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
|
- Temporal indexing for timeline operations
|
||||||
- User data isolation and security
|
- 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
|
- Interactive timeline with visits and trips
|
||||||
- Map visualization with location markers
|
- Map visualization with location markers
|
||||||
- Photo integration showing images taken at locations
|
- 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:**
|
- **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 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.
|
- 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:**
|
**Recommended Backup Strategy:**
|
||||||
- Use standard PostgreSQL backup tools (such as `pg_dump` or physical volume snapshots) to back up your database.
|
- 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.
|
- Back up the entire storage directory/volume used by Reitti for file storage.
|
||||||
- Ensure backups are performed regularly and stored securely.
|
- 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:**
|
**Restore:**
|
||||||
- In case of disaster recovery, restore both the PostGIS database and the storage path to recover all user data and history.
|
- In case of disaster recovery, restore both the PostGIS database and the storage path to recover all user data and history.
|
||||||
|
|||||||
@@ -521,7 +521,7 @@ function updatePointsList() {
|
|||||||
html += `
|
html += `
|
||||||
<div class="point-item" id="${pointId}" onclick="selectPoint(${trackIndex}, ${pointIndex})" style="border-left-color: ${speedColor}">
|
<div class="point-item" id="${pointId}" onclick="selectPoint(${trackIndex}, ${pointIndex})" style="border-left-color: ${speedColor}">
|
||||||
<div class="point-content">
|
<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-time">${formatCompactTimestamp(point.timestamp)}</div>
|
||||||
<div class="point-speed" style="color: ${speedColor}">${speedText}</div>
|
<div class="point-speed" style="color: ${speedColor}">${speedText}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -596,7 +596,20 @@ function formatTimestamp(timestamp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatCompactTimestamp(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() {
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -542,16 +542,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.point-item {
|
.point-item {
|
||||||
padding: 8px 20px;
|
padding: 6px 20px;
|
||||||
border-bottom: 1px solid rgba(237, 242, 247, 0.8);
|
border-bottom: 1px solid rgba(237, 242, 247, 0.8);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-height: 32px;
|
min-height: 28px;
|
||||||
border-left: 4px solid #e2e8f0;
|
border-left: 4px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,7 +568,7 @@
|
|||||||
.point-content {
|
.point-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-family: 'Monaco', 'Menlo', monospace;
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
}
|
}
|
||||||
@@ -576,15 +576,17 @@
|
|||||||
.point-coords {
|
.point-coords {
|
||||||
color: #4a5568;
|
color: #4a5568;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-time {
|
.point-time {
|
||||||
color: #718096;
|
color: #718096;
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.point-speed {
|
.point-speed {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
min-width: 60px;
|
min-width: 45px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,6 +905,21 @@
|
|||||||
<button onclick="newTrack()" class="control-button">➕ New Track</button>
|
<button onclick="newTrack()" class="control-button">➕ New Track</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -498,8 +498,8 @@ public class ReittiIntegrationService {
|
|||||||
(String) placeInfo.get("address"),
|
(String) placeInfo.get("address"),
|
||||||
(String) placeInfo.get("city"),
|
(String) placeInfo.get("city"),
|
||||||
(String) placeInfo.get("countryCode"),
|
(String) placeInfo.get("countryCode"),
|
||||||
getDoubleValue(placeInfo, "latitudeCentroid"),
|
getDoubleValue(placeInfo, "lat"),
|
||||||
getDoubleValue(placeInfo, "longitudeCentroid"),
|
getDoubleValue(placeInfo, "lng"),
|
||||||
(String) placeInfo.get("type")
|
(String) placeInfo.get("type")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -515,7 +515,7 @@ public class ReittiIntegrationService {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Parse summary data
|
// 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");
|
int visitCount = getIntValue(placeData, "visitCount");
|
||||||
String color = "#3388ff"; // Default color, could be extracted from response if available
|
String color = "#3388ff"; // Default color, could be extracted from response if available
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user