Skip to content
Merged
Binary file modified .github/screenshots/livemode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/main.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified .github/screenshots/multiple-users.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down
76 changes: 74 additions & 2 deletions docs/tools/gpx-generator/gpx-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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);
}

27 changes: 22 additions & 5 deletions docs/tools/gpx-generator/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -568,23 +568,25 @@
.point-content {
display: flex;
align-items: center;
gap: 12px;
gap: 8px;
flex: 1;
font-family: 'Monaco', 'Menlo', monospace;
}

.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;
}

Expand Down Expand Up @@ -903,6 +905,21 @@ <h1>GPX Test Data Generator</h1>
<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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,8 +498,8 @@ private ProcessedVisitResponse.PlaceVisitSummary parsePlaceVisitSummary(Map<Stri
(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")
);

Expand All @@ -515,7 +515,7 @@ private ProcessedVisitResponse.PlaceVisitSummary parsePlaceVisitSummary(Map<Stri
.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

Expand Down