mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-10 09:57:57 -05:00
132 custom map tile servers (#134)
This commit is contained in:
BIN
.github/screenshots/multiple-users.png
vendored
Normal file
BIN
.github/screenshots/multiple-users.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
10
README.md
10
README.md
@@ -10,6 +10,10 @@ Reitti is a comprehensive personal location tracking and analysis application th
|
||||
|
||||

|
||||
|
||||
### Multiple Users View
|
||||
|
||||

|
||||
|
||||
### Statistics View
|
||||
|
||||

|
||||
@@ -20,9 +24,10 @@ Reitti is a comprehensive personal location tracking and analysis application th
|
||||
- **Significant Places**: Recognize and categorize frequently visited locations with custom naming
|
||||
- **Timeline View**: Interactive daily timeline showing visits and trips with duration and distance information
|
||||
- **Raw Location Tracking**: Visualize your complete movement path with detailed GPS tracks
|
||||
- **Multi-User-View**: Visualize all your family and friends on a single map
|
||||
|
||||
### Data Import & Integration
|
||||
- **Multiple Import Formats**: Support for GPX files, Google Takeout JSON, 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
|
||||
- **Batch Processing**: Efficient handling of large location datasets with queue-based processing
|
||||
- **API Integration**: RESTful API for programmatic data access and ingestion
|
||||
@@ -45,6 +50,7 @@ Reitti is a comprehensive personal location tracking and analysis application th
|
||||
|
||||
### Customization & Localization
|
||||
- **Multi-language Support**: Available in English, Finnish, German, and French
|
||||
- **Unit System**: Display distances in the Imperial or Metric system
|
||||
- **Queue Monitoring**: Real-time job status and processing queue visibility
|
||||
|
||||
### Privacy & Self-hosting
|
||||
@@ -189,6 +195,8 @@ The included `docker-compose.yml` provides a complete setup with:
|
||||
| `PHOTON_BASE_URL` | Base URL for Photon geocoding service | |
|
||||
| `PROCESSING_WAIT_TIME` | How many seconds to wait after the last data input before starting to process all unprocessed data. (⚠️ This needs to be lower than your integrated app reports data in Reitti) | 15 |
|
||||
| `DANGEROUS_LIFE` | Enables data management features that can reset/delete all database data (⚠️ USE WITH CAUTION) | false |
|
||||
| `CUSTOM_TILES_SERVICE` | Custom tile service URL template (e.g., `https://tiles.example.com/{z}/{x}/{y}.png`) | |
|
||||
| `CUSTOM_TILES_ATTRIBUTION` | Custom attribution text for the tile service | |
|
||||
| `SERVER_PORT` | Application server port | 8080 |
|
||||
| `APP_UID` | User ID to run the application as | 1000 |
|
||||
| `APP_GID` | Group ID to run the application as | 1000 |
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import com.dedicatedcode.reitti.model.User;
|
||||
import com.dedicatedcode.reitti.repository.UserJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.UserSettingsJdbcService;
|
||||
import com.dedicatedcode.reitti.service.TilesCustomizationProvider;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
@@ -18,10 +19,12 @@ public class GlobalControllerAdvice {
|
||||
|
||||
private final UserJdbcService userJdbcService;
|
||||
private final UserSettingsJdbcService userSettingsJdbcService;
|
||||
|
||||
public GlobalControllerAdvice(UserJdbcService userJdbcService, UserSettingsJdbcService userSettingsJdbcService) {
|
||||
private final TilesCustomizationProvider tilesCustomizationProvider;
|
||||
|
||||
public GlobalControllerAdvice(UserJdbcService userJdbcService, UserSettingsJdbcService userSettingsJdbcService, TilesCustomizationProvider tilesCustomizationProvider) {
|
||||
this.userJdbcService = userJdbcService;
|
||||
this.userSettingsJdbcService = userSettingsJdbcService;
|
||||
this.tilesCustomizationProvider = tilesCustomizationProvider;
|
||||
}
|
||||
|
||||
@ModelAttribute("userSettings")
|
||||
@@ -31,7 +34,7 @@ public class GlobalControllerAdvice {
|
||||
if (authentication == null || !authentication.isAuthenticated() ||
|
||||
"anonymousUser".equals(authentication.getPrincipal())) {
|
||||
// Return default settings for anonymous users
|
||||
return new UserSettingsDTO(false, "en", List.of(), UnitSystem.METRIC);
|
||||
return new UserSettingsDTO(false, "en", List.of(), UnitSystem.METRIC, tilesCustomizationProvider.getTilesConfiguration());
|
||||
}
|
||||
|
||||
String username = authentication.getName();
|
||||
@@ -40,10 +43,10 @@ public class GlobalControllerAdvice {
|
||||
if (userOptional.isPresent()) {
|
||||
User user = userOptional.get();
|
||||
com.dedicatedcode.reitti.model.UserSettings dbSettings = userSettingsJdbcService.getOrCreateDefaultSettings(user.getId());
|
||||
return new UserSettingsDTO(dbSettings.isPreferColoredMap(), dbSettings.getSelectedLanguage(), dbSettings.getConnectedUserAccounts(), dbSettings.getUnitSystem());
|
||||
return new UserSettingsDTO(dbSettings.isPreferColoredMap(), dbSettings.getSelectedLanguage(), dbSettings.getConnectedUserAccounts(), dbSettings.getUnitSystem(), tilesCustomizationProvider.getTilesConfiguration());
|
||||
}
|
||||
|
||||
// Fallback for authenticated users not found in database
|
||||
return new UserSettingsDTO(false, "en", List.of(), UnitSystem.METRIC);
|
||||
return new UserSettingsDTO(false, "en", List.of(), UnitSystem.METRIC, tilesCustomizationProvider.getTilesConfiguration());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import java.util.List;
|
||||
|
||||
public record UserSettingsDTO(boolean preferColoredMap, String selectedLanguage,
|
||||
List<ConnectedUserAccount> connectedUserAccounts, UnitSystem unitSystem) {
|
||||
List<ConnectedUserAccount> connectedUserAccounts, UnitSystem unitSystem, TilesCustomizationDTO tiles) {
|
||||
|
||||
public record TilesCustomizationDTO(String service, String attribution){}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.dedicatedcode.reitti.service;
|
||||
|
||||
import com.dedicatedcode.reitti.dto.UserSettingsDTO;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Service
|
||||
public class TilesCustomizationProvider {
|
||||
private final String defaultService;
|
||||
private final String defaultAttribution;
|
||||
|
||||
private final String customService;
|
||||
private final String customAttribution;
|
||||
|
||||
public TilesCustomizationProvider(
|
||||
@Value("${reitti.ui.tiles.default.service}") String defaultService,
|
||||
@Value("${reitti.ui.tiles.default.attribution}") String defaultAttribution,
|
||||
@Value("${reitti.ui.tiles.custom.service:}") String customService,
|
||||
@Value("${reitti.ui.tiles.custom.attribution:}") String customAttribution) {
|
||||
this.defaultService = defaultService;
|
||||
this.defaultAttribution = defaultAttribution;
|
||||
this.customService = customService;
|
||||
this.customAttribution = customAttribution;
|
||||
}
|
||||
|
||||
public UserSettingsDTO.TilesCustomizationDTO getTilesConfiguration() {
|
||||
return new UserSettingsDTO.TilesCustomizationDTO(
|
||||
StringUtils.hasText(customService) ? customService : defaultService,
|
||||
StringUtils.hasText(customAttribution) ? customAttribution : defaultAttribution
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,4 +24,7 @@ reitti.geocoding.photon.base-url=${PHOTON_BASE_URL:}
|
||||
|
||||
reitti.process-data.schedule=${REITTI_PROCESS_DATA_CRON:0 */10 * * * *}
|
||||
|
||||
reitti.ui.tiles.custom.service=${CUSTOM_TILES_SERVICE:}
|
||||
reitti.ui.tiles.custom.attribution=${CUSTOM_TILES_ATTRIBUTION:}
|
||||
|
||||
logging.level.root = INFO
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
# Server configuration
|
||||
server.port=8080
|
||||
|
||||
server.tomcat.max-part-count=100
|
||||
|
||||
# Logging configuration
|
||||
|
||||
logging.level.root = INFO
|
||||
@@ -42,6 +40,12 @@ spring.rabbitmq.listener.simple.prefetch=10
|
||||
spring.cache.cache-names=processed-visits,significant-places
|
||||
spring.cache.redis.time-to-live=1d
|
||||
|
||||
|
||||
# Upload configuration
|
||||
spring.servlet.multipart.max-file-size=5GB
|
||||
spring.servlet.multipart.max-request-size=5GB
|
||||
server.tomcat.max-part-count=100
|
||||
|
||||
# Application specific settings
|
||||
reitti.import.batch-size=1000
|
||||
# How many seconds should we wait after the last data input before starting to process all unprocessed data?
|
||||
@@ -71,8 +75,13 @@ reitti.geocoding.max-errors=10
|
||||
# Geocoding fixed service configuration
|
||||
reitti.geocoding.photon.base-url=
|
||||
|
||||
# Tiles Configuration
|
||||
reitti.ui.tiles.default.service=https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png
|
||||
reitti.ui.tiles.default.attribution=© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>
|
||||
|
||||
# You can set custom tiles service and attribution by uncommenting the next two keys and setting them to appropriate values
|
||||
#reitti.ui.tiles.custom.service=
|
||||
#reitti.ui.tiles.custom.attribution=
|
||||
|
||||
# Data management configuration
|
||||
reitti.data-management.enabled=false
|
||||
|
||||
spring.servlet.multipart.max-file-size=5GB
|
||||
spring.servlet.multipart.max-request-size=5GB
|
||||
|
||||
@@ -143,18 +143,21 @@
|
||||
// Initialize the map
|
||||
const map = L.map('map', {zoomControl: false, attributionControl: false}).setView([60.1699, 24.9384], 12); // Helsinki coordinates as default
|
||||
|
||||
const tilesUrl = window.userSettings.tiles.service;
|
||||
const tilesAttribution = window.userSettings.tiles.attribution;
|
||||
|
||||
if (window.userSettings.preferColoredMap) {
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
||||
L.tileLayer(tilesUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>'
|
||||
attribution: tilesAttribution
|
||||
}).addTo(map);
|
||||
} else {
|
||||
L.tileLayer.grayscale('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
||||
L.tileLayer.grayscale(tilesUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>'
|
||||
attribution: tilesAttribution
|
||||
}).addTo(map);
|
||||
}
|
||||
L.control.attribution({position: 'topright'}).addAttribution('© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>')
|
||||
L.control.attribution({position: 'topright'}).addAttribution(tilesAttribution)
|
||||
.addTo(map)
|
||||
|
||||
// Initialize photo client
|
||||
|
||||
@@ -122,7 +122,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script th:inline="javascript">
|
||||
window.userSettings = /*[[${userSettings}]]*/ {}
|
||||
|
||||
function removeCurrentAvatar() {
|
||||
// Hide the current avatar preview
|
||||
@@ -160,10 +161,12 @@
|
||||
dragging: false,
|
||||
scrollWheelZoom: false
|
||||
})
|
||||
const tilesUrl = window.userSettings.tiles.service;
|
||||
const tilesAttribution = window.userSettings.tiles.attribution;
|
||||
|
||||
L.tileLayer.grayscale('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
||||
L.tileLayer.grayscale(tilesUrl, {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, Tiles style by <a href="https://www.hotosm.org/" target="_blank">Humanitarian OpenStreetMap Team</a> hosted by <a href="https://openstreetmap.fr/" target="_blank">OpenStreetMap France</a>'
|
||||
attribution: tilesAttribution
|
||||
}).addTo(placeMap);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.dedicatedcode.reitti.service;
|
||||
|
||||
import com.dedicatedcode.reitti.dto.UserSettingsDTO;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class TilesCustomizationProviderTest {
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithCustomServiceAndAttribution_ShouldReturnCustomValues() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = "https://custom.tiles.com/{z}/{x}/{y}.png";
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(customService);
|
||||
assertThat(result.attribution()).isEqualTo(customAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithEmptyCustomService_ShouldReturnDefaultService() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = "";
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(defaultService);
|
||||
assertThat(result.attribution()).isEqualTo(customAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithEmptyCustomAttribution_ShouldReturnDefaultAttribution() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = "https://custom.tiles.com/{z}/{x}/{y}.png";
|
||||
String customAttribution = "";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(customService);
|
||||
assertThat(result.attribution()).isEqualTo(defaultAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithNullCustomValues_ShouldReturnDefaultValues() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = null;
|
||||
String customAttribution = null;
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(defaultService);
|
||||
assertThat(result.attribution()).isEqualTo(defaultAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithWhitespaceOnlyCustomValues_ShouldReturnDefaultValues() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = " ";
|
||||
String customAttribution = "\t\n";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(defaultService);
|
||||
assertThat(result.attribution()).isEqualTo(defaultAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithBothEmptyCustomValues_ShouldReturnDefaultValues() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = "";
|
||||
String customAttribution = "";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(defaultService);
|
||||
assertThat(result.attribution()).isEqualTo(defaultAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithValidCustomServiceOnly_ShouldReturnCustomServiceAndDefaultAttribution() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = "https://custom.tiles.com/{z}/{x}/{y}.png";
|
||||
String customAttribution = "";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(customService);
|
||||
assertThat(result.attribution()).isEqualTo(defaultAttribution);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTilesConfiguration_WithValidCustomAttributionOnly_ShouldReturnDefaultServiceAndCustomAttribution() {
|
||||
// Given
|
||||
String defaultService = "https://default.tiles.com/{z}/{x}/{y}.png";
|
||||
String defaultAttribution = "Default Attribution";
|
||||
String customService = "";
|
||||
String customAttribution = "Custom Attribution";
|
||||
|
||||
TilesCustomizationProvider provider = new TilesCustomizationProvider(
|
||||
defaultService, defaultAttribution, customService, customAttribution
|
||||
);
|
||||
|
||||
// When
|
||||
UserSettingsDTO.TilesCustomizationDTO result = provider.getTilesConfiguration();
|
||||
|
||||
// Then
|
||||
assertThat(result.service()).isEqualTo(defaultService);
|
||||
assertThat(result.attribution()).isEqualTo(customAttribution);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user