mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-09 17:37:57 -05:00
192 feature request all dates and times are utc UI user settings to show in local time in area (#225)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
FROM eclipse-temurin:24-jre-alpine
|
||||
FROM eclipse-temurin:24.0.2_12-jre-alpine
|
||||
|
||||
LABEL maintainer="dedicatedcode"
|
||||
LABEL org.opencontainers.image.source="https://github.com/dedicatedcode/reitti"
|
||||
|
||||
5
pom.xml
5
pom.xml
@@ -90,6 +90,11 @@
|
||||
<groupId>org.thymeleaf.extras</groupId>
|
||||
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.iakovlev</groupId>
|
||||
<artifactId>timeshape</artifactId>
|
||||
<version>2025b.26</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
|
||||
@@ -10,19 +10,22 @@ import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Controller
|
||||
public class CustomErrorController implements ErrorController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomErrorController.class);
|
||||
private static final List<String> IGNORED_PATHS = List.of("/favicon.ico", "/js/", "/img/");
|
||||
|
||||
@RequestMapping("/error")
|
||||
public String handleError(HttpServletRequest request, Model model) {
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
Object errorMessage = request.getAttribute(RequestDispatcher.ERROR_MESSAGE);
|
||||
Object exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
|
||||
Object requestUri = request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
|
||||
String requestUri = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
|
||||
|
||||
if (requestUri.toString().equals("/favicon.ico")) {
|
||||
if (IGNORED_PATHS.stream().anyMatch(requestUri::startsWith)) {
|
||||
return null;
|
||||
}
|
||||
if (status != null) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -322,7 +323,7 @@ public class SettingsController {
|
||||
significantPlace.getLatitudeCentroid(),
|
||||
significantPlace.getLongitudeCentroid()
|
||||
);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.SIGNIFICANT_PLACE_QUEUE, event);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.SIGNIFICANT_PLACE_ROUTING_KEY, event);
|
||||
|
||||
model.addAttribute("successMessage", getMessage("places.geocode.success"));
|
||||
} catch (Exception e) {
|
||||
@@ -819,7 +820,7 @@ public class SettingsController {
|
||||
place.getLatitudeCentroid(),
|
||||
place.getLongitudeCentroid()
|
||||
);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.SIGNIFICANT_PLACE_QUEUE, event);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.SIGNIFICANT_PLACE_ROUTING_KEY, event);
|
||||
}
|
||||
|
||||
model.addAttribute("successMessage", getMessage("geocoding.run.success", nonGeocodedPlaces.size()));
|
||||
@@ -859,7 +860,7 @@ public class SettingsController {
|
||||
place.getLatitudeCentroid(),
|
||||
place.getLongitudeCentroid()
|
||||
);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.SIGNIFICANT_PLACE_QUEUE, event);
|
||||
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, RabbitMQConfig.SIGNIFICANT_PLACE_ROUTING_KEY, event);
|
||||
}
|
||||
|
||||
model.addAttribute("successMessage", getMessage("geocoding.clear.success", allPlaces.size()));
|
||||
@@ -903,8 +904,7 @@ public class SettingsController {
|
||||
model.addAttribute("externallyManaged", user.getExternalId() != null && oidcEnabled);
|
||||
model.addAttribute("externalProfile", user.getProfileUrl());
|
||||
model.addAttribute("localLoginDisabled", this.localLoginDisabled);
|
||||
UserSettings userSettings = userSettingsJdbcService.findByUserId(user.getId())
|
||||
.orElse(UserSettings.defaultSettings(user.getId()));
|
||||
UserSettings userSettings = userSettingsJdbcService.findByUserId(user.getId()).orElse(UserSettings.defaultSettings(user.getId()));
|
||||
model.addAttribute("selectedLanguage", userSettings.getSelectedLanguage());
|
||||
model.addAttribute("selectedUnitSystem", userSettings.getUnitSystem().name());
|
||||
model.addAttribute("preferColoredMap", userSettings.isPreferColoredMap());
|
||||
@@ -912,6 +912,10 @@ public class SettingsController {
|
||||
model.addAttribute("homeLongitude", userSettings.getHomeLongitude());
|
||||
model.addAttribute("unitSystems", UnitSystem.values());
|
||||
model.addAttribute("isAdmin", false);
|
||||
model.addAttribute("timeZoneOverride", userSettings.getTimeZoneOverride());
|
||||
model.addAttribute("timeDisplayMode", userSettings.getTimeDisplayMode().name());
|
||||
model.addAttribute("availableTimezones", ZoneId.getAvailableZoneIds().stream().sorted());
|
||||
model.addAttribute("availableTimeDisplayModes", TimeDisplayMode.values());
|
||||
|
||||
// Check if user has avatar
|
||||
boolean hasAvatar = this.avatarService.getInfo(user.getId()).isPresent();
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.SignificantPlaceJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.UserJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.UserSettingsJdbcService;
|
||||
import com.dedicatedcode.reitti.service.AvatarService;
|
||||
import com.dedicatedcode.reitti.service.TimelineService;
|
||||
import com.dedicatedcode.reitti.service.integration.ReittiIntegrationService;
|
||||
@@ -37,18 +38,21 @@ public class TimelineController {
|
||||
private final AvatarService avatarService;
|
||||
private final ReittiIntegrationService reittiIntegrationService;
|
||||
private final TimelineService timelineService;
|
||||
private final UserSettingsJdbcService userSettingsJdbcService;
|
||||
|
||||
@Autowired
|
||||
public TimelineController(SignificantPlaceJdbcService placeService,
|
||||
UserJdbcService userJdbcService,
|
||||
AvatarService avatarService,
|
||||
ReittiIntegrationService reittiIntegrationService,
|
||||
TimelineService timelineService) {
|
||||
TimelineService timelineService,
|
||||
UserSettingsJdbcService userSettingsJdbcService) {
|
||||
this.placeService = placeService;
|
||||
this.userJdbcService = userJdbcService;
|
||||
this.avatarService = avatarService;
|
||||
this.reittiIntegrationService = reittiIntegrationService;
|
||||
this.timelineService = timelineService;
|
||||
this.userSettingsJdbcService = userSettingsJdbcService;
|
||||
}
|
||||
|
||||
@GetMapping("/content")
|
||||
@@ -159,6 +163,7 @@ public class TimelineController {
|
||||
model.addAttribute("selectedPlaceId", selectedPlaceId);
|
||||
model.addAttribute("data", selectedDate);
|
||||
model.addAttribute("timezone", timezone);
|
||||
model.addAttribute("timeDisplayMode", userSettingsJdbcService.getOrCreateDefaultSettings(user.getId()).getTimeDisplayMode());
|
||||
return "fragments/timeline :: timeline-content";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.dedicatedcode.reitti.controller;
|
||||
|
||||
import com.dedicatedcode.reitti.model.Role;
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
@@ -24,6 +25,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -105,20 +107,14 @@ public class UserSettingsController {
|
||||
model.addAttribute("preferColoredMap", userSettings.isPreferColoredMap());
|
||||
model.addAttribute("homeLatitude", userSettings.getHomeLatitude());
|
||||
model.addAttribute("homeLongitude", userSettings.getHomeLongitude());
|
||||
|
||||
// Add available unit systems to model
|
||||
model.addAttribute("unitSystems", UnitSystem.values());
|
||||
|
||||
// Check if user has avatar
|
||||
boolean hasAvatar = this.avatarService.getInfo(currentUser.getId()).isPresent();
|
||||
model.addAttribute("hasAvatar", hasAvatar);
|
||||
|
||||
// Add default avatars to model
|
||||
model.addAttribute("hasAvatar", this.avatarService.getInfo(currentUser.getId()).isPresent());
|
||||
model.addAttribute("defaultAvatars", DEFAULT_AVATARS);
|
||||
|
||||
// Add admin status to model
|
||||
model.addAttribute("isAdmin", false);
|
||||
|
||||
model.addAttribute("timeZoneOverride", userSettings.getTimeZoneOverride());
|
||||
model.addAttribute("timeDisplayMode", userSettings.getTimeDisplayMode().name());
|
||||
model.addAttribute("availableTimezones", ZoneId.getAvailableZoneIds());
|
||||
model.addAttribute("availableTimeDisplayModes", TimeDisplayMode.values());
|
||||
return "fragments/user-management :: user-form-page";
|
||||
}
|
||||
|
||||
@@ -173,6 +169,8 @@ public class UserSettingsController {
|
||||
@RequestParam(defaultValue = "false") boolean preferColoredMap,
|
||||
@RequestParam(required = false) Double homeLatitude,
|
||||
@RequestParam(required = false) Double homeLongitude,
|
||||
@RequestParam(name = "timezone_override", required = false) String timezoneOverride,
|
||||
@RequestParam(name = "time_display_mode", defaultValue = "DEFAULT") TimeDisplayMode timeDisplayMode,
|
||||
@RequestParam(required = false) MultipartFile avatar,
|
||||
@RequestParam(required = false) String defaultAvatar,
|
||||
Authentication authentication,
|
||||
@@ -194,7 +192,16 @@ public class UserSettingsController {
|
||||
.withRole(role));
|
||||
|
||||
UnitSystem unitSystem = UnitSystem.valueOf(unit_system);
|
||||
UserSettings userSettings = new UserSettings(createdUser.getId(), preferColoredMap, preferred_language, unitSystem, homeLatitude, homeLongitude, null);
|
||||
UserSettings userSettings = new UserSettings(createdUser.getId(),
|
||||
preferColoredMap,
|
||||
preferred_language,
|
||||
unitSystem,
|
||||
homeLatitude,
|
||||
homeLongitude,
|
||||
StringUtils.hasText(timezoneOverride) ? ZoneId.of(timezoneOverride) : null,
|
||||
timeDisplayMode,
|
||||
null,
|
||||
null);
|
||||
userSettingsJdbcService.save(userSettings);
|
||||
|
||||
// Handle avatar - prioritize custom upload over default
|
||||
@@ -233,6 +240,8 @@ public class UserSettingsController {
|
||||
@RequestParam(required = false) Double homeLatitude,
|
||||
@RequestParam(required = false) Double homeLongitude,
|
||||
@RequestParam(required = false) MultipartFile avatar,
|
||||
@RequestParam(name = "timezone_override", required = false) String timezoneOverride,
|
||||
@RequestParam(name = "time_display_mode", defaultValue = "DEFAULT") TimeDisplayMode timeDisplayMode,
|
||||
@RequestParam(required = false) String defaultAvatar,
|
||||
@RequestParam(required = false) String removeAvatar,
|
||||
Authentication authentication,
|
||||
@@ -275,7 +284,7 @@ public class UserSettingsController {
|
||||
.orElse(UserSettings.defaultSettings(userId));
|
||||
|
||||
UnitSystem unitSystem = UnitSystem.valueOf(unit_system);
|
||||
UserSettings updatedSettings = new UserSettings(userId, preferColoredMap, preferred_language, unitSystem, homeLatitude, homeLongitude, existingSettings.getLatestData(), existingSettings.getVersion());
|
||||
UserSettings updatedSettings = new UserSettings(userId, preferColoredMap, preferred_language, unitSystem, homeLatitude, homeLongitude, StringUtils.hasText(timezoneOverride) ? ZoneId.of(timezoneOverride) : null, timeDisplayMode, existingSettings.getLatestData(), existingSettings.getVersion());
|
||||
userSettingsJdbcService.save(updatedSettings);
|
||||
|
||||
// Handle avatar operations
|
||||
@@ -355,6 +364,8 @@ public class UserSettingsController {
|
||||
model.addAttribute("preferColoredMap", userSettings.isPreferColoredMap());
|
||||
model.addAttribute("homeLatitude", userSettings.getHomeLatitude());
|
||||
model.addAttribute("homeLongitude", userSettings.getHomeLongitude());
|
||||
model.addAttribute("timeZoneOverride", userSettings.getTimeZoneOverride());
|
||||
model.addAttribute("timeDisplayMode", userSettings.getTimeDisplayMode().name());
|
||||
} else {
|
||||
// Default values for new users
|
||||
model.addAttribute("selectedLanguage", "en");
|
||||
@@ -367,10 +378,10 @@ public class UserSettingsController {
|
||||
model.addAttribute("externalProfile", null);
|
||||
model.addAttribute("localLoginDisabled", localLoginDisabled);
|
||||
}
|
||||
|
||||
// Add available unit systems to model
|
||||
|
||||
model.addAttribute("unitSystems", UnitSystem.values());
|
||||
|
||||
model.addAttribute("availableTimezones", ZoneId.getAvailableZoneIds().stream().sorted());
|
||||
model.addAttribute("availableTimeDisplayModes", TimeDisplayMode.values());
|
||||
// Check if user has avatar
|
||||
if (userId != null) {
|
||||
boolean hasAvatar = this.avatarService.getInfo(userId).isPresent();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.dedicatedcode.reitti.controller;
|
||||
|
||||
import com.dedicatedcode.reitti.dto.UserSettingsDTO;
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
@@ -38,10 +39,9 @@ public class UserSettingsControllerAdvice {
|
||||
public UserSettingsDTO getCurrentUserSettings() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !authentication.isAuthenticated() ||
|
||||
"anonymousUser".equals(authentication.getPrincipal())) {
|
||||
if (authentication == null || !authentication.isAuthenticated() || "anonymousUser".equals(authentication.getPrincipal())) {
|
||||
// Return default settings for anonymous users
|
||||
return new UserSettingsDTO(false, "en", Instant.now(), UnitSystem.METRIC, DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE, tilesCustomizationProvider.getTilesConfiguration(), UserSettingsDTO.UIMode.FULL);
|
||||
return new UserSettingsDTO(false, "en", Instant.now(), UnitSystem.METRIC, DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE, tilesCustomizationProvider.getTilesConfiguration(), UserSettingsDTO.UIMode.FULL, TimeDisplayMode.DEFAULT, null);
|
||||
}
|
||||
|
||||
String username = authentication.getName();
|
||||
@@ -57,11 +57,12 @@ public class UserSettingsControllerAdvice {
|
||||
dbSettings.getHomeLatitude(),
|
||||
dbSettings.getHomeLongitude(),
|
||||
tilesCustomizationProvider.getTilesConfiguration(),
|
||||
uiMode);
|
||||
uiMode,
|
||||
dbSettings.getTimeDisplayMode(),
|
||||
dbSettings.getTimeZoneOverride());
|
||||
}
|
||||
|
||||
// Fallback for authenticated users not found in database
|
||||
return new UserSettingsDTO(false, "en", Instant.now(), UnitSystem.METRIC, DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE, tilesCustomizationProvider.getTilesConfiguration(), uiMode);
|
||||
return new UserSettingsDTO(false, "en", Instant.now(), UnitSystem.METRIC, DEFAULT_HOME_LATITUDE, DEFAULT_HOME_LONGITUDE, tilesCustomizationProvider.getTilesConfiguration(), uiMode, TimeDisplayMode.DEFAULT, null);
|
||||
}
|
||||
|
||||
private UserSettingsDTO.UIMode mapUserToUiMode(Authentication authentication) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.dedicatedcode.reitti.dto;
|
||||
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* Inner class to represent timeline entries for the template
|
||||
@@ -15,8 +16,11 @@ public class TimelineEntry {
|
||||
private SignificantPlace place;
|
||||
private String path;
|
||||
private Instant startTime;
|
||||
private ZoneId startTimezone;
|
||||
private Instant endTime;
|
||||
private ZoneId endTimezone;
|
||||
private String formattedTimeRange;
|
||||
private String formattedLocalTimeRange;
|
||||
private String formattedDuration;
|
||||
private Double distanceMeters;
|
||||
private String formattedDistance;
|
||||
@@ -63,6 +67,22 @@ public class TimelineEntry {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public ZoneId getStartTimezone() {
|
||||
return startTimezone;
|
||||
}
|
||||
|
||||
public void setStartTimezone(ZoneId startTimezone) {
|
||||
this.startTimezone = startTimezone;
|
||||
}
|
||||
|
||||
public ZoneId getEndTimezone() {
|
||||
return endTimezone;
|
||||
}
|
||||
|
||||
public void setEndTimezone(ZoneId endTimezone) {
|
||||
this.endTimezone = endTimezone;
|
||||
}
|
||||
|
||||
public String getFormattedTimeRange() {
|
||||
return formattedTimeRange;
|
||||
}
|
||||
@@ -71,6 +91,14 @@ public class TimelineEntry {
|
||||
this.formattedTimeRange = formattedTimeRange;
|
||||
}
|
||||
|
||||
public String getFormattedLocalTimeRange() {
|
||||
return formattedLocalTimeRange;
|
||||
}
|
||||
|
||||
public void setFormattedLocalTimeRange(String formattedLocalTimeRange) {
|
||||
this.formattedLocalTimeRange = formattedLocalTimeRange;
|
||||
}
|
||||
|
||||
public String getFormattedDuration() {
|
||||
return formattedDuration;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.dedicatedcode.reitti.dto;
|
||||
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
|
||||
public record UserSettingsDTO(
|
||||
boolean preferColoredMap,
|
||||
@@ -12,7 +14,9 @@ public record UserSettingsDTO(
|
||||
Double homeLatitude,
|
||||
Double homeLongitude,
|
||||
TilesCustomizationDTO tiles,
|
||||
UIMode uiMode
|
||||
UIMode uiMode,
|
||||
TimeDisplayMode displayMode,
|
||||
ZoneId timezoneOverride
|
||||
) {
|
||||
|
||||
public enum UIMode {
|
||||
|
||||
@@ -2,26 +2,5 @@ package com.dedicatedcode.reitti.event;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public class SignificantPlaceCreatedEvent implements Serializable {
|
||||
private final Long placeId;
|
||||
private final Double latitude;
|
||||
private final Double longitude;
|
||||
|
||||
public SignificantPlaceCreatedEvent(Long placeId, Double latitude, Double longitude) {
|
||||
this.placeId = placeId;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public Long getPlaceId() {
|
||||
return placeId;
|
||||
}
|
||||
|
||||
public Double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public Double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
public record SignificantPlaceCreatedEvent(Long placeId, Double latitude, Double longitude) implements Serializable {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.dedicatedcode.reitti.model;
|
||||
|
||||
public enum TimeDisplayMode {
|
||||
DEFAULT,
|
||||
GEO_LOCAL
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.dedicatedcode.reitti.model.geo;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Objects;
|
||||
|
||||
public class SignificantPlace implements Serializable {
|
||||
@@ -12,25 +13,12 @@ public class SignificantPlace implements Serializable {
|
||||
private final Double latitudeCentroid;
|
||||
private final Double longitudeCentroid;
|
||||
private final PlaceType type;
|
||||
private final ZoneId timezone;
|
||||
private final boolean geocoded;
|
||||
private final Long version;
|
||||
|
||||
|
||||
public static SignificantPlace create(Double latitude, Double longitude) {
|
||||
return new SignificantPlace(null, null, latitude, longitude, PlaceType.OTHER, null);
|
||||
}
|
||||
|
||||
public SignificantPlace() {
|
||||
this(null, null, null, null, null, null, null, false, null);
|
||||
}
|
||||
|
||||
private SignificantPlace(String name,
|
||||
String address,
|
||||
Double latitudeCentroid,
|
||||
Double longitudeCentroid,
|
||||
PlaceType type,
|
||||
String countryCode) {
|
||||
this(null, name, address, countryCode, latitudeCentroid, longitudeCentroid, type, false, 1L);
|
||||
return new SignificantPlace(null, null, null, null, latitude, longitude, PlaceType.OTHER, null, false, 1L);
|
||||
}
|
||||
|
||||
public SignificantPlace(Long id,
|
||||
@@ -39,7 +27,7 @@ public class SignificantPlace implements Serializable {
|
||||
String countryCode,
|
||||
Double latitudeCentroid,
|
||||
Double longitudeCentroid,
|
||||
PlaceType type,
|
||||
PlaceType type, ZoneId timezone,
|
||||
boolean geocoded,
|
||||
Long version) {
|
||||
this.id = id;
|
||||
@@ -49,6 +37,7 @@ public class SignificantPlace implements Serializable {
|
||||
this.latitudeCentroid = latitudeCentroid;
|
||||
this.longitudeCentroid = longitudeCentroid;
|
||||
this.type = type;
|
||||
this.timezone = timezone;
|
||||
this.geocoded = geocoded;
|
||||
this.version = version;
|
||||
}
|
||||
@@ -82,6 +71,10 @@ public class SignificantPlace implements Serializable {
|
||||
return type;
|
||||
}
|
||||
|
||||
public ZoneId getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
public boolean isGeocoded() {
|
||||
return geocoded;
|
||||
}
|
||||
@@ -92,27 +85,31 @@ public class SignificantPlace implements Serializable {
|
||||
|
||||
// Wither methods
|
||||
public SignificantPlace withGeocoded(boolean geocoded) {
|
||||
return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, geocoded, this.version);
|
||||
return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, timezone, geocoded, this.version);
|
||||
}
|
||||
|
||||
public SignificantPlace withName(String name) {
|
||||
return new SignificantPlace(this.id, name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version);
|
||||
return new SignificantPlace(this.id, name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, timezone, this.geocoded, this.version);
|
||||
}
|
||||
|
||||
public SignificantPlace withAddress(String address) {
|
||||
return new SignificantPlace(this.id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version);
|
||||
return new SignificantPlace(this.id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, timezone, this.geocoded, this.version);
|
||||
}
|
||||
|
||||
public SignificantPlace withCountryCode(String countryCode) {
|
||||
return new SignificantPlace(this.id, this.name, this.address, countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version);
|
||||
return new SignificantPlace(this.id, this.name, this.address, countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, timezone, this.geocoded, this.version);
|
||||
}
|
||||
|
||||
public SignificantPlace withType(PlaceType type) {
|
||||
return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, type, this.geocoded, this.version);
|
||||
return new SignificantPlace(this.id, this.name, this.address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, type, timezone, this.geocoded, this.version);
|
||||
}
|
||||
|
||||
public SignificantPlace withId(Long id) {
|
||||
return new SignificantPlace(id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, this.geocoded, this.version);
|
||||
return new SignificantPlace(id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, timezone, this.geocoded, this.version);
|
||||
}
|
||||
|
||||
public SignificantPlace withTimezone(ZoneId timezone) {
|
||||
return new SignificantPlace(this.id, this.name, address, this.countryCode, this.latitudeCentroid, this.longitudeCentroid, this.type, timezone, this.geocoded, this.version);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,6 +132,7 @@ public class SignificantPlace implements Serializable {
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
public enum PlaceType {
|
||||
RESTAURANT("lni-restaurant", "place.type.restaurant"),
|
||||
PARK("lni-trees", "place.type.park"),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.dedicatedcode.reitti.model.security;
|
||||
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Objects;
|
||||
|
||||
public class UserSettings {
|
||||
@@ -13,25 +15,26 @@ public class UserSettings {
|
||||
private final UnitSystem unitSystem;
|
||||
private final Double homeLatitude;
|
||||
private final Double homeLongitude;
|
||||
private final ZoneId timeZoneOverride;
|
||||
private final TimeDisplayMode timeDisplayMode;
|
||||
private final Instant latestData;
|
||||
private final Long version;
|
||||
|
||||
public UserSettings(Long userId, boolean preferColoredMap, String selectedLanguage, UnitSystem unitSystem, Double homeLatitude, Double homeLongitude, Instant latestData, Long version) {
|
||||
public UserSettings(Long userId, boolean preferColoredMap, String selectedLanguage, UnitSystem unitSystem, Double homeLatitude, Double homeLongitude, ZoneId timeZoneOverride, TimeDisplayMode timeDisplayMode, Instant latestData, Long version) {
|
||||
this.userId = userId;
|
||||
this.preferColoredMap = preferColoredMap;
|
||||
this.selectedLanguage = selectedLanguage;
|
||||
this.unitSystem = unitSystem;
|
||||
this.homeLatitude = homeLatitude;
|
||||
this.homeLongitude = homeLongitude;
|
||||
this.timeZoneOverride = timeZoneOverride;
|
||||
this.timeDisplayMode = timeDisplayMode;
|
||||
this.latestData = latestData;
|
||||
this.version = version;
|
||||
}
|
||||
public UserSettings(Long userId, boolean preferColoredMap, String selectedLanguage, UnitSystem unitSystem, Double homeLatitude, Double homeLongitude, Instant latestData) {
|
||||
this(userId, preferColoredMap, selectedLanguage, unitSystem, homeLatitude, homeLongitude, latestData, null);
|
||||
}
|
||||
|
||||
public static UserSettings defaultSettings(Long userId) {
|
||||
return new UserSettings(userId, false, "en", UnitSystem.METRIC, null, null, null, null);
|
||||
return new UserSettings(userId, false, "en", UnitSystem.METRIC, null, null, null, TimeDisplayMode.DEFAULT, null, null);
|
||||
}
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
@@ -65,6 +68,14 @@ public class UserSettings {
|
||||
return latestData;
|
||||
}
|
||||
|
||||
public TimeDisplayMode getTimeDisplayMode() {
|
||||
return timeDisplayMode;
|
||||
}
|
||||
|
||||
public ZoneId getTimeZoneOverride() {
|
||||
return timeZoneOverride;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -25,25 +26,25 @@ public class SignificantPlaceJdbcService {
|
||||
public SignificantPlaceJdbcService(JdbcTemplate jdbcTemplate, PointReaderWriter pointReaderWriter) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
this.pointReaderWriter = pointReaderWriter;
|
||||
this.significantPlaceRowMapper = (rs, _) -> new SignificantPlace(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getString("address"),
|
||||
rs.getString("country_code"),
|
||||
rs.getDouble("latitude_centroid"),
|
||||
rs.getDouble("longitude_centroid"),
|
||||
SignificantPlace.PlaceType.valueOf(rs.getString("type")),
|
||||
rs.getBoolean("geocoded"),
|
||||
rs.getLong("version"));
|
||||
}
|
||||
|
||||
private final RowMapper<SignificantPlace> significantPlaceRowMapper;
|
||||
private final RowMapper<SignificantPlace> significantPlaceRowMapper = (rs, _) -> new SignificantPlace(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getString("address"),
|
||||
rs.getString("country_code"),
|
||||
rs.getDouble("latitude_centroid"),
|
||||
rs.getDouble("longitude_centroid"),
|
||||
SignificantPlace.PlaceType.valueOf(rs.getString("type")),
|
||||
rs.getString("timezone") != null ? ZoneId.of(rs.getString("timezone")) : null,
|
||||
rs.getBoolean("geocoded"),
|
||||
rs.getLong("version"));
|
||||
|
||||
public Page<SignificantPlace> findByUser(User user, PageRequest pageable) {
|
||||
String countSql = "SELECT COUNT(*) FROM significant_places WHERE user_id = ?";
|
||||
Integer total = jdbcTemplate.queryForObject(countSql, Integer.class, user.getId());
|
||||
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.geocoded, sp.version" +
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.timezone, sp.geocoded, sp.version" +
|
||||
" FROM significant_places sp " +
|
||||
"WHERE sp.user_id = ? ORDER BY sp.id " +
|
||||
"LIMIT ? OFFSET ? ";
|
||||
@@ -54,7 +55,7 @@ public class SignificantPlaceJdbcService {
|
||||
}
|
||||
|
||||
public List<SignificantPlace> findNearbyPlaces(Long userId, Point point, double distanceInMeters) {
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.geocoded, sp.version " +
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.timezone, sp.geocoded, sp.version " +
|
||||
"FROM significant_places sp " +
|
||||
"WHERE sp.user_id = ? " +
|
||||
"AND ST_DWithin(sp.geom, ST_GeomFromText(?, '4326'), ?)";
|
||||
@@ -72,12 +73,12 @@ public class SignificantPlaceJdbcService {
|
||||
place.getLongitudeCentroid(),
|
||||
this.pointReaderWriter.write(place.getLongitudeCentroid(), place.getLatitudeCentroid())
|
||||
);
|
||||
return place.withId(id);
|
||||
return findById(id).orElseThrow();
|
||||
}
|
||||
|
||||
@CacheEvict(cacheNames = "significant-places", key = "#place.id")
|
||||
public SignificantPlace update(SignificantPlace place) {
|
||||
String sql = "UPDATE significant_places SET name = ?, address = ?, country_code = ?, type = ?, latitude_centroid = ?, longitude_centroid = ?, geom = ST_GeomFromText(?, '4326'), geocoded = ? WHERE id = ?";
|
||||
String sql = "UPDATE significant_places SET name = ?, address = ?, country_code = ?, type = ?, latitude_centroid = ?, longitude_centroid = ?, geom = ST_GeomFromText(?, '4326'), timezone = ?, geocoded = ? WHERE id = ?";
|
||||
jdbcTemplate.update(sql,
|
||||
place.getName(),
|
||||
place.getAddress(),
|
||||
@@ -86,15 +87,16 @@ public class SignificantPlaceJdbcService {
|
||||
place.getLatitudeCentroid(),
|
||||
place.getLongitudeCentroid(),
|
||||
this.pointReaderWriter.write(place.getLongitudeCentroid(), place.getLatitudeCentroid()),
|
||||
place.getTimezone() != null ? place.getTimezone().getId() : null,
|
||||
place.isGeocoded(),
|
||||
place.getId()
|
||||
);
|
||||
return place;
|
||||
return findById(place.getId()).orElseThrow();
|
||||
}
|
||||
|
||||
@Cacheable("significant-places")
|
||||
public Optional<SignificantPlace> findById(Long id) {
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.geocoded, sp.version " +
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.timezone, sp.geocoded, sp.version " +
|
||||
"FROM significant_places sp " +
|
||||
"WHERE sp.id = ?";
|
||||
List<SignificantPlace> results = jdbcTemplate.query(sql, significantPlaceRowMapper, id);
|
||||
@@ -106,7 +108,7 @@ public class SignificantPlaceJdbcService {
|
||||
}
|
||||
|
||||
public List<SignificantPlace> findNonGeocodedByUser(User user) {
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.geocoded, sp.version " +
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.timezone, sp.geocoded, sp.version " +
|
||||
"FROM significant_places sp " +
|
||||
"WHERE sp.user_id = ? AND sp.geocoded = false " +
|
||||
"ORDER BY sp.id";
|
||||
@@ -114,10 +116,19 @@ public class SignificantPlaceJdbcService {
|
||||
}
|
||||
|
||||
public List<SignificantPlace> findAllByUser(User user) {
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.geocoded, sp.version " +
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.timezone, sp.geocoded, sp.version " +
|
||||
"FROM significant_places sp " +
|
||||
"WHERE sp.user_id = ? " +
|
||||
"ORDER BY sp.id";
|
||||
return jdbcTemplate.query(sql, significantPlaceRowMapper, user.getId());
|
||||
}
|
||||
|
||||
public List<SignificantPlace> findWithMissingTimezone() {
|
||||
String sql = "SELECT sp.id, sp.address, sp.country_code, sp.type, sp.latitude_centroid, sp.longitude_centroid, sp.name, sp.user_id, ST_AsText(sp.geom) as geom, sp.timezone, sp.geocoded, sp.version " +
|
||||
"FROM significant_places sp " +
|
||||
"WHERE sp.timezone IS NULL " +
|
||||
"ORDER BY sp.id";
|
||||
return jdbcTemplate.query(sql, significantPlaceRowMapper);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.dedicatedcode.reitti.repository;
|
||||
|
||||
import com.dedicatedcode.reitti.dto.LocationDataRequest;
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
@@ -11,6 +12,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
@@ -35,9 +37,10 @@ public class UserSettingsJdbcService {
|
||||
UnitSystem.valueOf(rs.getString("unit_system")),
|
||||
rs.getDouble("home_lat"),
|
||||
rs.getDouble("home_lng"),
|
||||
rs.getString("time_zone_override") != null ? ZoneId.of(rs.getString("time_zone_override")) : null,
|
||||
TimeDisplayMode.valueOf(rs.getString("time_display_mode")),
|
||||
newestData != null ? newestData.toInstant() : null,
|
||||
rs.getLong("version")
|
||||
);
|
||||
rs.getLong("version"));
|
||||
};
|
||||
|
||||
public Optional<UserSettings> findByUserId(Long userId) {
|
||||
@@ -56,13 +59,15 @@ public class UserSettingsJdbcService {
|
||||
public UserSettings save(UserSettings userSettings) {
|
||||
if (userSettings.getVersion() == null) {
|
||||
// Insert new settings
|
||||
this.jdbcTemplate.update("INSERT INTO user_settings (user_id, prefer_colored_map, selected_language, unit_system, home_lat, home_lng, latest_data, version) VALUES (?, ?, ?, ?, ?, ?, ?, 1)",
|
||||
this.jdbcTemplate.update("INSERT INTO user_settings (user_id, prefer_colored_map, selected_language, unit_system, home_lat, home_lng, time_zone_override, time_display_mode, latest_data, version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)",
|
||||
userSettings.getUserId(),
|
||||
userSettings.isPreferColoredMap(),
|
||||
userSettings.getSelectedLanguage(),
|
||||
userSettings.getUnitSystem().name(),
|
||||
userSettings.getHomeLatitude(),
|
||||
userSettings.getHomeLongitude(),
|
||||
userSettings.getTimeZoneOverride() != null ? userSettings.getTimeZoneOverride().getId() : null,
|
||||
userSettings.getTimeDisplayMode().name(),
|
||||
userSettings.getLatestData() != null ? Timestamp.from(userSettings.getLatestData()) : null);
|
||||
|
||||
return new UserSettings(userSettings.getUserId(),
|
||||
@@ -71,17 +76,20 @@ public class UserSettingsJdbcService {
|
||||
userSettings.getUnitSystem(),
|
||||
userSettings.getHomeLatitude(),
|
||||
userSettings.getHomeLongitude(),
|
||||
userSettings.getLatestData(),
|
||||
1L);
|
||||
userSettings.getTimeZoneOverride(),
|
||||
userSettings.getTimeDisplayMode(),
|
||||
userSettings.getLatestData(), 1L);
|
||||
} else {
|
||||
// Update existing settings
|
||||
jdbcTemplate.update(
|
||||
"UPDATE user_settings SET prefer_colored_map = ?, selected_language = ?, unit_system = ?, home_lat = ?, home_lng = ?, latest_data = GREATEST(latest_data, ?), version = version + 1 WHERE user_id = ?",
|
||||
"UPDATE user_settings SET prefer_colored_map = ?, selected_language = ?, unit_system = ?, home_lat = ?, home_lng = ?, time_zone_override = ?, time_display_mode = ?, latest_data = GREATEST(latest_data, ?), version = version + 1 WHERE user_id = ?",
|
||||
userSettings.isPreferColoredMap(),
|
||||
userSettings.getSelectedLanguage(),
|
||||
userSettings.getUnitSystem().name(),
|
||||
userSettings.getHomeLatitude(),
|
||||
userSettings.getHomeLongitude(),
|
||||
userSettings.getTimeZoneOverride() != null ? userSettings.getTimeZoneOverride().getId() : null,
|
||||
userSettings.getTimeDisplayMode().name(),
|
||||
userSettings.getLatestData() != null ? Timestamp.from(userSettings.getLatestData()) : null,
|
||||
userSettings.getUserId()
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.dedicatedcode.reitti.service;
|
||||
|
||||
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||
import com.dedicatedcode.reitti.repository.SignificantPlaceJdbcService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import net.iakovlev.timeshape.TimeZoneEngine;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class GeoLocationTimezoneService {
|
||||
private static final Logger log = LoggerFactory.getLogger(GeoLocationTimezoneService.class);
|
||||
private final TimeZoneEngine engine;
|
||||
private final SignificantPlaceJdbcService significantPlaceJdbcService;
|
||||
|
||||
public GeoLocationTimezoneService(SignificantPlaceJdbcService significantPlaceJdbcService) {
|
||||
this.significantPlaceJdbcService = significantPlaceJdbcService;
|
||||
this.engine = TimeZoneEngine.initialize();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
List<SignificantPlace> places = significantPlaceJdbcService.findWithMissingTimezone();
|
||||
log.info("Searching for SignificantPlaces without Timezone data. Found [{}]", places.size());
|
||||
places.forEach(place -> {
|
||||
Optional<ZoneId> zoneId = engine.query(place.getLatitudeCentroid(), place.getLongitudeCentroid());
|
||||
zoneId.ifPresent(id -> {
|
||||
log.debug("Zone ID [{}] found in for [{}]", id, place);
|
||||
this.significantPlaceJdbcService.update(place.withTimezone(id));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Optional<ZoneId> getTimezone(SignificantPlace place) {
|
||||
return this.engine.query(place.getLatitudeCentroid(), place.getLongitudeCentroid());
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@ public class MessageDispatcherService {
|
||||
|
||||
@RabbitListener(queues = RabbitMQConfig.SIGNIFICANT_PLACE_QUEUE, concurrency = "${reitti.events.concurrency}")
|
||||
public void handleSignificantPlaceCreated(SignificantPlaceCreatedEvent event) {
|
||||
logger.debug("Dispatching SignificantPlaceCreatedEvent for place: {}", event.getPlaceId());
|
||||
logger.debug("Dispatching SignificantPlaceCreatedEvent for place: {}", event.placeId());
|
||||
reverseGeocodingListener.handleSignificantPlaceCreated(event);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,16 +52,14 @@ public class TimelineService {
|
||||
public List<TimelineEntry> buildTimelineEntries(User user, ZoneId userTimeZone, LocalDate selectedDate, Instant startOfDay, Instant endOfDay) {
|
||||
|
||||
// Get processed visits and trips for the user and date range
|
||||
List<ProcessedVisit> processedVisits = processedVisitJdbcService.findByUserAndTimeOverlap(
|
||||
user, startOfDay, endOfDay);
|
||||
List<Trip> trips = tripJdbcService.findByUserAndTimeOverlap(
|
||||
user, startOfDay, endOfDay);
|
||||
List<ProcessedVisit> processedVisits = processedVisitJdbcService.findByUserAndTimeOverlap(user, startOfDay, endOfDay);
|
||||
List<Trip> trips = tripJdbcService.findByUserAndTimeOverlap(user, startOfDay, endOfDay);
|
||||
|
||||
// Get user settings for unit system and connected accounts
|
||||
UserSettings userSettings = userSettingsJdbcService.findByUserId(user.getId())
|
||||
.orElse(UserSettings.defaultSettings(user.getId()));
|
||||
try {
|
||||
return buildTimelineEntries(user, processedVisits, trips, userTimeZone, selectedDate, userSettings.getUnitSystem());
|
||||
return buildTimelineEntries(user, processedVisits, trips, userTimeZone, selectedDate, userSettings);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Unable to build timeline entries.", e);
|
||||
return Collections.emptyList();
|
||||
@@ -71,7 +69,7 @@ public class TimelineService {
|
||||
/**
|
||||
* Build timeline entries from processed visits and trips
|
||||
*/
|
||||
private List<TimelineEntry> buildTimelineEntries(User user, List<ProcessedVisit> processedVisits, List<Trip> trips, ZoneId timezone, LocalDate selectedDate, UnitSystem unitSystem) throws JsonProcessingException {
|
||||
private List<TimelineEntry> buildTimelineEntries(User user, List<ProcessedVisit> processedVisits, List<Trip> trips, ZoneId timezone, LocalDate selectedDate, UserSettings userSettings) throws JsonProcessingException {
|
||||
List<TimelineEntry> entries = new ArrayList<>();
|
||||
|
||||
// Add processed visits to timeline
|
||||
@@ -83,8 +81,11 @@ public class TimelineService {
|
||||
entry.setType(TimelineEntry.Type.VISIT);
|
||||
entry.setPlace(place);
|
||||
entry.setStartTime(visit.getStartTime());
|
||||
entry.setStartTimezone(visit.getPlace().getTimezone());
|
||||
entry.setEndTime(visit.getEndTime());
|
||||
entry.setEndTimezone(visit.getPlace().getTimezone());
|
||||
entry.setFormattedTimeRange(formatTimeRange(visit.getStartTime(), visit.getEndTime(), timezone, selectedDate));
|
||||
entry.setFormattedLocalTimeRange(formatTimeRange(visit.getStartTime(), visit.getEndTime(), visit.getPlace().getTimezone(), selectedDate));
|
||||
entry.setFormattedDuration(formatDuration(visit.getStartTime(), visit.getEndTime()));
|
||||
entries.add(entry);
|
||||
}
|
||||
@@ -96,9 +97,12 @@ public class TimelineService {
|
||||
entry.setId("trip-" + trip.getId());
|
||||
entry.setType(TimelineEntry.Type.TRIP);
|
||||
entry.setStartTime(trip.getStartTime());
|
||||
entry.setStartTimezone(trip.getStartVisit().getPlace().getTimezone());
|
||||
entry.setEndTime(trip.getEndTime());
|
||||
entry.setEndTimezone(trip.getEndVisit().getPlace().getTimezone());
|
||||
entry.setFormattedTimeRange(formatTimeRange(trip.getStartTime(), trip.getEndTime(), timezone, selectedDate));
|
||||
entry.setFormattedDuration(formatDuration(trip.getStartTime(), trip.getEndTime()));
|
||||
entry.setFormattedLocalTimeRange(formatTimeRange(trip.getStartTime(), trip.getEndTime(), trip.getStartVisit().getPlace().getTimezone(), trip.getEndVisit().getPlace().getTimezone(), selectedDate));
|
||||
|
||||
List<RawLocationPoint> path = this.rawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, trip.getStartTime(), trip.getEndTime());
|
||||
List<PointInfo> pathPoints = new ArrayList<>();
|
||||
@@ -109,10 +113,10 @@ public class TimelineService {
|
||||
entry.setPath(objectMapper.writeValueAsString(pathPoints));
|
||||
if (trip.getTravelledDistanceMeters() != null) {
|
||||
entry.setDistanceMeters(trip.getTravelledDistanceMeters());
|
||||
entry.setFormattedDistance(formatDistance(trip.getTravelledDistanceMeters(), unitSystem));
|
||||
entry.setFormattedDistance(formatDistance(trip.getTravelledDistanceMeters(), userSettings.getUnitSystem()));
|
||||
} else if (trip.getEstimatedDistanceMeters() != null) {
|
||||
entry.setDistanceMeters(trip.getEstimatedDistanceMeters());
|
||||
entry.setFormattedDistance(formatDistance(trip.getEstimatedDistanceMeters(), unitSystem));
|
||||
entry.setFormattedDistance(formatDistance(trip.getEstimatedDistanceMeters(), userSettings.getUnitSystem()));
|
||||
}
|
||||
|
||||
if (trip.getTransportModeInferred() != null) {
|
||||
@@ -128,36 +132,37 @@ public class TimelineService {
|
||||
return entries;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format time range for display
|
||||
*/
|
||||
private String formatTimeRange(Instant startTime, Instant endTime, ZoneId timezone, LocalDate selectedDate) {
|
||||
private String formatTimeRange(Instant startTime, Instant endTime, ZoneId startTimezone, ZoneId endTimezone, LocalDate selectedDate) {
|
||||
DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HH:mm");
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("MMM d HH:mm");
|
||||
|
||||
LocalDate startDate = startTime.atZone(timezone).toLocalDate();
|
||||
LocalDate endDate = endTime.atZone(timezone).toLocalDate();
|
||||
|
||||
LocalDate startDate = startTime.atZone(startTimezone).toLocalDate();
|
||||
LocalDate endDate = endTime.atZone(endTimezone).toLocalDate();
|
||||
LocalDate selectedDateInStartTimezone = selectedDate.atTime(10,0).atZone(startTimezone).toLocalDate();
|
||||
LocalDate selectedDateInEndTimezone = selectedDate.atTime(10,0).atZone(endTimezone).toLocalDate();
|
||||
String start, end;
|
||||
|
||||
// If start time is not on the selected date, show date + time
|
||||
if (!startDate.equals(selectedDate)) {
|
||||
start = startTime.atZone(timezone).format(dateTimeFormatter);
|
||||
if (!startDate.equals(selectedDateInStartTimezone)) {
|
||||
start = startTime.atZone(startTimezone).format(dateTimeFormatter);
|
||||
} else {
|
||||
start = startTime.atZone(timezone).format(timeFormatter);
|
||||
start = startTime.atZone(startTimezone).format(timeFormatter);
|
||||
}
|
||||
|
||||
// If end time is not on the selected date, show date + time
|
||||
if (!endDate.equals(selectedDate)) {
|
||||
end = endTime.atZone(timezone).format(dateTimeFormatter);
|
||||
if (!endDate.equals(selectedDateInEndTimezone)) {
|
||||
end = endTime.atZone(endTimezone).format(dateTimeFormatter);
|
||||
} else {
|
||||
end = endTime.atZone(timezone).format(timeFormatter);
|
||||
end = endTime.atZone(endTimezone).format(timeFormatter);
|
||||
}
|
||||
|
||||
return start + " - " + end;
|
||||
}
|
||||
|
||||
private String formatTimeRange(Instant startTime, Instant endTime, ZoneId timezone, LocalDate selectedDate) {
|
||||
return formatTimeRange(startTime, endTime, timezone, timezone, selectedDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration for display (this is a simple implementation, you might want to use HumanizeDuration)
|
||||
*/
|
||||
|
||||
@@ -25,11 +25,11 @@ public class ReverseGeocodingListener {
|
||||
}
|
||||
|
||||
public void handleSignificantPlaceCreated(SignificantPlaceCreatedEvent event) {
|
||||
logger.info("Received SignificantPlaceCreatedEvent for place ID: {}", event.getPlaceId());
|
||||
logger.info("Received SignificantPlaceCreatedEvent for place ID: {}", event.placeId());
|
||||
|
||||
Optional<SignificantPlace> placeOptional = significantPlaceJdbcService.findById(event.getPlaceId());
|
||||
Optional<SignificantPlace> placeOptional = significantPlaceJdbcService.findById(event.placeId());
|
||||
if (placeOptional.isEmpty()) {
|
||||
logger.error("Could not find SignificantPlace with ID: {}", event.getPlaceId());
|
||||
logger.error("Could not find SignificantPlace with ID: {}", event.placeId());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -48,12 +48,11 @@ public class ReverseGeocodingListener {
|
||||
SignificantPlace.PlaceType placeType = result.placeType();
|
||||
String countryCode = result.countryCode();
|
||||
|
||||
String address = String.format("%s %s, %s %s", street, houseNumber, postcode, city);
|
||||
if (!label.isEmpty()) {
|
||||
place = place.withName(label)
|
||||
.withAddress(String.format("%s %s, %s %s", street, houseNumber, postcode, city));
|
||||
place = place.withName(label).withAddress(address);
|
||||
} else {
|
||||
place = place.withName(street)
|
||||
.withAddress(String.format("%s %s, %s %s", street, houseNumber, postcode, city));
|
||||
place = place.withName(street).withAddress(address);
|
||||
}
|
||||
place = place.withType(placeType).withCountryCode(countryCode);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.dedicatedcode.reitti.event.VisitUpdatedEvent;
|
||||
import com.dedicatedcode.reitti.model.geo.*;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.*;
|
||||
import com.dedicatedcode.reitti.service.GeoLocationTimezoneService;
|
||||
import com.dedicatedcode.reitti.service.UserNotificationService;
|
||||
import org.locationtech.jts.geom.Coordinate;
|
||||
import org.locationtech.jts.geom.GeometryFactory;
|
||||
@@ -20,6 +21,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@@ -39,6 +41,7 @@ public class VisitMergingService {
|
||||
private final GeometryFactory geometryFactory;
|
||||
private final RabbitTemplate rabbitTemplate;
|
||||
private final UserNotificationService userNotificationService;
|
||||
private final GeoLocationTimezoneService timezoneService;
|
||||
private final long mergeThresholdSeconds;
|
||||
private final long mergeThresholdMeters;
|
||||
private final int searchRangeExtensionInHours;
|
||||
@@ -52,6 +55,7 @@ public class VisitMergingService {
|
||||
RawLocationPointJdbcService rawLocationPointJdbcService,
|
||||
GeometryFactory geometryFactory,
|
||||
UserNotificationService userNotificationService,
|
||||
GeoLocationTimezoneService timezoneService,
|
||||
@Value("${reitti.visit.merge-max-stay-search-extension-days:2}") int maxStaySearchExtensionInDays,
|
||||
@Value("${reitti.visit.merge-threshold-seconds:300}") long mergeThresholdSeconds,
|
||||
@Value("${reitti.visit.merge-threshold-meters:100}") long mergeThresholdMeters) {
|
||||
@@ -63,6 +67,7 @@ public class VisitMergingService {
|
||||
this.rawLocationPointJdbcService = rawLocationPointJdbcService;
|
||||
this.geometryFactory = geometryFactory;
|
||||
this.userNotificationService = userNotificationService;
|
||||
this.timezoneService = timezoneService;
|
||||
this.mergeThresholdSeconds = mergeThresholdSeconds;
|
||||
this.mergeThresholdMeters = mergeThresholdMeters;
|
||||
this.searchRangeExtensionInHours = maxStaySearchExtensionInDays * 24;
|
||||
@@ -218,9 +223,11 @@ public class VisitMergingService {
|
||||
}
|
||||
|
||||
private SignificantPlace createSignificantPlace(User user, Visit visit) {
|
||||
Point point = geometryFactory.createPoint(new Coordinate(visit.getLongitude(), visit.getLatitude()));
|
||||
|
||||
SignificantPlace significantPlace = SignificantPlace.create(visit.getLatitude(), visit.getLongitude());
|
||||
Optional<ZoneId> timezone = this.timezoneService.getTimezone(significantPlace);
|
||||
if (timezone.isPresent()) {
|
||||
significantPlace = significantPlace.withTimezone(timezone.get());
|
||||
}
|
||||
significantPlace = this.significantPlaceJdbcService.create(user, significantPlace);
|
||||
publishSignificantPlaceCreatedEvent(significantPlace);
|
||||
return significantPlace;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE significant_places ADD COLUMN timezone TEXT NULL;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE user_settings
|
||||
ADD COLUMN time_zone_override TEXT NULL,
|
||||
ADD COLUMN time_display_mode VARCHAR(255) NOT NULL DEFAULT 'DEFAULT'
|
||||
@@ -103,6 +103,20 @@ users.oidc.managed.message=This user is managed by an external OIDC provider. Se
|
||||
users.oidc.view.profile=View external profile
|
||||
users.avatar.oidc.managed=Avatar is managed by your OIDC provider and will be updated automatically.
|
||||
|
||||
time.title=Time
|
||||
time.display.mode.label=Time Display Mode
|
||||
time.display.mode.default=Default
|
||||
time.display.mode.geo.local=Geo Local
|
||||
time.display.mode.description=Choose how times are displayed throughout the application.
|
||||
time.display.mode.default.description=Default: All times are displayed in your timezone (from browser or timezone override below)
|
||||
time.display.mode.geo.local.description=Geo Local: All times are displayed in the timezone where the location is
|
||||
time.timezone.override.label=Timezone Override
|
||||
time.timezone.override.none=Use browser timezone
|
||||
time.timezone.override.description=Override your timezone instead of using the browser's detected timezone. This affects how times are displayed when using Default mode.
|
||||
|
||||
timeline.time.your=Your time
|
||||
timeline.time.local=Local time
|
||||
|
||||
form.remove=Remove
|
||||
|
||||
users.home.location.label=Home Location
|
||||
|
||||
@@ -110,6 +110,19 @@ users.home.longitude.placeholder=L\u00E4ngengrad eingeben (-180 bis 180)
|
||||
users.home.location.clear=L\u00F6schen
|
||||
|
||||
|
||||
time.title=Zeit
|
||||
time.display.mode.label=Zeitanzeige-Modus
|
||||
time.display.mode.default=Standard
|
||||
time.display.mode.geo.local=Geo-Lokal
|
||||
time.display.mode.description=W\u00E4hlen Sie, wie Zeiten in der Anwendung angezeigt werden.
|
||||
time.display.mode.default.description=Standard: Alle Zeiten werden in Ihrer Zeitzone angezeigt (vom Browser oder der Zeitzone-\u00DCberschreibung unten)
|
||||
time.display.mode.geo.local.description=Geo-Lokal: Alle Zeiten werden in der Zeitzone angezeigt, in der sich der Ort befindet
|
||||
time.timezone.override.label=Zeitzone \u00FCberschreiben
|
||||
time.timezone.override.none=Browser-Zeitzone verwenden
|
||||
time.timezone.override.description=\u00DCberschreiben Sie Ihre Zeitzone anstatt die vom Browser erkannte Zeitzone zu verwenden. Dies beeinflusst, wie Zeiten im Standard-Modus angezeigt werden.
|
||||
|
||||
timeline.time.your=Ihre Zeit
|
||||
timeline.time.local=Ortszeit
|
||||
# Avatar
|
||||
users.avatar.label=Profilbild
|
||||
users.avatar.upload=Bild ausw\u00E4hlen
|
||||
|
||||
@@ -99,6 +99,21 @@ users.role.label=Rooli
|
||||
users.role.admin=Yll\u00E4pit\u00E4j\u00E4
|
||||
users.role.user=K\u00E4ytt\u00E4j\u00E4
|
||||
users.delete.confirm=Oletko varma, ett\u00E4 haluat poistaa t\u00E4m\u00E4n k\u00E4ytt\u00E4j\u00E4n? T\u00E4m\u00E4 poistaa kaikki heid\u00E4n tietonsa.
|
||||
|
||||
time.title=Aika
|
||||
time.display.mode.label=Ajan n\u00E4ytt\u00F6tapa
|
||||
time.display.mode.default=Oletus
|
||||
time.display.mode.geo.local=Geo-paikallinen
|
||||
time.display.mode.description=Valitse, miten ajat n\u00E4ytet\u00E4\u00E4n sovelluksessa.
|
||||
time.display.mode.default.description=Oletus: Kaikki ajat n\u00E4ytet\u00E4\u00E4n aikavy\u00F6hykkeess\u00E4si (selaimesta tai aikavy\u00F6hykkeen ohituksesta alla)
|
||||
time.display.mode.geo.local.description=Geo-paikallinen: Kaikki ajat n\u00E4ytet\u00E4\u00E4n siin\u00E4 aikavy\u00F6hykkeess\u00E4, jossa sijainti on
|
||||
time.timezone.override.label=Aikavy\u00F6hykkeen ohitus
|
||||
time.timezone.override.none=K\u00E4yt\u00E4 selaimen aikavy\u00F6hykett\u00E4
|
||||
time.timezone.override.description=Ohita aikavy\u00F6hykkeesi sen sijaan, ett\u00E4 k\u00E4ytt\u00E4isit selaimen havaitsemaa aikavy\u00F6hykett\u00E4. T\u00E4m\u00E4 vaikuttaa siihen, miten ajat n\u00E4ytet\u00E4\u00E4n Oletus-tilassa.
|
||||
|
||||
timeline.time.your=Sinun aikasi
|
||||
timeline.time.local=Paikallinen aika
|
||||
|
||||
form.remove=Poista
|
||||
|
||||
# Avatar
|
||||
|
||||
@@ -99,8 +99,22 @@ users.role.label=R\u00F4le
|
||||
users.role.admin=Administrateur
|
||||
users.role.user=Utilisateur
|
||||
users.delete.confirm=\u00CAtes-vous s\u00FBr de vouloir supprimer cet utilisateur ? Cela supprimera toutes leurs donn\u00E9es.
|
||||
|
||||
time.title=Heure
|
||||
time.display.mode.label=Mode d'affichage de l'heure
|
||||
time.display.mode.default=Par d\u00E9faut
|
||||
time.display.mode.geo.local=G\u00E9o-local
|
||||
time.display.mode.description=Choisissez comment les heures sont affich\u00E9es dans l'application.
|
||||
time.display.mode.default.description=Par d\u00E9faut : Toutes les heures sont affich\u00E9es dans votre fuseau horaire (du navigateur ou du remplacement de fuseau horaire ci-dessous)
|
||||
time.display.mode.geo.local.description=G\u00E9o-local : Toutes les heures sont affich\u00E9es dans le fuseau horaire o\u00F9 se trouve l'emplacement
|
||||
time.timezone.override.label=Remplacement du fuseau horaire
|
||||
time.timezone.override.none=Utiliser le fuseau horaire du navigateur
|
||||
time.timezone.override.description=Remplacez votre fuseau horaire au lieu d'utiliser le fuseau horaire d\u00E9tect\u00E9 par le navigateur. Cela affecte la fa\u00E7on dont les heures sont affich\u00E9es en mode Par d\u00E9faut.
|
||||
|
||||
form.remove=Supprimer
|
||||
|
||||
timeline.time.your=Votre heure
|
||||
timeline.time.local=Heure locale
|
||||
|
||||
# Avatar
|
||||
users.avatar.label=Photo de profil
|
||||
|
||||
@@ -9,6 +9,7 @@ class PhotoClient {
|
||||
/**
|
||||
* Update photos for the selected date
|
||||
* @param {string} date - Date in YYYY-MM-DD format
|
||||
* @param timezone
|
||||
*/
|
||||
async updatePhotosForDate(date, timezone) {
|
||||
this.currentDate = date;
|
||||
|
||||
@@ -87,7 +87,12 @@
|
||||
|
||||
<!-- Time -->
|
||||
<div class="entry-time">
|
||||
<span th:text="${entry.formattedTimeRange}">09:00 - 10:30</span>
|
||||
<span th:if="${timeDisplayMode.name() == 'GEO_LOCAL'}"
|
||||
th:text="${entry.formattedLocalTimeRange}"
|
||||
th:title="|#{timeline.time.your}: ${entry.formattedTimeRange}|">09:00 - 10:30</span>
|
||||
<span th:if="${timeDisplayMode.name() == 'DEFAULT'}"
|
||||
th:text="${entry.formattedTimeRange}"
|
||||
th:title="|#{timeline.time.local}: ${entry.formattedLocalTimeRange}|">09:00 - 10:30</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -214,6 +214,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="form-group">
|
||||
<h3 th:text="#{time.title}">Time</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timeDisplayMode" th:text="#{time.display.mode.label}">Time Display Mode</label>
|
||||
<div class="select">
|
||||
<select id="timeDisplayMode" name="time_display_mode">
|
||||
<option value="DEFAULT" th:selected="${timeDisplayMode == 'DEFAULT' || timeDisplayMode == null}" th:text="#{time.display.mode.default}">Default</option>
|
||||
<option value="GEO_LOCAL" th:selected="${timeDisplayMode == 'GEO_LOCAL'}" th:text="#{time.display.mode.geo.local}">Geo Local</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="form-description" th:text="#{time.display.mode.description}">Choose how times are displayed throughout the application.</p>
|
||||
<ul class="form-description">
|
||||
<li th:text="#{time.display.mode.default.description}">Default: All times are displayed in your timezone (from browser or timezone override below)</li>
|
||||
<li th:text="#{time.display.mode.geo.local.description}">Geo Local: All times are displayed in the timezone where the location is</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="timezoneOverride" th:text="#{time.timezone.override.label}">Timezone Override</label>
|
||||
<div class="select">
|
||||
<select id="timezoneOverride" name="timezone_override">
|
||||
<option value="" th:text="#{time.timezone.override.none}">Use browser timezone</option>
|
||||
<option th:each="timezone : ${availableTimezones}"
|
||||
th:value="${timezone}"
|
||||
th:text="${timezone}"
|
||||
th:selected="${timeZoneOverride != null && timeZoneOverride.toString() == timezone}"></option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="form-description" th:text="#{time.timezone.override.description}">Override your timezone instead of using the browser's detected timezone. This affects how times are displayed when using Default mode.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" name="preferColoredMap" th:checked="${preferColoredMap}" class="checkbox-input">
|
||||
|
||||
@@ -119,9 +119,13 @@
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
}
|
||||
// Helper function for HTMX to get user timezone
|
||||
|
||||
function getUserTimezone() {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
if (window.userSettings.timezoneOverride) {
|
||||
return window.userSettings.timezoneOverride;
|
||||
} else {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
}
|
||||
|
||||
function selectUser(userHeader) {
|
||||
@@ -174,8 +178,7 @@
|
||||
}
|
||||
|
||||
// Set date picker to URL date or today's date
|
||||
const today = new Date();
|
||||
const formattedDate = initialDate || today.toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
const formattedDate = initialDate || new Date().toISOString().split('T')[0];
|
||||
|
||||
// Function to update URL with date parameter
|
||||
function updateUrlWithDate(date) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.dedicatedcode.reitti.repository;
|
||||
|
||||
import com.dedicatedcode.reitti.IntegrationTest;
|
||||
import com.dedicatedcode.reitti.model.*;
|
||||
import com.dedicatedcode.reitti.model.Page;
|
||||
import com.dedicatedcode.reitti.model.PageRequest;
|
||||
import com.dedicatedcode.reitti.model.Role;
|
||||
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -12,6 +14,7 @@ import org.locationtech.jts.geom.Point;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
@@ -127,6 +130,7 @@ class SignificantPlaceJdbcServiceTest {
|
||||
53.863149,
|
||||
10.700927,
|
||||
SignificantPlace.PlaceType.RESTAURANT,
|
||||
ZoneId.systemDefault(),
|
||||
true,
|
||||
created.getVersion()
|
||||
);
|
||||
@@ -139,6 +143,7 @@ class SignificantPlaceJdbcServiceTest {
|
||||
assertThat(result.getAddress()).isEqualTo("Updated Address");
|
||||
assertThat(result.getCountryCode()).isEqualTo("DE");
|
||||
assertThat(result.getType()).isEqualTo(SignificantPlace.PlaceType.RESTAURANT);
|
||||
assertThat(result.getTimezone()).isEqualTo(ZoneId.systemDefault());
|
||||
assertThat(result.isGeocoded()).isTrue();
|
||||
}
|
||||
|
||||
@@ -209,6 +214,7 @@ class SignificantPlaceJdbcServiceTest {
|
||||
created1.getLatitudeCentroid(),
|
||||
created1.getLongitudeCentroid(),
|
||||
SignificantPlace.PlaceType.HOME,
|
||||
ZoneId.systemDefault(),
|
||||
true, // geocoded = true
|
||||
created1.getVersion()
|
||||
);
|
||||
@@ -262,7 +268,8 @@ class SignificantPlaceJdbcServiceTest {
|
||||
latitude,
|
||||
longitude,
|
||||
SignificantPlace.PlaceType.OTHER,
|
||||
false,
|
||||
ZoneId.systemDefault()
|
||||
, false,
|
||||
0L
|
||||
);
|
||||
}
|
||||
@@ -276,6 +283,7 @@ class SignificantPlaceJdbcServiceTest {
|
||||
latitude,
|
||||
longitude,
|
||||
SignificantPlace.PlaceType.OTHER,
|
||||
ZoneId.systemDefault(),
|
||||
false,
|
||||
0L
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.dedicatedcode.reitti.repository;
|
||||
import com.dedicatedcode.reitti.IntegrationTest;
|
||||
import com.dedicatedcode.reitti.dto.LocationDataRequest;
|
||||
import com.dedicatedcode.reitti.model.Role;
|
||||
import com.dedicatedcode.reitti.model.TimeDisplayMode;
|
||||
import com.dedicatedcode.reitti.model.UnitSystem;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.model.security.UserSettings;
|
||||
@@ -59,7 +60,7 @@ public class UserSettingsJdbcServiceTest {
|
||||
|
||||
@Test
|
||||
void save_WhenCreatingNewUserSettings_ShouldInsertAndReturnWithId() {
|
||||
UserSettings newSettings = new UserSettings(testUserId1, true, "fi", UnitSystem.METRIC, 60.1699, 24.9384, Instant.now());
|
||||
UserSettings newSettings = new UserSettings(testUserId1, true, "fi", UnitSystem.METRIC, 60.1699, 24.9384, null, TimeDisplayMode.DEFAULT, Instant.now(), null);
|
||||
|
||||
UserSettings savedSettings = userSettingsJdbcService.save(newSettings);
|
||||
|
||||
@@ -74,7 +75,7 @@ public class UserSettingsJdbcServiceTest {
|
||||
@Test
|
||||
void save_WhenUpdatingExistingUserSettings_ShouldUpdateAndIncrementVersion() {
|
||||
// Create initial settings
|
||||
UserSettings initialSettings = new UserSettings(testUserId1, false, "en", UnitSystem.METRIC, null, null, Instant.now());
|
||||
UserSettings initialSettings = new UserSettings(testUserId1, false, "en", UnitSystem.METRIC, null, null, null, TimeDisplayMode.DEFAULT, Instant.now(), null);
|
||||
UserSettings savedSettings = userSettingsJdbcService.save(initialSettings);
|
||||
|
||||
// Update settings
|
||||
@@ -85,9 +86,9 @@ public class UserSettingsJdbcServiceTest {
|
||||
UnitSystem.IMPERIAL,
|
||||
52.5200,
|
||||
13.4050,
|
||||
Instant.now(),
|
||||
savedSettings.getVersion()
|
||||
);
|
||||
null,
|
||||
TimeDisplayMode.DEFAULT,
|
||||
Instant.now(), savedSettings.getVersion());
|
||||
|
||||
UserSettings result = userSettingsJdbcService.save(updatedSettings);
|
||||
|
||||
@@ -103,7 +104,7 @@ public class UserSettingsJdbcServiceTest {
|
||||
@Test
|
||||
void findByUserId_WhenUserSettingsExist_ShouldReturnSettings() {
|
||||
// Create settings
|
||||
UserSettings newSettings = new UserSettings(testUserId1, true, "fr", UnitSystem.METRIC, 48.8566, 2.3522, Instant.now());
|
||||
UserSettings newSettings = new UserSettings(testUserId1, true, "fr", UnitSystem.METRIC, 48.8566, 2.3522, null, TimeDisplayMode.DEFAULT, Instant.now(), null);
|
||||
userSettingsJdbcService.save(newSettings);
|
||||
|
||||
Optional<UserSettings> result = userSettingsJdbcService.findByUserId(testUserId1);
|
||||
@@ -136,7 +137,7 @@ public class UserSettingsJdbcServiceTest {
|
||||
@Test
|
||||
void getOrCreateDefaultSettings_WhenUserSettingsExist_ShouldReturnExisting() {
|
||||
// Create existing settings
|
||||
UserSettings existingSettings = new UserSettings(testUserId1, true, "fi", UnitSystem.METRIC, 60.1699, 24.9384, Instant.now());
|
||||
UserSettings existingSettings = new UserSettings(testUserId1, true, "fi", UnitSystem.METRIC, 60.1699, 24.9384, null, TimeDisplayMode.DEFAULT, Instant.now(), null);
|
||||
userSettingsJdbcService.save(existingSettings);
|
||||
|
||||
UserSettings result = userSettingsJdbcService.getOrCreateDefaultSettings(testUserId1);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.dedicatedcode.reitti.service;
|
||||
|
||||
import com.dedicatedcode.reitti.model.geo.SignificantPlace;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GeoLocationTimezoneServiceTest {
|
||||
|
||||
@InjectMocks
|
||||
private GeoLocationTimezoneService geoLocationTimezoneService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// Initialize the service if needed
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForNewYorkCity() {
|
||||
// New York City, USA - Eastern Time
|
||||
SignificantPlace newYork = SignificantPlace.create(40.7128, -74.0060);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(newYork);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("America/New_York"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForLondon() {
|
||||
// London, UK - Greenwich Mean Time
|
||||
SignificantPlace london = SignificantPlace.create(51.5074, -0.1278);
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(london);
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("Europe/London"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForTokyo() {
|
||||
// Tokyo, Japan - Japan Standard Time
|
||||
SignificantPlace tokyo = SignificantPlace.create(35.6762, 139.6503);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(tokyo);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("Asia/Tokyo"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForSydney() {
|
||||
// Sydney, Australia - Australian Eastern Time
|
||||
SignificantPlace sydney = SignificantPlace.create(-33.8688, 151.2093);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(sydney);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("Australia/Sydney"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForSaoPaulo() {
|
||||
// São Paulo, Brazil - Brasília Time
|
||||
SignificantPlace saoPaulo = SignificantPlace.create(-23.5505, -46.6333);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(saoPaulo);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("America/Sao_Paulo"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForAntarctica() {
|
||||
// McMurdo Station, Antarctica - Multiple timezones possible
|
||||
// This location can have different timezone interpretations
|
||||
SignificantPlace mcmurdo = SignificantPlace.create(-77.8419, 166.6863);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(mcmurdo);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
// Antarctica/McMurdo is linked to Pacific/Auckland
|
||||
assertTrue(timezone.get().equals(ZoneId.of("Antarctica/McMurdo")) ||
|
||||
timezone.get().equals(ZoneId.of("Pacific/Auckland")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForNorthPole() {
|
||||
// North Pole - Ambiguous timezone area
|
||||
SignificantPlace northPole = SignificantPlace.create(90.0, 0.0);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(northPole);
|
||||
|
||||
// At the North Pole, timezone is ambiguous, but service should return something
|
||||
assertTrue(timezone.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForInternationalDateLine() {
|
||||
// Location near International Date Line - Fiji
|
||||
SignificantPlace fiji = SignificantPlace.create(-18.1248, 178.4501);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(fiji);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("Pacific/Fiji"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForEquator() {
|
||||
// Location on the Equator - Quito, Ecuador
|
||||
SignificantPlace quito = SignificantPlace.create(-0.1807, -78.4678);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(quito);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("America/Guayaquil"), timezone.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTimezoneForPrimeMeridian() {
|
||||
// Location on Prime Meridian - Greenwich, London
|
||||
SignificantPlace greenwich = SignificantPlace.create(51.4769, 0.0005);
|
||||
|
||||
Optional<ZoneId> timezone = geoLocationTimezoneService.getTimezone(greenwich);
|
||||
|
||||
assertTrue(timezone.isPresent());
|
||||
assertEquals(ZoneId.of("Europe/London"), timezone.get());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user