diff --git a/src/main/java/com/dedicatedcode/reitti/config/TaskConfig.java b/src/main/java/com/dedicatedcode/reitti/config/TaskConfig.java index 8e3b905e2..06ab3e2ce 100644 --- a/src/main/java/com/dedicatedcode/reitti/config/TaskConfig.java +++ b/src/main/java/com/dedicatedcode/reitti/config/TaskConfig.java @@ -9,6 +9,7 @@ import com.dedicatedcode.reitti.service.importer.PromotionJobHandler; import com.dedicatedcode.reitti.service.jobs.JobSchedulingService; import com.dedicatedcode.reitti.service.jobs.JobType; +import com.dedicatedcode.reitti.service.jobs.TransportModeRecalculationTask; import com.dedicatedcode.reitti.service.jobs.VisitSensitivityConfigurationRecalculationTask; import com.dedicatedcode.reitti.service.processing.*; import com.github.kagkarlsson.scheduler.task.Task; @@ -97,4 +98,13 @@ public Task dataRecalcu handler.execute(data); }); } + + @Bean + public Task transportModeRecalculationTaks(TransportModeRecalculationTask handler) { + return Tasks.oneTime("transport-mode-recalculation-task", TransportModeRecalculationTask.TaskData.class) + .execute((instance, context) -> { + TransportModeRecalculationTask.TaskData data = instance.getData(); + handler.execute(data); + }); + } } diff --git a/src/main/java/com/dedicatedcode/reitti/controller/settings/DeviceSettingsController.java b/src/main/java/com/dedicatedcode/reitti/controller/settings/DeviceSettingsController.java index 0b3db6ab3..d1a7efa2a 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/settings/DeviceSettingsController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/settings/DeviceSettingsController.java @@ -261,6 +261,7 @@ public String deleteDevice(@PathVariable Long deviceId, @AuthenticationPrincipal if (deviceToDelete.defaultDevice()) { model.addAttribute("errorMessage", i18n.translate("message.error.device.deletion.default")); } else { + avatarService.deleteAvatar(user.getId(), deviceId); deviceJdbcService.delete(deviceToDelete, user); model.addAttribute("successMessage", i18n.translate("message.success.device.deleted")); } diff --git a/src/main/java/com/dedicatedcode/reitti/controller/settings/TransportationModesController.java b/src/main/java/com/dedicatedcode/reitti/controller/settings/TransportationModesController.java index 9c20597bd..010efdd13 100644 --- a/src/main/java/com/dedicatedcode/reitti/controller/settings/TransportationModesController.java +++ b/src/main/java/com/dedicatedcode/reitti/controller/settings/TransportationModesController.java @@ -2,16 +2,16 @@ import com.dedicatedcode.reitti.model.Role; import com.dedicatedcode.reitti.model.UnitSystem; -import com.dedicatedcode.reitti.model.geo.RawLocationPoint; import com.dedicatedcode.reitti.model.geo.TransportMode; import com.dedicatedcode.reitti.model.geo.TransportModeConfig; import com.dedicatedcode.reitti.model.security.User; import com.dedicatedcode.reitti.model.security.UserSettings; -import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService; import com.dedicatedcode.reitti.repository.TransportModeJdbcService; -import com.dedicatedcode.reitti.repository.TripJdbcService; import com.dedicatedcode.reitti.repository.UserSettingsJdbcService; -import com.dedicatedcode.reitti.service.processing.TransportModeService; +import com.dedicatedcode.reitti.service.jobs.JobSchedulingService; +import com.dedicatedcode.reitti.service.jobs.JobType; +import com.dedicatedcode.reitti.service.jobs.TransportModeRecalculationTask; +import com.github.kagkarlsson.scheduler.task.Task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; @@ -21,12 +21,9 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; -import java.time.Instant; -import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; import java.util.Objects; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @Controller @@ -34,24 +31,21 @@ public class TransportationModesController { private static final Logger log = LoggerFactory.getLogger(TransportationModesController.class); - private final TripJdbcService tripJdbcService; private final TransportModeJdbcService transportModeJdbcService; private final UserSettingsJdbcService userSettingsJdbcService; - private final TransportModeService transportModeService; - private final RawLocationPointJdbcService rawLocationPointJdbcService; + private final Task recalculationJobTask; + private final JobSchedulingService jobSchedulingService; private final boolean dataManagementEnabled; - public TransportationModesController(TripJdbcService tripJdbcService, - TransportModeJdbcService transportModeJdbcService, + public TransportationModesController(TransportModeJdbcService transportModeJdbcService, UserSettingsJdbcService userSettingsJdbcService, - TransportModeService transportModeService, - RawLocationPointJdbcService rawLocationPointJdbcService, + Task recalculationJobTask, + JobSchedulingService jobSchedulingService, @Value("${reitti.data-management.enabled:false}") boolean dataManagementEnabled) { - this.tripJdbcService = tripJdbcService; this.transportModeJdbcService = transportModeJdbcService; this.userSettingsJdbcService = userSettingsJdbcService; - this.transportModeService = transportModeService; - this.rawLocationPointJdbcService = rawLocationPointJdbcService; + this.recalculationJobTask = recalculationJobTask; + this.jobSchedulingService = jobSchedulingService; this.dataManagementEnabled = dataManagementEnabled; } @@ -199,21 +193,12 @@ private Double mphToKmh(Double mph) { @PostMapping("/reclassify") public String reclassifyTrips(@AuthenticationPrincipal User user, Model model) { try { - // Start async reclassification - CompletableFuture.runAsync(() -> { - tripJdbcService.findByUser(user).forEach(trip -> { - Instant startTime = trip.getStartTime(); - Instant endTime = trip.getEndTime(); - List tripPoints = this.rawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, startTime, endTime.plus(1, ChronoUnit.MILLIS)); - TransportMode transportMode = this.transportModeService.inferTransportMode(user, tripPoints, startTime, endTime); - if (transportMode != trip.getTransportModeInferred()) { - log.trace("Reclassified trip {} from {} to {} to mode {}", trip.getId(), startTime, endTime, transportMode); - trip = trip.withTransportMode(transportMode); - this.tripJdbcService.update(trip); - } - }); - }); - + log.debug("Scheduling recalculation task"); + this.jobSchedulingService.enqueueTask(recalculationJobTask, new TransportModeRecalculationTask.TaskData(user), + JobSchedulingService.Metadata.builder() + .user(user) + .friendlyName("Recalculation for changed Transportation Mode Settings") + .jobType(JobType.DATA_RECALCULATION).build()); model.addAttribute("reclassifyStatus", "started"); model.addAttribute("message", "transportation.modes.reclassify.started"); diff --git a/src/main/java/com/dedicatedcode/reitti/repository/TimelineOverviewStatisticsService.java b/src/main/java/com/dedicatedcode/reitti/repository/TimelineOverviewStatisticsService.java index 091b48ae1..f2a014c3b 100644 --- a/src/main/java/com/dedicatedcode/reitti/repository/TimelineOverviewStatisticsService.java +++ b/src/main/java/com/dedicatedcode/reitti/repository/TimelineOverviewStatisticsService.java @@ -14,6 +14,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; +import java.sql.Date; import java.sql.Timestamp; import java.time.*; import java.time.format.DateTimeFormatter; @@ -48,143 +49,143 @@ public List load(User user, Instant start, Instant end, Zo .addValue("end", Timestamp.from(end)); List> allTripsByLower = this.jdbcTemplate.queryForList(""" - SELECT - d.day AT TIME ZONE :timezone AS time_bucket, - COUNT(t.id) AS amount - FROM ( - SELECT GENERATE_SERIES( - :start::timestamptz, - :end::timestamptz, - '1 day'::interval - ) AS day - ) d - LEFT JOIN trips t ON - DATE_TRUNC('day', t.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone = d.day - AND t.user_id = :userId - GROUP BY d.day - ORDER BY d.day; - """, params); + SELECT + d.day::timestamp AS time_bucket, -- Explicitly forces a consistent Timestamp object type return + COUNT(t.id) AS amount + FROM ( + SELECT GENERATE_SERIES( + (:start AT TIME ZONE :timezone)::date, + (:end AT TIME ZONE :timezone)::date, + '1 day'::interval + )::date AS day + ) d LEFT JOIN trips t ON + DATE_TRUNC('day', t.start_time AT TIME ZONE :timezone)::date = d.day + AND t.user_id = :userId + AND t.start_time >= :start AND t.start_time <= :end + GROUP BY d.day + ORDER BY d.day; + """, params); List> allVisitsByLower = this.jdbcTemplate.queryForList(""" - SELECT - d.day AT TIME ZONE :timezone AS time_bucket, - COUNT(pv.id) AS amount - FROM ( - SELECT GENERATE_SERIES( - :start::timestamptz, - :end::timestamptz, - '1 day'::interval - ) AS day - ) d - LEFT JOIN processed_visits pv ON - DATE_TRUNC('day', pv.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone = d.day - AND pv.user_id = :userId - GROUP BY d.day - ORDER BY d.day; - """, params); + SELECT + d.day::timestamp AS time_bucket, -- Explicitly forces a consistent Timestamp object type return + COUNT(pv.id) AS amount + FROM ( + SELECT GENERATE_SERIES( + (:start AT TIME ZONE :timezone)::date, + (:end AT TIME ZONE :timezone)::date, + '1 day'::interval + )::date AS day + ) d + LEFT JOIN processed_visits pv ON + DATE_TRUNC('day', pv.start_time AT TIME ZONE :timezone)::date = d.day + AND pv.user_id = :userId + AND pv.start_time >= :start AND pv.start_time <= :end + GROUP BY d.day + ORDER BY d.day; + """, params); List> tripMoodCountsPerSlice = this.jdbcTemplate.queryForList(""" - SELECT - DATE_TRUNC(:granularity, t.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, - t.transport_mode_inferred AS name, - lm.metadata->>'mood' AS mood, - SUM(t.duration_seconds)::BIGINT AS duration_seconds, - SUM(t.travelled_distance_meters)::BIGINT AS distance_meters, - COUNT(*) AS mood_count - FROM trips t - LEFT JOIN LATERAL ( - SELECT metadata - FROM location_metadata lm - WHERE lm.user_id = :userId - AND lm.time_range && TSTZRANGE(t.start_time, t.end_time) - AND lm.context_type = 'TRIP' - AND lm.metadata->>'mood' IS NOT NULL - ORDER BY UPPER(lm.time_range * TSTZRANGE(t.start_time, t.end_time)) - - LOWER(lm.time_range * TSTZRANGE(t.start_time, t.end_time)) DESC - LIMIT 1 - ) lm ON TRUE - WHERE t.user_id = :userId - AND t.start_time >= :start AND t.start_time <= :end - AND t.start_time < t.end_time - GROUP BY 1, 2, 3 - ORDER BY time_bucket; - """, params); + SELECT + DATE_TRUNC(:granularity, t.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, + t.transport_mode_inferred AS name, + lm.metadata->>'mood' AS mood, + SUM(t.duration_seconds)::BIGINT AS duration_seconds, + SUM(t.travelled_distance_meters)::BIGINT AS distance_meters, + COUNT(*) AS mood_count + FROM trips t + LEFT JOIN LATERAL ( + SELECT metadata + FROM location_metadata lm + WHERE lm.user_id = :userId + AND lm.time_range && TSTZRANGE(t.start_time, t.end_time) + AND lm.context_type = 'TRIP' + AND lm.metadata->>'mood' IS NOT NULL + ORDER BY UPPER(lm.time_range * TSTZRANGE(t.start_time, t.end_time)) + - LOWER(lm.time_range * TSTZRANGE(t.start_time, t.end_time)) DESC + LIMIT 1 + ) lm ON TRUE + WHERE t.user_id = :userId + AND t.start_time >= :start AND t.start_time <= :end + AND t.start_time < t.end_time + GROUP BY 1, 2, 3 + ORDER BY time_bucket; + """, params); List> visitMoodCountsPerSlice = this.jdbcTemplate.queryForList(""" - SELECT - DATE_TRUNC(:granularity, v.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, - lm.metadata->>'mood' AS mood, - SUM(v.duration_seconds)::BIGINT AS duration_seconds, - COUNT(*) AS mood_count - FROM processed_visits v - LEFT JOIN LATERAL ( - SELECT metadata - FROM location_metadata lm - WHERE lm.user_id = :userId - AND lm.time_range && TSTZRANGE(v.start_time, v.end_time) - AND lm.context_type = 'VISIT' - AND lm.metadata->>'mood' IS NOT NULL - ORDER BY UPPER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) - - LOWER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) DESC - LIMIT 1 - ) lm ON TRUE - WHERE v.user_id = :userId - AND v.start_time >= :start AND v.start_time <= :end - AND v.start_time < v.end_time - GROUP BY 1, 2 - ORDER BY time_bucket; - """, params); + SELECT + DATE_TRUNC(:granularity, v.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, + lm.metadata->>'mood' AS mood, + SUM(v.duration_seconds)::BIGINT AS duration_seconds, + COUNT(*) AS mood_count + FROM processed_visits v + LEFT JOIN LATERAL ( + SELECT metadata + FROM location_metadata lm + WHERE lm.user_id = :userId + AND lm.time_range && TSTZRANGE(v.start_time, v.end_time) + AND lm.context_type = 'VISIT' + AND lm.metadata->>'mood' IS NOT NULL + ORDER BY UPPER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) + - LOWER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) DESC + LIMIT 1 + ) lm ON TRUE + WHERE v.user_id = :userId + AND v.start_time >= :start AND v.start_time <= :end + AND v.start_time < v.end_time + GROUP BY 1, 2 + ORDER BY time_bucket; + """, params); List> visitCountsPerSlice = this.jdbcTemplate.queryForList(""" - SELECT - DATE_TRUNC(:granularity, v.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, - lm.metadata->>'mood' AS mood, - v.id, - s.id AS place_id, - s.name AS place_name, - SUM(v.duration_seconds)::BIGINT AS duration_seconds, - COUNT(*) AS mood_count - FROM processed_visits v - LEFT JOIN LATERAL ( - SELECT metadata - FROM location_metadata lm - WHERE lm.user_id = :userId - AND lm.time_range && TSTZRANGE(v.start_time, v.end_time) - AND lm.context_type = 'VISIT' - AND lm.metadata->>'mood' IS NOT NULL - ORDER BY UPPER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) - - LOWER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) DESC - LIMIT 1 - ) lm ON TRUE - LEFT JOIN significant_places s ON s.id = v.place_id - WHERE v.user_id = :userId - AND v.start_time >= :start AND v.start_time <= :end - GROUP BY 1, 2, 3, 4, 5 - ORDER BY time_bucket; - """, params); + SELECT + DATE_TRUNC(:granularity, v.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, + lm.metadata->>'mood' AS mood, + v.id, + s.id AS place_id, + s.name AS place_name, + SUM(v.duration_seconds)::BIGINT AS duration_seconds, + COUNT(*) AS mood_count + FROM processed_visits v + LEFT JOIN LATERAL ( + SELECT metadata + FROM location_metadata lm + WHERE lm.user_id = :userId + AND lm.time_range && TSTZRANGE(v.start_time, v.end_time) + AND lm.context_type = 'VISIT' + AND lm.metadata->>'mood' IS NOT NULL + ORDER BY UPPER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) + - LOWER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) DESC + LIMIT 1 + ) lm ON TRUE + LEFT JOIN significant_places s ON s.id = v.place_id + WHERE v.user_id = :userId + AND v.start_time >= :start AND v.start_time <= :end + GROUP BY 1, 2, 3, 4, 5 + ORDER BY time_bucket; + """, params); List> visits = this.jdbcTemplate.queryForList(""" - SELECT - DATE_TRUNC(:granularity, pv.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, - sp.name AS place_name, - COUNT(pv.id) AS amount - FROM processed_visits pv - LEFT JOIN significant_places sp ON pv.place_id = sp.id - WHERE pv.user_id = :userId - AND pv.start_time >= :start - AND pv.start_time <= :end - GROUP BY 1, 2 - ORDER BY time_bucket, amount DESC; - """, params); + SELECT + DATE_TRUNC(:granularity, pv.start_time AT TIME ZONE :timezone) AT TIME ZONE :timezone AS time_bucket, + COUNT(DISTINCT pv.place_id) AS amount + FROM processed_visits pv + WHERE pv.user_id = :userId + AND pv.start_time >= :start + AND pv.start_time <= :end + GROUP BY 1 + ORDER BY 1; + """, params); + Locale locale = LocaleContextHolder.getLocale(); List entries = new ArrayList<>(); Map>> splitTrips = splitupIntoDays(start, userTimezone, granularity, allTripsByLower); Map>> splitVisits = splitupIntoDays(start, userTimezone, granularity, allVisitsByLower); - Map amountOfTrips = calculateAmountPerSlice(granularity, userTimezone, locale, allTripsByLower); - Map amountOfPlaces = calculateAmountPerSlice(granularity, userTimezone, locale, visits); + Map amountOfTrips = calculateAmountPerSlice(granularity, userTimezone, allTripsByLower); + Map amountOfPlaces = calculateAmountPerSlice(granularity, userTimezone, visits); Map>> transportModeData = calculateTransportModeData(userTimezone, tripMoodCountsPerSlice); Map>> visitsMoodDurationData = calculateVisitsDataPerPlace(userTimezone, visitCountsPerSlice); @@ -232,11 +233,13 @@ ORDER BY UPPER(lm.time_range * TSTZRANGE(v.start_time, v.end_time)) LocalDate rangeEnd = granularity == Granularity.MONTHLY ? sortedKey.withDayOfMonth(1).plusMonths(1).minusDays(1) : sortedKey.with(ChronoField.DAY_OF_WEEK, 1).plusWeeks(1).minusDays(1); - subheadline = i18nService.translate("js.common.time-range", sortedKey.format(DateTimeFormatter.ofPattern("dd.MM").withLocale(locale)), rangeEnd.format(DateTimeFormatter.ofPattern("dd.MM.yyyy").withLocale(locale))); + LocalDate adjustedRangeStart = sortedKey.isBefore(start.atZone(userTimezone).toLocalDate()) ? start.atZone(userTimezone).toLocalDate() : sortedKey; + LocalDate adjustedRangeEnd = rangeEnd.isAfter(end.atZone(userTimezone).toLocalDate()) ? end.atZone(userTimezone).toLocalDate() : rangeEnd; + subheadline = i18nService.translate("js.common.time-range", adjustedRangeStart.format(DateTimeFormatter.ofPattern("dd.MM").withLocale(locale)), adjustedRangeEnd.format(DateTimeFormatter.ofPattern("dd.MM.yyyy").withLocale(locale))); entries.add(new GroupedTimelineEntry(UUID.randomUUID(), name, subheadline, - String.format(contextPathHolder.getContextPath() + "/?startDate=%s&endDate=%s", sortedKey, endDate), + String.format(contextPathHolder.getContextPath() + "/?startDate=%s&endDate=%s", adjustedRangeStart, adjustedRangeEnd), overviewEntries, amountOfPlaces.get(sortedKey), amountOfTrips.get(sortedKey), @@ -327,7 +330,7 @@ private Map>> calcul return result; } - private Map calculateAmountPerSlice(Granularity granularity, ZoneId userTimezone, Locale locale, List> entries) { + private Map calculateAmountPerSlice(Granularity granularity, ZoneId userTimezone, List> entries) { TemporalAdjuster temporalAdjuster = granularity == Granularity.WEEKLY ? TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY) : TemporalAdjusters.firstDayOfMonth(); diff --git a/src/main/java/com/dedicatedcode/reitti/repository/TripJdbcService.java b/src/main/java/com/dedicatedcode/reitti/repository/TripJdbcService.java index 2425c96cf..ac9cbee29 100644 --- a/src/main/java/com/dedicatedcode/reitti/repository/TripJdbcService.java +++ b/src/main/java/com/dedicatedcode/reitti/repository/TripJdbcService.java @@ -65,9 +65,7 @@ public Trip mapRow(ResultSet rs, int rowNum) throws SQLException { }; public List findByUser(User user) { - String sql = "SELECT t.*" + - "FROM trips t " + - "WHERE t.user_id = ? ORDER BY start_time"; + String sql = "SELECT t.* FROM trips t WHERE t.user_id = ? ORDER BY start_time"; return jdbcTemplate.query(sql, TRIP_ROW_MAPPER, user.getId()); } @@ -212,6 +210,11 @@ public long count() { return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM trips", Long.class); } + @SuppressWarnings("DataFlowIssue") + public long count(User user) { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM trips WHERE user_id = ?", Long.class, user.getId()); + } + public void deleteAll(List existingTrips) { if (existingTrips == null || existingTrips.isEmpty()) { return; @@ -251,4 +254,5 @@ private String asJson(Object value) { throw new RuntimeException(e); } } + } diff --git a/src/main/java/com/dedicatedcode/reitti/repository/UserMapStyleJdbcService.java b/src/main/java/com/dedicatedcode/reitti/repository/UserMapStyleJdbcService.java index 6a3ce2482..6d5119b32 100644 --- a/src/main/java/com/dedicatedcode/reitti/repository/UserMapStyleJdbcService.java +++ b/src/main/java/com/dedicatedcode/reitti/repository/UserMapStyleJdbcService.java @@ -49,8 +49,8 @@ public UserMapStyleJdbcService(JdbcTemplate jdbcTemplate) { rs.getString("glyphs_url_override"), rs.getString("sprite_url_override") ), - rs.getBoolean("shared"), rs.getBoolean("default_style"), + rs.getBoolean("shared"), rs.getLong("version") ); diff --git a/src/main/java/com/dedicatedcode/reitti/service/DatabaseJanitorService.java b/src/main/java/com/dedicatedcode/reitti/service/DatabaseJanitorService.java new file mode 100644 index 000000000..88a162c81 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/DatabaseJanitorService.java @@ -0,0 +1,26 @@ +package com.dedicatedcode.reitti.service; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class DatabaseJanitorService { + private static final Logger log = LoggerFactory.getLogger(DatabaseJanitorService.class); + private final JdbcTemplate jdbcTemplate; + + public DatabaseJanitorService(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Scheduled(cron = "${reitti.db-janitor.schedule}") + public void runJanitor() { + log.info("Running Janitor"); + long start = System.currentTimeMillis(); + jdbcTemplate.update("DELETE FROM api_token_usages WHERE at < now() - interval '1 week';"); + log.info("Clearing old api-token-usages in {}ms", System.currentTimeMillis() - start); + jdbcTemplate.execute("VACUUM ANALYZE;"); + } +} diff --git a/src/main/java/com/dedicatedcode/reitti/service/UserService.java b/src/main/java/com/dedicatedcode/reitti/service/UserService.java index c811a07aa..9c612bce0 100644 --- a/src/main/java/com/dedicatedcode/reitti/service/UserService.java +++ b/src/main/java/com/dedicatedcode/reitti/service/UserService.java @@ -142,6 +142,7 @@ public User createNewUser(String username, setDefaultMapStyle(createdUser); saveDefaultVisitDetectionParameters(createdUser); saveDefaultTransportationModeDetectionParameters(createdUser); + createDefaultDeviceForUser(createdUser); userSettingsJdbcService.save(userSettings); return createdUser; } diff --git a/src/main/java/com/dedicatedcode/reitti/service/jobs/TransportModeRecalculationTask.java b/src/main/java/com/dedicatedcode/reitti/service/jobs/TransportModeRecalculationTask.java new file mode 100644 index 000000000..0ffacac52 --- /dev/null +++ b/src/main/java/com/dedicatedcode/reitti/service/jobs/TransportModeRecalculationTask.java @@ -0,0 +1,79 @@ +package com.dedicatedcode.reitti.service.jobs; + +import com.dedicatedcode.reitti.model.geo.RawLocationPoint; +import com.dedicatedcode.reitti.model.geo.TransportMode; +import com.dedicatedcode.reitti.model.security.User; +import com.dedicatedcode.reitti.repository.JobMetadataRepository; +import com.dedicatedcode.reitti.repository.RawLocationPointJdbcService; +import com.dedicatedcode.reitti.repository.TripJdbcService; +import com.dedicatedcode.reitti.service.JobContext; +import com.dedicatedcode.reitti.service.processing.TransportModeService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +@Service +public class TransportModeRecalculationTask { + private static final Logger log = LoggerFactory.getLogger(TransportModeRecalculationTask.class); + private final TripJdbcService tripJdbcService; + private final RawLocationPointJdbcService rawLocationPointJdbcService; + private final TransportModeService transportModeService; + private final JobMetadataRepository metadataRepository; + + public TransportModeRecalculationTask(TripJdbcService tripJdbcService, RawLocationPointJdbcService rawLocationPointJdbcService, TransportModeService transportModeService, JobMetadataRepository metadataRepository) { + this.tripJdbcService = tripJdbcService; + this.rawLocationPointJdbcService = rawLocationPointJdbcService; + this.transportModeService = transportModeService; + this.metadataRepository = metadataRepository; + } + + public void execute(TaskData taskData) { + User user = taskData.user; + long allTripsAmountForUser = this.tripJdbcService.count(taskData.user); + metadataRepository.updateProgress(taskData.getJobId(), 0, allTripsAmountForUser, "Updating trips"); + AtomicLong currentTrip = new AtomicLong(); + tripJdbcService.findByUser(user).forEach(trip -> { + Instant startTime = trip.getStartTime(); + Instant endTime = trip.getEndTime(); + List tripPoints = this.rawLocationPointJdbcService.findByUserAndTimestampBetweenOrderByTimestampAsc(user, startTime, endTime.plus(1, ChronoUnit.MILLIS)); + TransportMode transportMode = this.transportModeService.inferTransportMode(user, tripPoints, startTime, endTime); + if (transportMode != trip.getTransportModeInferred()) { + log.trace("Reclassified trip {} from {} to {} to mode {}", trip.getId(), startTime, endTime, transportMode); + trip = trip.withTransportMode(transportMode); + this.tripJdbcService.update(trip); + } + if (currentTrip.getAndIncrement() % 100 == 0) { + metadataRepository.updateProgress(taskData.getJobId(), currentTrip.get(), allTripsAmountForUser, "Updating trips"); + } + }); + } + public static class TaskData extends JobContext { + + public final User user; + + public TaskData(User user) { + this.user = user; + } + + private TaskData(User user, UUID jobId, UUID parentJobId) { + super(jobId, parentJobId); + this.user = user; + } + + @Override + public TaskData withJobId(UUID jobId) { + return new TaskData(user, jobId, parentJobId); + } + + @Override + public TaskData withParentJobId(UUID parentJobId) { + return new TaskData(user, jobId, parentJobId); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index edb3115c7..17fd3e9c2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -67,6 +67,8 @@ reitti.import.grace-time-seconds=30 reitti.jobs.cleanup.cron=0 0 4 * * ? reitti.jobs.cleanup.max-age-hours=24 +reitti.db-janitor.schedule=0 0 4 * * ? + reitti.geo-point-filter.max-speed-kmh=1000 reitti.geo-point-filter.max-accuracy-meters=100 reitti.geo-point-filter.history-lookback-hours=24 diff --git a/src/main/resources/db/migration/V109__fix_nominatim_default_geocoder.sql b/src/main/resources/db/migration/V109__fix_nominatim_default_geocoder.sql new file mode 100644 index 000000000..bc6fb219f --- /dev/null +++ b/src/main/resources/db/migration/V109__fix_nominatim_default_geocoder.sql @@ -0,0 +1 @@ +UPDATE geocode_services SET priority = 3, type = 'NOMINATIM', url = 'https://nominatim.openstreetmap.org' WHERE url = 'https://nominatim.openstreetmap.org/reverse?format=geocodejson&lat={lat}&lon={lng}'; \ No newline at end of file diff --git a/src/main/resources/static/css/main.css b/src/main/resources/static/css/main.css index 0dbea1ee2..50bd2d8ba 100644 --- a/src/main/resources/static/css/main.css +++ b/src/main/resources/static/css/main.css @@ -111,6 +111,29 @@ --memories-header-height: 550px; --box-shadow: 0 8px 25px rgba(245, 222, 179, 0.2); --sans-serif-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Sans SC", 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; + + --bg: #0e1826; + --bg-deep: #0a1320; + --panel: rgba(22, 32, 48, 0.78); + --panel-solid: #162030; + --ink: #ece6d6; + --ink-dim: #a5a99f; + --ink-faint: #6b7285; + --gold: #d9a441; + --gold-bright: #f2c470; + --gold-deep: #b8862c; + --gold-glow: rgba(217, 164, 65, 0.55); + --sky: #8fc5e6; + --sky-dim: #5a8bb0; + --violet: #b89adc; + --slate: #7e8a9e; + --danger: #e07a6b; + + --drawer-h: 320px; + --drawer-inset: 16px; + --connector-h: 36px; + --panel-line: rgba(212, 175, 108, 0.14); + --drawer-top-edge: calc(var(--drawer-h) + var(--drawer-inset)); } /* CJK Language-specific font stacks */ @@ -1145,18 +1168,22 @@ tr:hover { flex-grow: 1; } +.timeline-container .aggregated-entry-container { + position: relative; + pointer-events: all; + left: 24px; +} .timeline-container .aggregated-entry { - border-radius: 8px; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(12px); - background-color: rgba(0, 0, 0, 0.15); - border: 1px solid rgba(0, 0, 0, 0.16); - box-shadow: var(--box-shadow); + -webkit-backdrop-filter: blur(14px); + background-color: rgba(0, 0, 0, 0.45); padding: 0; color: #fff; - left: 24px; margin-bottom: 24px; text-shadow: 0 0 0 white, 2px 2px 0 #24201c, 1px 1px 0 #171414; + backdrop-filter: blur(14px); + border: 1px solid var(--panel-line); + border-radius: 8px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.04); } diff --git a/src/main/resources/static/css/map.css b/src/main/resources/static/css/map.css index 96a02847d..1bde07266 100644 --- a/src/main/resources/static/css/map.css +++ b/src/main/resources/static/css/map.css @@ -176,7 +176,7 @@ details.maplibregl-ctrl-attrib[open] { backdrop-filter: blur(14px); border: 1px solid var(--panel-line); border-radius: 8px; - background: rgba(255, 255, 255, 0.02); + background: rgba(255, 255, 255, 0.6); box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.04); } diff --git a/src/main/resources/static/css/workbench.css b/src/main/resources/static/css/workbench.css index 86b3b210d..0d1265a11 100644 --- a/src/main/resources/static/css/workbench.css +++ b/src/main/resources/static/css/workbench.css @@ -1,29 +1,3 @@ -:root { - --bg: #0e1826; - --bg-deep: #0a1320; - --panel: rgba(22, 32, 48, 0.78); - --panel-solid: #162030; - --ink: #ece6d6; - --ink-dim: #a5a99f; - --ink-faint: #6b7285; - --gold: #d9a441; - --gold-bright: #f2c470; - --gold-deep: #b8862c; - --gold-glow: rgba(217, 164, 65, 0.55); - --sky: #8fc5e6; - --sky-dim: #5a8bb0; - --violet: #b89adc; - --slate: #7e8a9e; - --danger: #e07a6b; - - --drawer-h: 320px; - --drawer-inset: 16px; - --connector-h: 36px; - --panel-line: rgba(212, 175, 108, 0.14); - --drawer-top-edge: calc(var(--drawer-h) + var(--drawer-inset)); - -} - #drawer .device-select { border-color: var(--color-highlight); } diff --git a/src/main/resources/templates/fragments/map-styles.html b/src/main/resources/templates/fragments/map-styles.html deleted file mode 100644 index 75643970f..000000000 --- a/src/main/resources/templates/fragments/map-styles.html +++ /dev/null @@ -1,355 +0,0 @@ - - - - - -
- - -
- - -
- -
-
-
- -
-
-

Custom Styles

- -
- -
- -
-
-
- - -
-
No custom styles yet
-
-
-
Style name
-
- Vector - · - Tile Template - URL - · Shared - · Proxied -
-
-
- - -
-
-
- - -
- - -
- Edit Custom Style - Add Custom Style -
- -
- - -
- -
- -
-
- -

Tile requests for this style are fetched through Reitti.

-
- -
-
-
- -

Other users can select this style, but only you can edit it.

-
- -
- - - -
- - -
-
- - -
-
- Map Type -
- - -
-
- - -
-
- - -
- -
- -
- Advanced Options -
- - -
-
- - -
-
- - -
-
-
- - -
-
- Style Input -
- - -
-
- - -
- - -
-
- - -
-
- - -
- -
- -
- Tile Settings -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
-
- - -
-
- Source Input -
- - -
-
- - -
- - -
-
- - -
-
- - -
-
- - -
-
- - - diff --git a/src/main/resources/templates/fragments/timeline.html b/src/main/resources/templates/fragments/timeline.html index 7c93d979b..f9321f48b 100644 --- a/src/main/resources/templates/fragments/timeline.html +++ b/src/main/resources/templates/fragments/timeline.html @@ -80,270 +80,273 @@ -
- -
-
-
-
Activity Overview
- +
+
+ -
-
-
-
Visited Places
-
1
+
+
+
+
Activity Overview
+
-
-
-
Journeys
-
1
+
+
+
+
Visited Places
+
1
+
+
+
+
Journeys
+
1
+
-
-
- -
- -
+ }() + +
+
Color Theme

Enable or disable this device for tracking.

- +