Skip to content

Commit

Permalink
DPO-2301 Waze-feedin täydentäminen (#130)
Browse files Browse the repository at this point in the history
* DPO-2301 new waze type&subtype code, first version

* DPO-2301 enable some old tests

* DPO-2301 add hazard-types

* DPO-2301 add roadworks, more subtypes

* DPO-2301 fix roadClosed-handling, add test

* DPO-2301 convert maintenance work & construction work types

* DPO-2301 roadwork filtering, some debug

* DPO-2301 fix roadwork valid check

* DPO-2301 test with just roadworks

* DPO-2301 better convert, add test

* DPO-2301 better tests

* DPO-2301 fix tests

* DPO-2301 use list for subtypes, filter out low-severity road-works

* DPO-2301 filter out TransitInformation

* DPO-2301 remove mappings for other

* DPO-2301 review fixes

* DPO-2009 for address geocoding, use middle point from middle linestring of multilinestring

* DPO-2009 use first linestring
  • Loading branch information
teijosol authored Jun 13, 2024
1 parent 77dc35a commit bd21a04
Show file tree
Hide file tree
Showing 18 changed files with 996 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import org.springframework.web.bind.annotation.RestController;

import fi.livi.digitraffic.tie.dto.wazefeed.WazeFeedAnnouncementDto;
import fi.livi.digitraffic.tie.service.WazeFeedService;
import fi.livi.digitraffic.tie.service.waze.WazeFeedService;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.util.Optional;
import java.util.stream.Collectors;

import fi.livi.digitraffic.tie.datex2.*;
import fi.livi.digitraffic.tie.dto.trafficmessage.v1.SituationType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
Expand All @@ -16,18 +18,9 @@

import com.fasterxml.jackson.core.JsonProcessingException;

import fi.livi.digitraffic.tie.datex2.D2LogicalModel;
import fi.livi.digitraffic.tie.datex2.OverallPeriod;
import fi.livi.digitraffic.tie.datex2.RoadOrCarriagewayOrLaneManagement;
import fi.livi.digitraffic.tie.datex2.RoadOrCarriagewayOrLaneManagementExtensionType;
import fi.livi.digitraffic.tie.datex2.Situation;
import fi.livi.digitraffic.tie.datex2.SituationPublication;
import fi.livi.digitraffic.tie.datex2.SituationRecord;
import fi.livi.digitraffic.tie.datex2.ValidityStatusEnum;
import fi.livi.digitraffic.tie.dto.trafficmessage.v1.TrafficAnnouncementFeature;
import fi.livi.digitraffic.tie.dto.wazefeed.WazeDatex2FeatureDto;
import fi.livi.digitraffic.tie.model.trafficmessage.datex2.Datex2;
import fi.livi.digitraffic.tie.model.trafficmessage.datex2.SituationType;
import fi.livi.digitraffic.tie.service.trafficmessage.Datex2XmlStringToObjectMarshaller;
import fi.livi.digitraffic.tie.service.trafficmessage.TrafficMessageImsJsonConverterV1;

Expand All @@ -44,17 +37,16 @@ public WazeDatex2Converter(final TrafficMessageImsJsonConverterV1 datex2JsonConv
}

public Optional<WazeDatex2FeatureDto> convertToWazeDatex2FeatureDto(final Datex2 datex2) {

final TrafficAnnouncementFeature feature;
final String jsonMessage = datex2.getJsonMessage();

try {
feature = datex2JsonConverterV1.convertToFeatureJsonObject_V1(
jsonMessage,
SituationType.TRAFFIC_ANNOUNCEMENT,
datex2.getSituationType(),
datex2.getTrafficAnnouncementType(),
false,
datex2.getModified()
datex2.getModified()
);
} catch (final JsonProcessingException e) {
logger.error("method=convertToWazeDatex2FeatureDto json string conversion to feature object failed", e);
Expand Down Expand Up @@ -83,8 +75,8 @@ public static boolean hasActiveSituationRecords(final WazeDatex2FeatureDto wazeD
final String situationId = wazeDatex2FeatureDto.feature.getProperties().situationId;

final boolean result = getSituationRecords(situationId, wazeDatex2FeatureDto.d2LogicalModel)
.stream()
.anyMatch(WazeDatex2Converter::isActiveSituationRecord);
.stream()
.anyMatch(WazeDatex2Converter::isActiveSituationRecord);

if (!result) {
logger.info("method=hasActiveSituationRecords situation {} did not have any valid situation records", situationId);
Expand All @@ -97,13 +89,13 @@ public static boolean isActiveSituationRecord(final SituationRecord situationRec
final ValidityStatusEnum validityStatus = situationRecord.getValidity().getValidityStatus();

switch (validityStatus) {
case ACTIVE:
return true;
case DEFINED_BY_VALIDITY_TIME_SPEC:
final OverallPeriod validityTimeSpec = situationRecord.getValidity().getValidityTimeSpecification();
final Instant now = Instant.now();
final Instant overallEndTime = validityTimeSpec.getOverallEndTime();
return now.isAfter(validityTimeSpec.getOverallStartTime()) &&
case ACTIVE:
return true;
case DEFINED_BY_VALIDITY_TIME_SPEC:
final OverallPeriod validityTimeSpec = situationRecord.getValidity().getValidityTimeSpecification();
final Instant now = Instant.now();
final Instant overallEndTime = validityTimeSpec.getOverallEndTime();
return now.isAfter(validityTimeSpec.getOverallStartTime()) &&
overallEndTime == null || now.isBefore(overallEndTime);
}

Expand All @@ -124,20 +116,48 @@ public static List<SituationRecord> getSituationRecords(final String situationId
}

return situations.stream()
.map(Situation::getSituationRecords)
.flatMap(Collection::stream)
.collect(Collectors.toList());
.map(Situation::getSituationRecords)
.flatMap(Collection::stream)
.collect(Collectors.toList());
}

public static boolean hasIceRoadOpenRecord(final WazeDatex2FeatureDto wazeDatex2FeatureDto) {
final String situationId = wazeDatex2FeatureDto.feature.getProperties().situationId;
return getSituationRecords(situationId, wazeDatex2FeatureDto.d2LogicalModel)
.stream()
.filter(sr -> sr instanceof RoadOrCarriagewayOrLaneManagement)
.anyMatch(sr -> Optional.of((RoadOrCarriagewayOrLaneManagement) sr)
.map(RoadOrCarriagewayOrLaneManagement::getRoadOrCarriagewayOrLaneManagementExtension)
.map(RoadOrCarriagewayOrLaneManagementExtensionType::getRoadOrCarriagewayOrLaneManagementType)
.map(x -> x.equals(ICE_ROAD_OPEN))
.orElse(false));
.stream()
.filter(sr -> sr instanceof RoadOrCarriagewayOrLaneManagement)
.anyMatch(sr -> Optional.of((RoadOrCarriagewayOrLaneManagement) sr)
.map(RoadOrCarriagewayOrLaneManagement::getRoadOrCarriagewayOrLaneManagementExtension)
.map(RoadOrCarriagewayOrLaneManagementExtensionType::getRoadOrCarriagewayOrLaneManagementType)
.map(x -> x.equals(ICE_ROAD_OPEN))
.orElse(false));
}

public static boolean hasTransitInformationRecord(final WazeDatex2FeatureDto wazeDatex2FeatureDto) {
final String situationId = wazeDatex2FeatureDto.feature.getProperties().situationId;
return getSituationRecords(situationId, wazeDatex2FeatureDto.d2LogicalModel)
.stream()
.anyMatch(sr -> sr instanceof TransitInformation);
}

public static boolean isValidWazeEntry(final WazeDatex2FeatureDto feature) {
return hasGeometry(feature)
&& hasActiveSituationRecords(feature)
&& isValidType(feature);
}

private static boolean isValidType(final WazeDatex2FeatureDto feature) {
// if it is a traffic announcement, we check that it's not ice road open or TransitInformation
if(feature.feature.getProperties().getSituationType() == SituationType.TRAFFIC_ANNOUNCEMENT) {
return !hasIceRoadOpenRecord(feature) && !hasTransitInformationRecord(feature);
}

// low serverity road-works are not relevant
if (feature.datex2.getMessage().contains(("<overallSeverity>low"))) {
return false;
}

return feature.datex2.getMessage().contains("<roadOrCarriagewayOrLaneManagementType>roadClosed")
|| feature.datex2.getMessage().contains("<constructionWorkType>constructionWork");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import fi.livi.digitraffic.tie.metadata.geojson.Geometry;
import fi.livi.digitraffic.tie.metadata.geojson.MultiLineString;
import fi.livi.digitraffic.tie.metadata.geojson.Point;
import fi.livi.digitraffic.tie.service.WazeReverseGeocodingService;
import fi.livi.digitraffic.tie.service.waze.WazeReverseGeocodingService;

@ConditionalOnWebApplication
@Component
Expand All @@ -40,19 +40,23 @@ public class WazeDatex2JsonConverter {

private final WazeReverseGeocodingService wazeReverseGeocodingService;

private final WazeTypeConverter wazeTypeConverter;

@Autowired
public WazeDatex2JsonConverter(final WazeDatex2MessageConverter wazeDatex2MessageConverter,
final WazeReverseGeocodingService wazeReverseGeocodingService) {
final WazeReverseGeocodingService wazeReverseGeocodingService,
final WazeTypeConverter wazeTypeConverter) {
this.wazeDatex2MessageConverter = wazeDatex2MessageConverter;
this.wazeReverseGeocodingService = wazeReverseGeocodingService;
this.wazeTypeConverter = wazeTypeConverter;
}

public Optional<WazeFeedIncidentDto> convertToWazeFeedAnnouncementDto(final WazeDatex2FeatureDto wazeDatex2FeatureDto) {
final TrafficAnnouncementFeature feature = wazeDatex2FeatureDto.feature;

final TrafficAnnouncementProperties properties = feature.getProperties();
final String situationId = properties.situationId;
final Optional<WazeFeedIncidentDto.Type> maybeType = this.convertToWazeType(properties.getTrafficAnnouncementType());
final Optional<WazeFeedIncidentDto.WazeType> maybeType = wazeTypeConverter.convertToWazeType(wazeDatex2FeatureDto);

final TrafficAnnouncement announcement = properties.announcements.get(0);
final Optional<Geometry<?>> maybeGeometry = Optional.ofNullable(feature.getGeometry());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package fi.livi.digitraffic.tie.converter.waze;

import fi.livi.digitraffic.tie.dto.trafficmessage.v1.Restriction;
import fi.livi.digitraffic.tie.dto.trafficmessage.v1.SituationType;
import fi.livi.digitraffic.tie.dto.trafficmessage.v1.TrafficAnnouncementProperties;
import fi.livi.digitraffic.tie.dto.wazefeed.WazeDatex2FeatureDto;
import fi.livi.digitraffic.tie.dto.wazefeed.WazeFeedIncidentDto;
import fi.livi.digitraffic.tie.model.trafficmessage.datex2.Datex2;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Optional;

import static fi.livi.digitraffic.tie.dto.wazefeed.WazeFeedIncidentDto.WazeType.*;

@ConditionalOnWebApplication
@Component
public class WazeTypeConverter {
public Optional<WazeFeedIncidentDto.WazeType> convertToWazeType(final WazeDatex2FeatureDto dto) {
final TrafficAnnouncementProperties properties = dto.feature.getProperties();

if (properties.getSituationType() == SituationType.TRAFFIC_ANNOUNCEMENT && properties.getTrafficAnnouncementType() == null) {
return Optional.empty();
}

if (isRoadClosed(dto.datex2)) {
if (properties.getSituationType() == SituationType.TRAFFIC_ANNOUNCEMENT) {
return Optional.of(ROAD_CLOSED_HAZARD);
} else {
return Optional.of(ROAD_CLOSED_CONSTRUCTION);
}
}

// Check datex2 for any common type for TrafficAnnouncement or MaintenanceWork
final WazeFeedIncidentDto.WazeType commonWazeType = getCommonWazeTypeFromDatex2(dto.datex2);
if (commonWazeType != null) {
return Optional.of(commonWazeType);
}

if (properties.getSituationType() == SituationType.TRAFFIC_ANNOUNCEMENT) {
// traffic announcement
// old functionality
switch (properties.getTrafficAnnouncementType()) {
case ACCIDENT_REPORT:
case PRELIMINARY_ACCIDENT_REPORT:
return Optional.of(ACCIDENT_NONE);
case GENERAL:
return Optional.of(HAZARD_NONE);
default:
return Optional.empty();
}
} else if (properties.getSituationType() == SituationType.ROAD_WORK) {
// road works

// default type for ROAD_WORK
return Optional.empty();
}

throw new IllegalArgumentException("Could not figure out type for id " + dto.feature.getProperties().situationId);
}

/**
* Check if any restriction is of type ROAD_CLOSED
*/
private boolean isRoadClosed(final Datex2 d2) {
return d2.getMessage().contains("<roadOrCarriagewayOrLaneManagementType>roadClosed");
}

// note, this is also a priority listing
// if multiple mappings are present, the one higher in the list will be selected
private static List<Pair<String, WazeFeedIncidentDto.WazeType>> typeMapping = List.of(
Pair.of("<obstructionType>hazardsOnTheRoad", HAZARD_ON_ROAD),
Pair.of("<obstructionType>objectOnTheRoad", HAZARD_ON_ROAD_OBJECT),
Pair.of("<roadOrCarriagewayOrLaneManagementType>laneClosures", HAZARD_ON_ROAD_LANE_CLOSED),
Pair.of("<animalPresenceType>animalsOnTheRoad", HAZARD_ON_SHOULDER_ANIMALS),
Pair.of("<animalPresenceType>largeAnimalsOnTheRoad", HAZARD_ON_SHOULDER_ANIMALS),
Pair.of("<animalPresenceType>herdOfAnimalsOnTheRoad", HAZARD_ON_SHOULDER_ANIMALS),

Pair.of("<poorEnvironmentType>badWeather", HAZARD_WEATHER),

Pair.of("<nonWeatherRelatedRoadConditionType>oilOnRoad", HAZARD_ON_ROAD_OIL),
Pair.of("<nonWeatherRelatedRoadConditionType>roadSurfaceInPoorCondition", HAZARD_ON_ROAD_POT_HOLE),
Pair.of("<weatherRelatedRoadConditionType>ice", HAZARD_ON_ROAD_ICE),
Pair.of("<weatherRelatedRoadConditionType>freezingRain", HAZARD_WEATHER_FREEZING_RAIN),
Pair.of("<weatherRelatedRoadConditionType>heavyRain", HAZARD_WEATHER_HEAVY_RAIN),
Pair.of("<weatherRelatedRoadConditionType>damagingHail", HAZARD_WEATHER_HAIL),
Pair.of("<weatherRelatedRoadConditionType>blizzard", HAZARD_WEATHER_HEAVY_SNOW),
Pair.of("<poorEnvironmentType>heavySnowfall", HAZARD_WEATHER_HEAVY_SNOW),

Pair.of("<environmentalObstructionType>flooding", HAZARD_WEATHER_FLOOD),
Pair.of("<environmentalObstructionType>denseFog", HAZARD_WEATHER_FOG),
Pair.of("<environmentalObstructionType>fog", HAZARD_WEATHER_FOG),

Pair.of("<abnormalTrafficType>stationaryTraffic", JAM_STAND_STILL_TRAFFIC),
Pair.of("<abnormalTrafficType>heavyTraffic", JAM_HEAVY_TRAFFIC),
Pair.of("<abnormalTrafficType>queuingTraffic", JAM_MODERATE_TRAFFIC)
);

private WazeFeedIncidentDto.WazeType getCommonWazeTypeFromDatex2(final Datex2 d2) {
return typeMapping.stream()
.filter(e -> d2.getMessage().contains(e.getKey()))
.findFirst()
.map(e -> e.getValue())
.orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ public class TrafficAnnouncementProperties extends PropertiesV1 {
@Schema(description = "The type of the traffic announcement. Omitted for other situation types. Note that ended and retracted are not actual types.")
private TrafficAnnouncementType trafficAnnouncementType;

@Schema(description = "Annoucement release time", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "Announcement release time", requiredMode = Schema.RequiredMode.REQUIRED)
public final ZonedDateTime releaseTime;

@Schema(description = "Annoucement version time", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "Announcement version time", requiredMode = Schema.RequiredMode.REQUIRED)
public final ZonedDateTime versionTime;

@Schema(description = "Contains announcement's different language versions available.", requiredMode = Schema.RequiredMode.REQUIRED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,30 +11,78 @@ public class WazeFeedIncidentDto implements Serializable {

public final String id;
public final String description;
public final Type type;
public final String type;
public final String subtype;
public final WazeFeedLocationDto location;
public final String starttime;
public final String endtime;

public WazeFeedIncidentDto(final String id, final String street, final String description, final WazeFeedLocationDto.Direction direction,
final String polyline, final Type type, final String starttime, final String endtime) {
final String polyline, final WazeType type, final String starttime, final String endtime) {
this.id = id;
this.location = new WazeFeedLocationDto(street, polyline, direction);
this.description = description;
this.type = type;
this.type = type == null ? null : type.type.name();
this.subtype = type == null ? null : type.getSubtype();
this.starttime = starttime;
this.endtime = endtime;
}
}

public enum Type {
// Defined the only type currently in use in our Waze integration. More can be added as needed
// Defined types currently in use in our Waze integration. More can be added as needed
// See: https://developers.google.com/waze/data-feed/incident-information
HAZARD,
ACCIDENT;
ACCIDENT,
ROAD_CLOSED,
JAM,
POLICE;
}

public enum WazeType {
// Defined subtypes currently in use in our Waze integration.
// See: https://developers.google.com/waze/data-feed/cifs-specification#incident-subtypes

ACCIDENT_NONE(Type.ACCIDENT),

HAZARD_NONE(Type.HAZARD),
HAZARD_ON_ROAD(Type.HAZARD),
HAZARD_ON_ROAD_CONSTRUCTION(Type.HAZARD),
HAZARD_ON_ROAD_ICE(Type.HAZARD),
HAZARD_ON_ROAD_LANE_CLOSED(Type.HAZARD),
HAZARD_ON_ROAD_OBJECT(Type.HAZARD),
HAZARD_ON_ROAD_OIL(Type.HAZARD),
HAZARD_ON_ROAD_POT_HOLE(Type.HAZARD),
HAZARD_ON_ROAD_TRAFFIC_LIGHT_FAULT(Type.HAZARD),
HAZARD_ON_SHOULDER_ANIMALS(Type.HAZARD),
HAZARD_WEATHER(Type.HAZARD),
HAZARD_WEATHER_FLOOD(Type.HAZARD),
HAZARD_WEATHER_FOG(Type.HAZARD),
HAZARD_WEATHER_FREEZING_RAIN(Type.HAZARD),
HAZARD_WEATHER_HAIL(Type.HAZARD),
HAZARD_WEATHER_HEAVY_RAIN(Type.HAZARD),
HAZARD_WEATHER_HEAVY_SNOW(Type.HAZARD),

ROAD_CLOSED_NONE(Type.ROAD_CLOSED),
ROAD_CLOSED_HAZARD(Type.ROAD_CLOSED),
ROAD_CLOSED_CONSTRUCTION(Type.ROAD_CLOSED),

JAM_NONE(Type.JAM),
JAM_MODERATE_TRAFFIC(Type.JAM),
JAM_HEAVY_TRAFFIC(Type.JAM),
JAM_STAND_STILL_TRAFFIC(Type.JAM),
;

public final Type type;
WazeType(final Type type) {
this.type = type;
}

public String getSubtype() {
if(name().endsWith("_NONE")) {
return null;
}

@JsonCreator
public static Type fromValue(final String value) {
return Type.valueOf(value.toUpperCase());
return name();
}
}
}
Loading

0 comments on commit bd21a04

Please sign in to comment.