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 {
- 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();
- }
+ var resolvedTrackPoint = trackPoint.get();
+ // 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("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);
+ 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(resolvedTrackPoint.lat(), resolvedTrackPoint.lon(), activity.getId());
+ } else {
+ 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();
}
- } 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) {
log.error("EXCEPTION while fetching weather data for activity {}: {}",
- activity.getId(), e.getMessage(), e);
+ activity.getId(), e.getMessage(), e);
}
return Optional.empty();
@@ -185,7 +161,7 @@ public class WeatherService {
log.info("API response received: {} characters", response.length());
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...");
WeatherData weatherData = parseWeatherResponse(response, activityId);
@@ -194,13 +170,13 @@ public class WeatherService {
log.error("FAILED to parse weather response - see parsing errors above");
} else {
log.info("Successfully parsed weather data: temp={}°C, feels_like={}°C, condition='{}', description='{}', humidity={}%, pressure={} hPa, wind={} m/s",
- weatherData.getTemperatureCelsius(),
- weatherData.getFeelsLikeCelsius(),
- weatherData.getWeatherCondition(),
- weatherData.getWeatherDescription(),
- weatherData.getHumidity(),
- weatherData.getPressure(),
- weatherData.getWindSpeedMps());
+ weatherData.getTemperatureCelsius(),
+ weatherData.getFeelsLikeCelsius(),
+ weatherData.getWeatherCondition(),
+ weatherData.getWeatherDescription(),
+ weatherData.getHumidity(),
+ weatherData.getPressure(),
+ weatherData.getWindSpeedMps());
}
log.info("=== fetchCurrentWeather END === success={}", (weatherData != null));
@@ -274,8 +250,8 @@ public class WeatherService {
weatherData.setHumidity(getInteger(main, "humidity"));
weatherData.setPressure(getInteger(main, "pressure"));
log.debug("Extracted main data: temp={}, feels_like={}, humidity={}, pressure={}",
- weatherData.getTemperatureCelsius(), weatherData.getFeelsLikeCelsius(),
- weatherData.getHumidity(), weatherData.getPressure());
+ weatherData.getTemperatureCelsius(), weatherData.getFeelsLikeCelsius(),
+ weatherData.getHumidity(), weatherData.getPressure());
} else {
log.warn("Response JSON does not contain 'main' section");
}
@@ -287,7 +263,7 @@ public class WeatherService {
weatherData.setWindSpeedMps(getBigDecimal(wind, "speed"));
weatherData.setWindDirection(getInteger(wind, "deg"));
log.debug("Extracted wind data: speed={} m/s, direction={} degrees",
- weatherData.getWindSpeedMps(), weatherData.getWindDirection());
+ weatherData.getWindSpeedMps(), weatherData.getWindDirection());
} else {
log.debug("Response JSON does not contain 'wind' section");
}
@@ -300,8 +276,8 @@ public class WeatherService {
weatherData.setWeatherDescription(getString(weather, "description"));
weatherData.setWeatherIcon(getString(weather, "icon"));
log.debug("Extracted weather condition: main='{}', description='{}', icon='{}'",
- weatherData.getWeatherCondition(), weatherData.getWeatherDescription(),
- weatherData.getWeatherIcon());
+ weatherData.getWeatherCondition(), weatherData.getWeatherDescription(),
+ weatherData.getWeatherIcon());
} else {
log.warn("Response JSON does not contain valid 'weather' array");
}
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index a2a2705..7da0d67 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -49,7 +49,6 @@ logging:
level:
root: INFO
net.javahippie.fitpub: DEBUG
- org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.springframework.security: DEBUG
org.springframework.web: DEBUG
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index baf1721..06625e1 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -107,7 +107,6 @@ logging:
level:
root: INFO
net.javahippie.fitpub: DEBUG
- org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.springframework.security: DEBUG
diff --git a/src/main/resources/db/migration/V23__add_gadm_table.sql b/src/main/resources/db/migration/V23__add_gadm_table.sql
new file mode 100644
index 0000000..eec250d
--- /dev/null
+++ b/src/main/resources/db/migration/V23__add_gadm_table.sql
@@ -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);
+
diff --git a/src/main/resources/db/migration/V24__add_location_to_activity.sql b/src/main/resources/db/migration/V24__add_location_to_activity.sql
new file mode 100644
index 0000000..a8b87dc
--- /dev/null
+++ b/src/main/resources/db/migration/V24__add_location_to_activity.sql
@@ -0,0 +1,2 @@
+alter table activities
+add column activity_location VARCHAR(255);
\ No newline at end of file
diff --git a/src/main/resources/static/js/timeline.js b/src/main/resources/static/js/timeline.js
index e8b1b06..e0f27a6 100644
--- a/src/main/resources/static/js/timeline.js
+++ b/src/main/resources/static/js/timeline.js
@@ -141,7 +141,7 @@ const FitPubTimeline = {
@${this.escapeHtml(activity.username)}
${!activity.isLocal ? ' Remote' : ''}
- • ${this.formatTimeAgo(activity.startedAt)}
+ • ${this.formatTimeAgo(activity.startedAt)} • ${activity.activityLocation}
diff --git a/src/main/resources/templates/activities/detail.html b/src/main/resources/templates/activities/detail.html
index 896fad0..f832fdb 100644
--- a/src/main/resources/templates/activities/detail.html
+++ b/src/main/resources/templates/activities/detail.html
@@ -44,6 +44,10 @@
+
+
+
+
@@ -506,6 +510,7 @@
activity.startedAt,
activity.timezone || 'UTC'
);
+ document.getElementById('activityLocation').textContent = activity.activityLocation;
document.getElementById('activityVisibility').textContent = activity.visibility;
// Visibility icon
diff --git a/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java b/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java
index 74a67ac..836efcc 100644
--- a/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java
+++ b/src/test/java/net/javahippie/fitpub/config/TestcontainersConfiguration.java
@@ -3,7 +3,11 @@ package net.javahippie.fitpub.config;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
+import org.testcontainers.containers.BindMode;
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;
/**
@@ -16,17 +20,21 @@ public class TestcontainersConfiguration {
/**
* PostgreSQL container with PostGIS extension for tests.
* PostGIS image is treated as a standard PostgreSQL container.
+ *
* @ServiceConnection automatically configures the datasource from this container.
*/
@Bean
@ServiceConnection
public PostgreSQLContainer> postgresContainer() {
return new PostgreSQLContainer<>(
- DockerImageName.parse("postgis/postgis:16-3.4")
- .asCompatibleSubstituteFor("postgres")
+ DockerImageName.parse("postgis/postgis:16-3.4")
+ .asCompatibleSubstituteFor("postgres")
)
- .withDatabaseName("testdb")
- .withUsername("test")
- .withPassword("test");
+ .withDatabaseName("testdb")
+ .withUsername("test")
+ .withPassword("test")
+ .waitingFor(new HostPortWaitStrategy())
+ .withReuse(true)
+ .withFileSystemBind(".postgresdata", "/var/lib/postgresql/data", BindMode.READ_WRITE);
}
}