Reverse Lookup for activities
This commit is contained in:
parent
612d67ccda
commit
debc55d484
22 changed files with 347 additions and 94 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -45,4 +45,6 @@ build/
|
||||||
|
|
||||||
### Application Files ###
|
### Application Files ###
|
||||||
uploads/
|
uploads/
|
||||||
logs/
|
logs/
|
||||||
|
/gadm_410.gpkg
|
||||||
|
/.postgresdata/
|
||||||
|
|
|
||||||
17
.idea/dataSources.xml
generated
Normal file
17
.idea/dataSources.xml
generated
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="testdb@localhost" uuid="2564811a-81f9-4d83-b1b1-04cb2763e3fa">
|
||||||
|
<driver-ref>postgresql</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:postgresql://localhost:51826/testdb</jdbc-url>
|
||||||
|
<jdbc-additional-properties>
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||||
|
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||||
|
</jdbc-additional-properties>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/data_source_mapping.xml
generated
Normal file
7
.idea/data_source_mapping.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourcePerFileMappings">
|
||||||
|
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/2564811a-81f9-4d83-b1b1-04cb2763e3fa/console.sql" value="2564811a-81f9-4d83-b1b1-04cb2763e3fa" />
|
||||||
|
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/2564811a-81f9-4d83-b1b1-04cb2763e3fa/console_1.sql" value="2564811a-81f9-4d83-b1b1-04cb2763e3fa" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/sqldialects.xml
generated
Normal file
7
.idea/sqldialects.xml
generated
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources/db/migration/V23__add_gadm_table.sql" dialect="PostgreSQL" />
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/resources/db/migration/V24__add_location_to_activity.sql" dialect="PostgreSQL" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
1
ogr.sh
Executable file
1
ogr.sh
Executable file
|
|
@ -0,0 +1 @@
|
||||||
|
ogr2ogr -nln public.gadm_410 -lco GEOMETRY_NAME=geom -lco FID=fid -f PostgreSQL PG:"host=localhost port=49812 dbname=testdb user=test password=test" -progress gadm_410.gpkg
|
||||||
|
|
@ -46,6 +46,7 @@ public class ActivityDTO {
|
||||||
private ActivityMetricsDTO metrics;
|
private ActivityMetricsDTO metrics;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
private String activityLocation;
|
||||||
|
|
||||||
// Map rendering data
|
// Map rendering data
|
||||||
private Map<String, Object> simplifiedTrack; // GeoJSON LineString
|
private Map<String, Object> simplifiedTrack; // GeoJSON LineString
|
||||||
|
|
@ -124,7 +125,8 @@ public class ActivityDTO {
|
||||||
.elevationGain(activity.getElevationGain())
|
.elevationGain(activity.getElevationGain())
|
||||||
.elevationLoss(activity.getElevationLoss())
|
.elevationLoss(activity.getElevationLoss())
|
||||||
.createdAt(activity.getCreatedAt())
|
.createdAt(activity.getCreatedAt())
|
||||||
.updatedAt(activity.getUpdatedAt());
|
.updatedAt(activity.getUpdatedAt())
|
||||||
|
.activityLocation(activity.getActivityLocation());
|
||||||
|
|
||||||
if (activity.getTotalDurationSeconds() != null) {
|
if (activity.getTotalDurationSeconds() != null) {
|
||||||
builder.totalDurationSeconds(activity.getTotalDurationSeconds());
|
builder.totalDurationSeconds(activity.getTotalDurationSeconds());
|
||||||
|
|
@ -237,6 +239,7 @@ public class ActivityDTO {
|
||||||
.subSport(activity.getSubSport())
|
.subSport(activity.getSubSport())
|
||||||
.indoorDetectionMethod(activity.getIndoorDetectionMethod())
|
.indoorDetectionMethod(activity.getIndoorDetectionMethod())
|
||||||
.race(activity.getRace() != null ? activity.getRace() : false)
|
.race(activity.getRace() != null ? activity.getRace() : false)
|
||||||
|
.activityLocation(activity.getActivityLocation())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -255,7 +258,8 @@ public class ActivityDTO {
|
||||||
.elevationGain(activity.getElevationGain())
|
.elevationGain(activity.getElevationGain())
|
||||||
.elevationLoss(activity.getElevationLoss())
|
.elevationLoss(activity.getElevationLoss())
|
||||||
.createdAt(activity.getCreatedAt())
|
.createdAt(activity.getCreatedAt())
|
||||||
.updatedAt(activity.getUpdatedAt());
|
.updatedAt(activity.getUpdatedAt())
|
||||||
|
.activityLocation(activity.getActivityLocation());
|
||||||
|
|
||||||
if (activity.getTotalDurationSeconds() != null) {
|
if (activity.getTotalDurationSeconds() != null) {
|
||||||
builder.totalDurationSeconds(activity.getTotalDurationSeconds());
|
builder.totalDurationSeconds(activity.getTotalDurationSeconds());
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ public class TimelineActivityDTO {
|
||||||
private Double elevationLoss;
|
private Double elevationLoss;
|
||||||
private String visibility;
|
private String visibility;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
|
private String activityLocation;
|
||||||
|
|
||||||
// User information
|
// User information
|
||||||
private String username;
|
private String username;
|
||||||
|
|
@ -99,6 +100,7 @@ public class TimelineActivityDTO {
|
||||||
.indoorDetectionMethod(activity.getIndoorDetectionMethod())
|
.indoorDetectionMethod(activity.getIndoorDetectionMethod())
|
||||||
.race(activity.getRace() != null ? activity.getRace() : false)
|
.race(activity.getRace() != null ? activity.getRace() : false)
|
||||||
.metrics(activity.getMetrics() != null ? ActivityMetricsSummary.fromMetrics(activity.getMetrics()) : null)
|
.metrics(activity.getMetrics() != null ? ActivityMetricsSummary.fromMetrics(activity.getMetrics()) : null)
|
||||||
|
.activityLocation(activity.getActivityLocation())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
package net.javahippie.fitpub.model.entity;
|
package net.javahippie.fitpub.model.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
|
import io.hypersistence.utils.hibernate.type.json.JsonBinaryType;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
import org.hibernate.annotations.UpdateTimestamp;
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
import org.locationtech.jts.geom.LineString;
|
import org.locationtech.jts.geom.LineString;
|
||||||
|
|
||||||
|
import javax.sound.midi.Track;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,6 +34,7 @@ import java.util.UUID;
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Builder
|
@Builder
|
||||||
|
@Slf4j
|
||||||
public class Activity {
|
public class Activity {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
|
@ -93,6 +100,9 @@ public class Activity {
|
||||||
@Column(name = "elevation_loss", precision = 8, scale = 2)
|
@Column(name = "elevation_loss", precision = 8, scale = 2)
|
||||||
private BigDecimal elevationLoss;
|
private BigDecimal elevationLoss;
|
||||||
|
|
||||||
|
@Column(name = "activity_location")
|
||||||
|
private String activityLocation;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Original activity file (FIT or GPX) for re-processing if needed.
|
* Original activity file (FIT or GPX) for re-processing if needed.
|
||||||
* Allows us to re-parse with updated algorithms.
|
* Allows us to re-parse with updated algorithms.
|
||||||
|
|
@ -149,6 +159,42 @@ public class Activity {
|
||||||
@Column(name = "updated_at", nullable = false)
|
@Column(name = "updated_at", nullable = false)
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
public Optional<TrackPoint> findFirstTrackpoint() throws JsonProcessingException {
|
||||||
|
// Get first track point for location
|
||||||
|
JsonNode trackPoints = new ObjectMapper().readTree(this.getTrackPointsJson());
|
||||||
|
log.debug("Parsed track points, is array: {}, size: {}",
|
||||||
|
trackPoints.isArray(), trackPoints.isArray() ? trackPoints.size() : "N/A");
|
||||||
|
|
||||||
|
if (!trackPoints.isArray() || trackPoints.isEmpty()) {
|
||||||
|
log.warn("Track points is not an array or is empty for activity {}", this.getId());
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonNode firstPoint = trackPoints.get(0);
|
||||||
|
log.debug("First track point JSON: {}", firstPoint.toString());
|
||||||
|
|
||||||
|
// Check if lat/lon fields exist (support both "lat"/"lon" and "latitude"/"longitude")
|
||||||
|
boolean hasLat = firstPoint.has("lat") || firstPoint.has("latitude");
|
||||||
|
boolean hasLon = firstPoint.has("lon") || firstPoint.has("longitude");
|
||||||
|
|
||||||
|
if (!hasLat || !hasLon) {
|
||||||
|
// Collect field names from iterator
|
||||||
|
java.util.List<String> fieldNames = new java.util.ArrayList<>();
|
||||||
|
firstPoint.fieldNames().forEachRemaining(fieldNames::add);
|
||||||
|
|
||||||
|
log.error("First track point MISSING lat/lon fields for activity {}.", this.getId());
|
||||||
|
log.error("Available fields in track point: {}", fieldNames);
|
||||||
|
log.error("First track point content: {}", firstPoint);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract coordinates (try both short and long field names)
|
||||||
|
double lat = firstPoint.has("lat") ? firstPoint.get("lat").asDouble() : firstPoint.get("latitude").asDouble();
|
||||||
|
double lon = firstPoint.has("lon") ? firstPoint.get("lon").asDouble() : firstPoint.get("longitude").asDouble();
|
||||||
|
log.debug("Extracted location from first track point: lat={}, lon={}", lat, lon);
|
||||||
|
return Optional.of(new TrackPoint(lon, lat));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to set metrics for this activity
|
* Helper method to set metrics for this activity
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
package net.javahippie.fitpub.model.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Index;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "gadm_410", indexes = {
|
||||||
|
@Index(name = "gadm_410_pkey", columnList = "fid", unique = true),
|
||||||
|
@Index(name = "gadm_410_geom_geom_idx", columnList = "geom", unique = true)
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class ReverseGeolocation {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "fid", nullable = false)
|
||||||
|
private Integer id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Country
|
||||||
|
*/
|
||||||
|
@Column(name = "name_0")
|
||||||
|
private String name_0;
|
||||||
|
|
||||||
|
@Column(name = "name_1")
|
||||||
|
private String name_1;
|
||||||
|
|
||||||
|
@Column(name = "name_2")
|
||||||
|
private String name_2;
|
||||||
|
|
||||||
|
@Column(name = "name_3")
|
||||||
|
private String name_3;
|
||||||
|
|
||||||
|
@Column(name = "name_4")
|
||||||
|
private String name_4;
|
||||||
|
|
||||||
|
public String formatWithHighestResolution() {
|
||||||
|
String country = name_0;
|
||||||
|
|
||||||
|
if(containsText(name_4)) {
|
||||||
|
return "%s, %s".formatted(name_4, country);
|
||||||
|
}
|
||||||
|
if(containsText(name_3)) {
|
||||||
|
return "%s, %s".formatted(name_3, country);
|
||||||
|
}
|
||||||
|
if(containsText(name_2)) {
|
||||||
|
return "%s, %s".formatted(name_2, country);
|
||||||
|
}
|
||||||
|
if(containsText(name_1)) {
|
||||||
|
return "%s, %s".formatted(name_1, country);
|
||||||
|
}
|
||||||
|
|
||||||
|
return country;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean containsText(String val) {
|
||||||
|
if(null == val) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var trimmedValue = val.trim();
|
||||||
|
return !trimmedValue.isBlank();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
package net.javahippie.fitpub.model.entity;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
public record TrackPoint(double lon, double lat) {
|
||||||
|
}
|
||||||
|
|
@ -234,7 +234,8 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
u.username, u.display_name, u.avatar_url,
|
u.username, u.display_name, u.avatar_url,
|
||||||
COUNT(DISTINCT l.id) AS likes_count,
|
COUNT(DISTINCT l.id) AS likes_count,
|
||||||
COUNT(DISTINCT c.id) AS comments_count,
|
COUNT(DISTINCT c.id) AS comments_count,
|
||||||
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user
|
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user,
|
||||||
|
a.activity_location
|
||||||
FROM activities a
|
FROM activities a
|
||||||
INNER JOIN users u ON a.user_id = u.id
|
INNER JOIN users u ON a.user_id = u.id
|
||||||
LEFT JOIN likes l ON a.id = l.activity_id
|
LEFT JOIN likes l ON a.id = l.activity_id
|
||||||
|
|
@ -268,7 +269,8 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
u.username, u.display_name, u.avatar_url,
|
u.username, u.display_name, u.avatar_url,
|
||||||
COUNT(DISTINCT l.id) AS likes_count,
|
COUNT(DISTINCT l.id) AS likes_count,
|
||||||
COUNT(DISTINCT c.id) AS comments_count,
|
COUNT(DISTINCT c.id) AS comments_count,
|
||||||
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user
|
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user,
|
||||||
|
a.activity_location
|
||||||
FROM activities a
|
FROM activities a
|
||||||
INNER JOIN users u ON a.user_id = u.id
|
INNER JOIN users u ON a.user_id = u.id
|
||||||
LEFT JOIN likes l ON a.id = l.activity_id
|
LEFT JOIN likes l ON a.id = l.activity_id
|
||||||
|
|
@ -303,7 +305,8 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
u.username, u.display_name, u.avatar_url,
|
u.username, u.display_name, u.avatar_url,
|
||||||
COUNT(DISTINCT l.id) AS likes_count,
|
COUNT(DISTINCT l.id) AS likes_count,
|
||||||
COUNT(DISTINCT c.id) AS comments_count,
|
COUNT(DISTINCT c.id) AS comments_count,
|
||||||
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user
|
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user,
|
||||||
|
a.activity_location
|
||||||
FROM activities a
|
FROM activities a
|
||||||
INNER JOIN users u ON a.user_id = u.id
|
INNER JOIN users u ON a.user_id = u.id
|
||||||
LEFT JOIN likes l ON a.id = l.activity_id
|
LEFT JOIN likes l ON a.id = l.activity_id
|
||||||
|
|
@ -350,7 +353,8 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
u.username, u.display_name, u.avatar_url,
|
u.username, u.display_name, u.avatar_url,
|
||||||
COUNT(DISTINCT l.id) AS likes_count,
|
COUNT(DISTINCT l.id) AS likes_count,
|
||||||
COUNT(DISTINCT c.id) AS comments_count,
|
COUNT(DISTINCT c.id) AS comments_count,
|
||||||
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user
|
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user,
|
||||||
|
a.activity_location
|
||||||
FROM activities a
|
FROM activities a
|
||||||
INNER JOIN users u ON a.user_id = u.id
|
INNER JOIN users u ON a.user_id = u.id
|
||||||
LEFT JOIN likes l ON a.id = l.activity_id
|
LEFT JOIN likes l ON a.id = l.activity_id
|
||||||
|
|
@ -360,6 +364,7 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
AND (:searchText IS NULL OR (
|
AND (:searchText IS NULL OR (
|
||||||
LOWER(a.title) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
LOWER(a.title) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
OR LOWER(a.description) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
OR LOWER(a.description) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
|
OR LOWER(a.activity_location) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
))
|
))
|
||||||
GROUP BY a.id, a.user_id, a.activity_type, a.title, a.description, a.started_at, a.ended_at,
|
GROUP BY a.id, a.user_id, a.activity_type, a.title, a.description, a.started_at, a.ended_at,
|
||||||
a.timezone, a.visibility, a.total_distance, a.total_duration_seconds, a.elevation_gain, a.elevation_loss,
|
a.timezone, a.visibility, a.total_distance, a.total_duration_seconds, a.elevation_gain, a.elevation_loss,
|
||||||
|
|
@ -390,7 +395,8 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
u.username, u.display_name, u.avatar_url,
|
u.username, u.display_name, u.avatar_url,
|
||||||
COUNT(DISTINCT l.id) AS likes_count,
|
COUNT(DISTINCT l.id) AS likes_count,
|
||||||
COUNT(DISTINCT c.id) AS comments_count,
|
COUNT(DISTINCT c.id) AS comments_count,
|
||||||
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user
|
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user,
|
||||||
|
a.activity_location
|
||||||
FROM activities a
|
FROM activities a
|
||||||
INNER JOIN users u ON a.user_id = u.id
|
INNER JOIN users u ON a.user_id = u.id
|
||||||
LEFT JOIN likes l ON a.id = l.activity_id
|
LEFT JOIN likes l ON a.id = l.activity_id
|
||||||
|
|
@ -400,6 +406,7 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
AND (:searchText IS NULL OR (
|
AND (:searchText IS NULL OR (
|
||||||
LOWER(a.title) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
LOWER(a.title) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
OR LOWER(a.description) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
OR LOWER(a.description) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
|
OR LOWER(a.activity_location) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
))
|
))
|
||||||
GROUP BY a.id, a.user_id, a.activity_type, a.title, a.description, a.started_at, a.ended_at,
|
GROUP BY a.id, a.user_id, a.activity_type, a.title, a.description, a.started_at, a.ended_at,
|
||||||
a.timezone, a.visibility, a.total_distance, a.total_duration_seconds, a.elevation_gain, a.elevation_loss,
|
a.timezone, a.visibility, a.total_distance, a.total_duration_seconds, a.elevation_gain, a.elevation_loss,
|
||||||
|
|
@ -431,7 +438,8 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
u.username, u.display_name, u.avatar_url,
|
u.username, u.display_name, u.avatar_url,
|
||||||
COUNT(DISTINCT l.id) AS likes_count,
|
COUNT(DISTINCT l.id) AS likes_count,
|
||||||
COUNT(DISTINCT c.id) AS comments_count,
|
COUNT(DISTINCT c.id) AS comments_count,
|
||||||
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user
|
CASE WHEN ul.id IS NOT NULL THEN true ELSE false END AS liked_by_current_user,
|
||||||
|
a.activity_location
|
||||||
FROM activities a
|
FROM activities a
|
||||||
INNER JOIN users u ON a.user_id = u.id
|
INNER JOIN users u ON a.user_id = u.id
|
||||||
LEFT JOIN likes l ON a.id = l.activity_id
|
LEFT JOIN likes l ON a.id = l.activity_id
|
||||||
|
|
@ -442,6 +450,7 @@ public interface ActivityRepository extends JpaRepository<Activity, UUID> {
|
||||||
AND (:searchText IS NULL OR (
|
AND (:searchText IS NULL OR (
|
||||||
LOWER(a.title) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
LOWER(a.title) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
OR LOWER(a.description) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
OR LOWER(a.description) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
|
OR LOWER(a.activity_location) LIKE LOWER(CONCAT('%', :searchText, '%'))
|
||||||
))
|
))
|
||||||
GROUP BY a.id, a.user_id, a.activity_type, a.title, a.description, a.started_at, a.ended_at,
|
GROUP BY a.id, a.user_id, a.activity_type, a.title, a.description, a.started_at, a.ended_at,
|
||||||
a.timezone, a.visibility, a.total_distance, a.total_duration_seconds, a.elevation_gain, a.elevation_loss,
|
a.timezone, a.visibility, a.total_distance, a.total_duration_seconds, a.elevation_gain, a.elevation_loss,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.javahippie.fitpub.repository;
|
||||||
|
|
||||||
|
import net.javahippie.fitpub.model.entity.ReverseGeolocation;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface ReverseGeolocationRepository extends JpaRepository<ReverseGeolocation, Integer> {
|
||||||
|
|
||||||
|
@Query(value = """
|
||||||
|
SELECT fid, name_0, name_1, name_2, name_3, name_4, name_5
|
||||||
|
FROM gadm_410
|
||||||
|
WHERE ST_Intersects(ST_SetSRID(ST_Point(:lon, :lat), 4326), geom)
|
||||||
|
""",
|
||||||
|
nativeQuery = true)
|
||||||
|
ReverseGeolocation findForLocation(double lon, double lat);
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.javahippie.fitpub.repository.ReverseGeolocationRepository;
|
||||||
import net.javahippie.fitpub.util.ActivityFormatter;
|
import net.javahippie.fitpub.util.ActivityFormatter;
|
||||||
import net.javahippie.fitpub.util.FitFileValidator;
|
import net.javahippie.fitpub.util.FitFileValidator;
|
||||||
import net.javahippie.fitpub.util.FitParser;
|
import net.javahippie.fitpub.util.FitParser;
|
||||||
|
|
@ -113,6 +114,7 @@ public class ActivityFileService {
|
||||||
private final AchievementService achievementService;
|
private final AchievementService achievementService;
|
||||||
private final TrainingLoadService trainingLoadService;
|
private final TrainingLoadService trainingLoadService;
|
||||||
private final ActivitySummaryService activitySummaryService;
|
private final ActivitySummaryService activitySummaryService;
|
||||||
|
private final ReverseGeolocationRepository reverseGeolocationRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes an uploaded activity file (FIT or GPX) and creates an activity.
|
* Processes an uploaded activity file (FIT or GPX) and creates an activity.
|
||||||
|
|
@ -243,7 +245,7 @@ public class ActivityFileService {
|
||||||
Activity.Visibility visibility,
|
Activity.Visibility visibility,
|
||||||
byte[] rawFile,
|
byte[] rawFile,
|
||||||
ProcessingOptions options
|
ProcessingOptions options
|
||||||
) {
|
) throws JsonProcessingException {
|
||||||
// Generate title if not provided
|
// Generate title if not provided
|
||||||
String activityTitle = title != null && !title.isBlank()
|
String activityTitle = title != null && !title.isBlank()
|
||||||
? title
|
? title
|
||||||
|
|
@ -299,6 +301,11 @@ public class ActivityFileService {
|
||||||
activity.setMetrics(metrics);
|
activity.setMetrics(metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var res = activity.findFirstTrackpoint()
|
||||||
|
.map(tp -> reverseGeolocationRepository.findForLocation(tp.lon(), tp.lat()));
|
||||||
|
|
||||||
|
res.ifPresent(reverseGeolocation -> activity.setActivityLocation(reverseGeolocation.formatWithHighestResolution()));
|
||||||
|
|
||||||
// Save activity (single INSERT instead of 855!)
|
// Save activity (single INSERT instead of 855!)
|
||||||
Activity savedActivity = activityRepository.save(activity);
|
Activity savedActivity = activityRepository.save(activity);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,7 @@ public class TimelineResultMapper {
|
||||||
Long likesCount = ((Number) result[idx++]).longValue();
|
Long likesCount = ((Number) result[idx++]).longValue();
|
||||||
Long commentsCount = ((Number) result[idx++]).longValue();
|
Long commentsCount = ((Number) result[idx++]).longValue();
|
||||||
Boolean likedByCurrentUser = (Boolean) result[idx++];
|
Boolean likedByCurrentUser = (Boolean) result[idx++];
|
||||||
|
String activityLocation = (String) result[idx++];
|
||||||
|
|
||||||
// Build DTO
|
// Build DTO
|
||||||
return TimelineActivityDTO.builder()
|
return TimelineActivityDTO.builder()
|
||||||
|
|
@ -97,6 +98,7 @@ public class TimelineResultMapper {
|
||||||
.commentsCount(commentsCount)
|
.commentsCount(commentsCount)
|
||||||
.likedByCurrentUser(likedByCurrentUser)
|
.likedByCurrentUser(likedByCurrentUser)
|
||||||
.hasGpsTrack(true) // Will be refined based on actual data
|
.hasGpsTrack(true) // Will be refined based on actual data
|
||||||
|
.activityLocation(activityLocation)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
package net.javahippie.fitpub.service;
|
package net.javahippie.fitpub.service;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import net.javahippie.fitpub.model.entity.Activity;
|
import net.javahippie.fitpub.model.entity.Activity;
|
||||||
|
import net.javahippie.fitpub.model.entity.TrackPoint;
|
||||||
import net.javahippie.fitpub.model.entity.WeatherData;
|
import net.javahippie.fitpub.model.entity.WeatherData;
|
||||||
import net.javahippie.fitpub.repository.WeatherDataRepository;
|
import net.javahippie.fitpub.repository.WeatherDataRepository;
|
||||||
|
import org.locationtech.jts.geom.Point;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
@ -53,8 +56,8 @@ public class WeatherService {
|
||||||
public Optional<WeatherData> fetchWeatherForActivity(Activity activity) {
|
public Optional<WeatherData> fetchWeatherForActivity(Activity activity) {
|
||||||
log.info("=== Weather fetch requested for activity {} ===", activity.getId());
|
log.info("=== Weather fetch requested for activity {} ===", activity.getId());
|
||||||
log.info("Weather configuration: enabled={}, API key configured={}, API key length={}",
|
log.info("Weather configuration: enabled={}, API key configured={}, API key length={}",
|
||||||
weatherEnabled, (apiKey != null && !apiKey.isBlank()),
|
weatherEnabled, (apiKey != null && !apiKey.isBlank()),
|
||||||
(apiKey != null ? apiKey.length() : 0));
|
(apiKey != null ? apiKey.length() : 0));
|
||||||
|
|
||||||
if (!weatherEnabled) {
|
if (!weatherEnabled) {
|
||||||
log.warn("Weather fetching is DISABLED in configuration (fitpub.weather.enabled=false). " +
|
log.warn("Weather fetching is DISABLED in configuration (fitpub.weather.enabled=false). " +
|
||||||
|
|
@ -64,12 +67,12 @@ public class WeatherService {
|
||||||
|
|
||||||
if (apiKey == null || apiKey.isBlank()) {
|
if (apiKey == null || apiKey.isBlank()) {
|
||||||
log.error("Weather API key is NOT CONFIGURED (fitpub.weather.api-key is empty). " +
|
log.error("Weather API key is NOT CONFIGURED (fitpub.weather.api-key is empty). " +
|
||||||
"Please set fitpub.weather.api-key in application properties.");
|
"Please set fitpub.weather.api-key in application properties.");
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Weather API key present: length={} chars, first 4 chars={}...",
|
log.debug("Weather API key present: length={} chars, first 4 chars={}...",
|
||||||
apiKey.length(), apiKey.length() > 4 ? apiKey.substring(0, 4) : "???");
|
apiKey.length(), apiKey.length() > 4 ? apiKey.substring(0, 4) : "???");
|
||||||
|
|
||||||
// Check if weather data already exists
|
// Check if weather data already exists
|
||||||
if (weatherDataRepository.existsByActivityId(activity.getId())) {
|
if (weatherDataRepository.existsByActivityId(activity.getId())) {
|
||||||
|
|
@ -86,73 +89,46 @@ public class WeatherService {
|
||||||
log.debug("Track points JSON length: {} chars", activity.getTrackPointsJson().length());
|
log.debug("Track points JSON length: {} chars", activity.getTrackPointsJson().length());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get first track point for location
|
Optional<TrackPoint> trackPoint = activity.findFirstTrackpoint();
|
||||||
JsonNode trackPoints = objectMapper.readTree(activity.getTrackPointsJson());
|
|
||||||
log.debug("Parsed track points, is array: {}, size: {}",
|
|
||||||
trackPoints.isArray(), trackPoints.isArray() ? trackPoints.size() : "N/A");
|
|
||||||
|
|
||||||
if (!trackPoints.isArray() || trackPoints.isEmpty()) {
|
if (trackPoint.isEmpty()) {
|
||||||
log.warn("Track points is not an array or is empty for activity {}", activity.getId());
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
|
||||||
|
|
||||||
JsonNode firstPoint = trackPoints.get(0);
|
|
||||||
log.info("First track point JSON: {}", firstPoint.toString());
|
|
||||||
|
|
||||||
// Check if lat/lon fields exist (support both "lat"/"lon" and "latitude"/"longitude")
|
|
||||||
boolean hasLat = firstPoint.has("lat") || firstPoint.has("latitude");
|
|
||||||
boolean hasLon = firstPoint.has("lon") || firstPoint.has("longitude");
|
|
||||||
|
|
||||||
if (!hasLat || !hasLon) {
|
|
||||||
// Collect field names from iterator
|
|
||||||
java.util.List<String> fieldNames = new java.util.ArrayList<>();
|
|
||||||
firstPoint.fieldNames().forEachRemaining(fieldNames::add);
|
|
||||||
|
|
||||||
log.error("First track point MISSING lat/lon fields for activity {}.", activity.getId());
|
|
||||||
log.error("Available fields in track point: {}", fieldNames);
|
|
||||||
log.error("First track point content: {}", firstPoint.toString());
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract coordinates (try both short and long field names)
|
|
||||||
double lat = firstPoint.has("lat") ? firstPoint.get("lat").asDouble() : firstPoint.get("latitude").asDouble();
|
|
||||||
double lon = firstPoint.has("lon") ? firstPoint.get("lon").asDouble() : firstPoint.get("longitude").asDouble();
|
|
||||||
log.info("Extracted location from first track point: lat={}, lon={}", lat, lon);
|
|
||||||
|
|
||||||
// Check if activity is recent (within 5 days) - use current weather API
|
|
||||||
// Otherwise use historical data API (requires paid plan)
|
|
||||||
long activityTimestamp = activity.getStartedAt().atZone(ZoneId.systemDefault()).toEpochSecond();
|
|
||||||
long currentTimestamp = Instant.now().getEpochSecond();
|
|
||||||
long daysDifference = (currentTimestamp - activityTimestamp) / 86400;
|
|
||||||
|
|
||||||
log.info("Activity started at: {}, days ago: {}", activity.getStartedAt(), daysDifference);
|
|
||||||
|
|
||||||
WeatherData weatherData;
|
|
||||||
if (daysDifference <= 5) {
|
|
||||||
log.info("Activity is RECENT ({} days old, within 5 day threshold), fetching current weather from OpenWeatherMap", daysDifference);
|
|
||||||
weatherData = fetchCurrentWeather(lat, lon, activity.getId());
|
|
||||||
} else {
|
} else {
|
||||||
log.warn("Activity is {} days old (exceeds 5 day threshold). Historical weather data requires OpenWeatherMap paid API plan. Skipping weather fetch.", daysDifference);
|
var resolvedTrackPoint = trackPoint.get();
|
||||||
return Optional.empty();
|
// Check if activity is recent (within 5 days) because it's free to use. Don't call other timeframes, expensive.
|
||||||
}
|
long activityTimestamp = activity.getStartedAt().atZone(ZoneId.systemDefault()).toEpochSecond();
|
||||||
|
long currentTimestamp = Instant.now().getEpochSecond();
|
||||||
|
long daysDifference = (currentTimestamp - activityTimestamp) / 86400;
|
||||||
|
|
||||||
if (weatherData != null) {
|
log.info("Activity started at: {}, days ago: {}", activity.getStartedAt(), daysDifference);
|
||||||
log.info("Successfully fetched and parsed weather data. Attempting to save to database...");
|
|
||||||
try {
|
WeatherData weatherData;
|
||||||
WeatherData saved = weatherDataRepository.save(weatherData);
|
if (daysDifference <= 5) {
|
||||||
log.info("Weather data SUCCESSFULLY SAVED to database with ID: {}", saved.getId());
|
log.info("Activity is RECENT ({} days old, within 5 day threshold), fetching current weather from OpenWeatherMap", daysDifference);
|
||||||
return Optional.of(saved);
|
weatherData = fetchCurrentWeather(resolvedTrackPoint.lat(), resolvedTrackPoint.lon(), activity.getId());
|
||||||
} catch (Exception e) {
|
} else {
|
||||||
log.error("FAILED to save weather data to database: {}", e.getMessage(), e);
|
log.warn("Activity is {} days old (exceeds 5 day threshold). Historical weather data requires OpenWeatherMap paid API plan. Skipping weather fetch.", daysDifference);
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
log.error("Weather data fetch returned NULL - check API errors above");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (weatherData != null) {
|
||||||
|
log.info("Successfully fetched and parsed weather data. Attempting to save to database...");
|
||||||
|
try {
|
||||||
|
WeatherData saved = weatherDataRepository.save(weatherData);
|
||||||
|
log.info("Weather data SUCCESSFULLY SAVED to database with ID: {}", saved.getId());
|
||||||
|
return Optional.of(saved);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("FAILED to save weather data to database: {}", e.getMessage(), e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("Weather data fetch returned NULL - check API errors above");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("EXCEPTION while fetching weather data for activity {}: {}",
|
log.error("EXCEPTION while fetching weather data for activity {}: {}",
|
||||||
activity.getId(), e.getMessage(), e);
|
activity.getId(), e.getMessage(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
|
|
@ -185,7 +161,7 @@ public class WeatherService {
|
||||||
|
|
||||||
log.info("API response received: {} characters", response.length());
|
log.info("API response received: {} characters", response.length());
|
||||||
log.info("API response (first 300 chars): {}",
|
log.info("API response (first 300 chars): {}",
|
||||||
response.length() > 300 ? response.substring(0, 300) + "..." : response);
|
response.length() > 300 ? response.substring(0, 300) + "..." : response);
|
||||||
|
|
||||||
log.info("Parsing weather response JSON...");
|
log.info("Parsing weather response JSON...");
|
||||||
WeatherData weatherData = parseWeatherResponse(response, activityId);
|
WeatherData weatherData = parseWeatherResponse(response, activityId);
|
||||||
|
|
@ -194,13 +170,13 @@ public class WeatherService {
|
||||||
log.error("FAILED to parse weather response - see parsing errors above");
|
log.error("FAILED to parse weather response - see parsing errors above");
|
||||||
} else {
|
} else {
|
||||||
log.info("Successfully parsed weather data: temp={}°C, feels_like={}°C, condition='{}', description='{}', humidity={}%, pressure={} hPa, wind={} m/s",
|
log.info("Successfully parsed weather data: temp={}°C, feels_like={}°C, condition='{}', description='{}', humidity={}%, pressure={} hPa, wind={} m/s",
|
||||||
weatherData.getTemperatureCelsius(),
|
weatherData.getTemperatureCelsius(),
|
||||||
weatherData.getFeelsLikeCelsius(),
|
weatherData.getFeelsLikeCelsius(),
|
||||||
weatherData.getWeatherCondition(),
|
weatherData.getWeatherCondition(),
|
||||||
weatherData.getWeatherDescription(),
|
weatherData.getWeatherDescription(),
|
||||||
weatherData.getHumidity(),
|
weatherData.getHumidity(),
|
||||||
weatherData.getPressure(),
|
weatherData.getPressure(),
|
||||||
weatherData.getWindSpeedMps());
|
weatherData.getWindSpeedMps());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("=== fetchCurrentWeather END === success={}", (weatherData != null));
|
log.info("=== fetchCurrentWeather END === success={}", (weatherData != null));
|
||||||
|
|
@ -274,8 +250,8 @@ public class WeatherService {
|
||||||
weatherData.setHumidity(getInteger(main, "humidity"));
|
weatherData.setHumidity(getInteger(main, "humidity"));
|
||||||
weatherData.setPressure(getInteger(main, "pressure"));
|
weatherData.setPressure(getInteger(main, "pressure"));
|
||||||
log.debug("Extracted main data: temp={}, feels_like={}, humidity={}, pressure={}",
|
log.debug("Extracted main data: temp={}, feels_like={}, humidity={}, pressure={}",
|
||||||
weatherData.getTemperatureCelsius(), weatherData.getFeelsLikeCelsius(),
|
weatherData.getTemperatureCelsius(), weatherData.getFeelsLikeCelsius(),
|
||||||
weatherData.getHumidity(), weatherData.getPressure());
|
weatherData.getHumidity(), weatherData.getPressure());
|
||||||
} else {
|
} else {
|
||||||
log.warn("Response JSON does not contain 'main' section");
|
log.warn("Response JSON does not contain 'main' section");
|
||||||
}
|
}
|
||||||
|
|
@ -287,7 +263,7 @@ public class WeatherService {
|
||||||
weatherData.setWindSpeedMps(getBigDecimal(wind, "speed"));
|
weatherData.setWindSpeedMps(getBigDecimal(wind, "speed"));
|
||||||
weatherData.setWindDirection(getInteger(wind, "deg"));
|
weatherData.setWindDirection(getInteger(wind, "deg"));
|
||||||
log.debug("Extracted wind data: speed={} m/s, direction={} degrees",
|
log.debug("Extracted wind data: speed={} m/s, direction={} degrees",
|
||||||
weatherData.getWindSpeedMps(), weatherData.getWindDirection());
|
weatherData.getWindSpeedMps(), weatherData.getWindDirection());
|
||||||
} else {
|
} else {
|
||||||
log.debug("Response JSON does not contain 'wind' section");
|
log.debug("Response JSON does not contain 'wind' section");
|
||||||
}
|
}
|
||||||
|
|
@ -300,8 +276,8 @@ public class WeatherService {
|
||||||
weatherData.setWeatherDescription(getString(weather, "description"));
|
weatherData.setWeatherDescription(getString(weather, "description"));
|
||||||
weatherData.setWeatherIcon(getString(weather, "icon"));
|
weatherData.setWeatherIcon(getString(weather, "icon"));
|
||||||
log.debug("Extracted weather condition: main='{}', description='{}', icon='{}'",
|
log.debug("Extracted weather condition: main='{}', description='{}', icon='{}'",
|
||||||
weatherData.getWeatherCondition(), weatherData.getWeatherDescription(),
|
weatherData.getWeatherCondition(), weatherData.getWeatherDescription(),
|
||||||
weatherData.getWeatherIcon());
|
weatherData.getWeatherIcon());
|
||||||
} else {
|
} else {
|
||||||
log.warn("Response JSON does not contain valid 'weather' array");
|
log.warn("Response JSON does not contain valid 'weather' array");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
net.javahippie.fitpub: DEBUG
|
net.javahippie.fitpub: DEBUG
|
||||||
org.hibernate.SQL: DEBUG
|
|
||||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||||
org.springframework.security: DEBUG
|
org.springframework.security: DEBUG
|
||||||
org.springframework.web: DEBUG
|
org.springframework.web: DEBUG
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ logging:
|
||||||
level:
|
level:
|
||||||
root: INFO
|
root: INFO
|
||||||
net.javahippie.fitpub: DEBUG
|
net.javahippie.fitpub: DEBUG
|
||||||
org.hibernate.SQL: DEBUG
|
|
||||||
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
|
||||||
org.springframework.security: DEBUG
|
org.springframework.security: DEBUG
|
||||||
|
|
||||||
|
|
|
||||||
62
src/main/resources/db/migration/V23__add_gadm_table.sql
Normal file
62
src/main/resources/db/migration/V23__add_gadm_table.sql
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
create table gadm_410
|
||||||
|
(
|
||||||
|
fid serial
|
||||||
|
primary key,
|
||||||
|
uid bigint,
|
||||||
|
gid_0 varchar(10),
|
||||||
|
name_0 varchar(32),
|
||||||
|
varname_0 varchar(29),
|
||||||
|
gid_1 varchar(10),
|
||||||
|
name_1 varchar(34),
|
||||||
|
varname_1 varchar(82),
|
||||||
|
nl_name_1 varchar(87),
|
||||||
|
iso_1 varchar(10),
|
||||||
|
hasc_1 varchar(10),
|
||||||
|
cc_1 varchar(10),
|
||||||
|
type_1 varchar(32),
|
||||||
|
engtype_1 varchar(32),
|
||||||
|
validfr_1 varchar(15),
|
||||||
|
gid_2 varchar(12),
|
||||||
|
name_2 varchar(34),
|
||||||
|
varname_2 varchar(39),
|
||||||
|
nl_name_2 varchar(75),
|
||||||
|
hasc_2 varchar(15),
|
||||||
|
cc_2 varchar(12),
|
||||||
|
type_2 varchar(34),
|
||||||
|
engtype_2 varchar(32),
|
||||||
|
validfr_2 varchar(15),
|
||||||
|
gid_3 varchar(15),
|
||||||
|
name_3 varchar(38),
|
||||||
|
varname_3 varchar(44),
|
||||||
|
nl_name_3 varchar(56),
|
||||||
|
hasc_3 varchar(27),
|
||||||
|
cc_3 varchar(10),
|
||||||
|
type_3 varchar(32),
|
||||||
|
engtype_3 varchar(32),
|
||||||
|
validfr_3 varchar(10),
|
||||||
|
gid_4 varchar(18),
|
||||||
|
name_4 varchar(34),
|
||||||
|
varname_4 varchar(35),
|
||||||
|
cc_4 varchar(12),
|
||||||
|
type_4 varchar(29),
|
||||||
|
engtype_4 varchar(29),
|
||||||
|
validfr_4 varchar(10),
|
||||||
|
gid_5 varchar(19),
|
||||||
|
name_5 varchar(34),
|
||||||
|
cc_5 varchar(10),
|
||||||
|
type_5 varchar(22),
|
||||||
|
engtype_5 varchar(10),
|
||||||
|
governedby varchar(17),
|
||||||
|
sovereign varchar(32),
|
||||||
|
disputedby varchar(32),
|
||||||
|
region varchar(32),
|
||||||
|
varregion varchar(48),
|
||||||
|
country varchar(32),
|
||||||
|
continent varchar(13),
|
||||||
|
subcont varchar(10),
|
||||||
|
geom geometry(MultiPolygon, 4326)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index geodata_pkey_geom_geom_idx
|
||||||
|
on gadm_410 using gist (geom);
|
||||||
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
alter table activities
|
||||||
|
add column activity_location VARCHAR(255);
|
||||||
|
|
@ -141,7 +141,7 @@ const FitPubTimeline = {
|
||||||
@${this.escapeHtml(activity.username)}
|
@${this.escapeHtml(activity.username)}
|
||||||
</a>
|
</a>
|
||||||
${!activity.isLocal ? ' <span class="badge bg-info ms-1" title="Federated Activity"><i class="bi bi-globe2"></i> Remote</span>' : ''}
|
${!activity.isLocal ? ' <span class="badge bg-info ms-1" title="Federated Activity"><i class="bi bi-globe2"></i> Remote</span>' : ''}
|
||||||
• ${this.formatTimeAgo(activity.startedAt)}
|
• ${this.formatTimeAgo(activity.startedAt)} • ${activity.activityLocation}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,10 @@
|
||||||
<i class="bi bi-calendar"></i>
|
<i class="bi bi-calendar"></i>
|
||||||
<span id="activityDate"></span>
|
<span id="activityDate"></span>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="ms-2">
|
||||||
|
<i class="bi bi-geo-alt"></i>
|
||||||
|
<span id="activityLocation"></span>
|
||||||
|
</span>
|
||||||
<span class="ms-2" id="visibilityBadge">
|
<span class="ms-2" id="visibilityBadge">
|
||||||
<i class="bi bi-globe"></i>
|
<i class="bi bi-globe"></i>
|
||||||
<span id="activityVisibility"></span>
|
<span id="activityVisibility"></span>
|
||||||
|
|
@ -506,6 +510,7 @@
|
||||||
activity.startedAt,
|
activity.startedAt,
|
||||||
activity.timezone || 'UTC'
|
activity.timezone || 'UTC'
|
||||||
);
|
);
|
||||||
|
document.getElementById('activityLocation').textContent = activity.activityLocation;
|
||||||
document.getElementById('activityVisibility').textContent = activity.visibility;
|
document.getElementById('activityVisibility').textContent = activity.visibility;
|
||||||
|
|
||||||
// Visibility icon
|
// Visibility icon
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,11 @@ package net.javahippie.fitpub.config;
|
||||||
import org.springframework.boot.test.context.TestConfiguration;
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.testcontainers.containers.BindMode;
|
||||||
import org.testcontainers.containers.PostgreSQLContainer;
|
import org.testcontainers.containers.PostgreSQLContainer;
|
||||||
|
import org.testcontainers.containers.startupcheck.IsRunningStartupCheckStrategy;
|
||||||
|
import org.testcontainers.containers.startupcheck.StartupCheckStrategy;
|
||||||
|
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
|
||||||
import org.testcontainers.utility.DockerImageName;
|
import org.testcontainers.utility.DockerImageName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -16,17 +20,21 @@ public class TestcontainersConfiguration {
|
||||||
/**
|
/**
|
||||||
* PostgreSQL container with PostGIS extension for tests.
|
* PostgreSQL container with PostGIS extension for tests.
|
||||||
* PostGIS image is treated as a standard PostgreSQL container.
|
* PostGIS image is treated as a standard PostgreSQL container.
|
||||||
|
*
|
||||||
* @ServiceConnection automatically configures the datasource from this container.
|
* @ServiceConnection automatically configures the datasource from this container.
|
||||||
*/
|
*/
|
||||||
@Bean
|
@Bean
|
||||||
@ServiceConnection
|
@ServiceConnection
|
||||||
public PostgreSQLContainer<?> postgresContainer() {
|
public PostgreSQLContainer<?> postgresContainer() {
|
||||||
return new PostgreSQLContainer<>(
|
return new PostgreSQLContainer<>(
|
||||||
DockerImageName.parse("postgis/postgis:16-3.4")
|
DockerImageName.parse("postgis/postgis:16-3.4")
|
||||||
.asCompatibleSubstituteFor("postgres")
|
.asCompatibleSubstituteFor("postgres")
|
||||||
)
|
)
|
||||||
.withDatabaseName("testdb")
|
.withDatabaseName("testdb")
|
||||||
.withUsername("test")
|
.withUsername("test")
|
||||||
.withPassword("test");
|
.withPassword("test")
|
||||||
|
.waitingFor(new HostPortWaitStrategy())
|
||||||
|
.withReuse(true)
|
||||||
|
.withFileSystemBind(".postgresdata", "/var/lib/postgresql/data", BindMode.READ_WRITE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue