From 8e7dd5a813656db21af4e406634d3b2305d3093a Mon Sep 17 00:00:00 2001 From: teppope Date: Tue, 21 Jul 2026 10:44:27 +0300 Subject: [PATCH] Add parser support for parking opening hours (AvailabilityCondition) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../netex/index/api/NetexEntitiesIndex.java | 8 ++ .../index/impl/NetexEntitiesIndexImpl.java | 9 +++ .../netex/loader/parser/SiteFrameParser.java | 18 +++++ .../TestParkingAvailabilityConditions.java | 74 +++++++++++++++++++ .../ParkingWithAvailabilityConditions.xml | 66 +++++++++++++++++ 5 files changed, 175 insertions(+) create mode 100644 src/test/java/org/entur/netex/TestParkingAvailabilityConditions.java create mode 100644 src/test/resources/ParkingWithAvailabilityConditions.xml diff --git a/src/main/java/org/entur/netex/index/api/NetexEntitiesIndex.java b/src/main/java/org/entur/netex/index/api/NetexEntitiesIndex.java index 373b73b..59a1e7f 100644 --- a/src/main/java/org/entur/netex/index/api/NetexEntitiesIndex.java +++ b/src/main/java/org/entur/netex/index/api/NetexEntitiesIndex.java @@ -5,6 +5,7 @@ import java.util.Collection; import java.util.Map; import org.rutebanken.netex.model.Authority; +import org.rutebanken.netex.model.AvailabilityCondition; import org.rutebanken.netex.model.Block; import org.rutebanken.netex.model.Branding; import org.rutebanken.netex.model.CompositeFrame; @@ -297,6 +298,13 @@ public interface NetexEntitiesIndex { */ Multimap getParkingsByParentSiteRefIndex(); + /** + * Get a Multimap of AvailabilityConditions by Parking id. + * Populated from {@code validityConditions} on each parsed Parking. + * @return + */ + Multimap getAvailabilityConditionsByParkingIdIndex(); + /** * Get an entity index of FareZone * @return diff --git a/src/main/java/org/entur/netex/index/impl/NetexEntitiesIndexImpl.java b/src/main/java/org/entur/netex/index/impl/NetexEntitiesIndexImpl.java index 3718d5a..d7e854b 100644 --- a/src/main/java/org/entur/netex/index/impl/NetexEntitiesIndexImpl.java +++ b/src/main/java/org/entur/netex/index/impl/NetexEntitiesIndexImpl.java @@ -12,6 +12,7 @@ import org.entur.netex.index.api.NetexEntityIndex; import org.entur.netex.index.api.VersionedNetexEntityIndex; import org.rutebanken.netex.model.Authority; +import org.rutebanken.netex.model.AvailabilityCondition; import org.rutebanken.netex.model.Block; import org.rutebanken.netex.model.Branding; import org.rutebanken.netex.model.CompositeFrame; @@ -102,6 +103,7 @@ public class NetexEntitiesIndexImpl implements NetexEntitiesIndex { public final VersionedNetexEntityIndex fareZoneById; public final VersionedNetexEntityIndex groupOfTariffZonesById; public final Multimap parkingsByParentSiteRef; + public final Multimap availabilityConditionsByParkingId; // Relations between entities - The Netex XML sometimes rely on the // nested structure of the XML document, rater than explicit references. @@ -179,6 +181,8 @@ public NetexEntitiesIndexImpl() { this.timetableFrames = new HashSet<>(); this.parkingsByParentSiteRef = Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); + this.availabilityConditionsByParkingId = + Multimaps.synchronizedListMultimap(ArrayListMultimap.create()); } @Override @@ -440,4 +444,9 @@ public void setPublicationTimestamp(LocalDateTime publicationTimestamp) { public Multimap getParkingsByParentSiteRefIndex() { return parkingsByParentSiteRef; } + + @Override + public Multimap getAvailabilityConditionsByParkingIdIndex() { + return availabilityConditionsByParkingId; + } } diff --git a/src/main/java/org/entur/netex/loader/parser/SiteFrameParser.java b/src/main/java/org/entur/netex/loader/parser/SiteFrameParser.java index 897b965..03dd32f 100644 --- a/src/main/java/org/entur/netex/loader/parser/SiteFrameParser.java +++ b/src/main/java/org/entur/netex/loader/parser/SiteFrameParser.java @@ -43,6 +43,9 @@ class SiteFrameParser extends NetexParser { private final Multimap parkingsByStopPlaceId = ArrayListMultimap.create(); + private final Multimap availabilityConditionsByParkingId = + ArrayListMultimap.create(); + @Override public void parse(Site_VersionFrameStructure frame) { if (frame.getStopPlaces() != null) { @@ -119,6 +122,9 @@ void setResultOnIndex(NetexEntitiesIndex netexIndex) { netexIndex.getQuayIndex().putAll(quays.values()); netexIndex.getStopPlaceIdByQuayIdIndex().putAll(stopPlaceIdByQuayId); netexIndex.getParkingsByParentSiteRefIndex().putAll(parkingsByStopPlaceId); + netexIndex + .getAvailabilityConditionsByParkingIdIndex() + .putAll(availabilityConditionsByParkingId); netexIndex.getGroupOfTariffZonesIndex().putAll(groupsOfTariffZones); } @@ -166,6 +172,18 @@ private void parseParkings(Collection parkingList) { for (Parking parking : parkingList) { parkings.add(parking); parkingsByStopPlaceId.put(parking.getParentSiteRef().getRef(), parking); + if (parking.getValidityConditions() != null) { + for (Object obj : parking + .getValidityConditions() + .getValidityConditionRefOrValidBetweenOrValidityCondition_()) { + if ( + obj instanceof JAXBElement jaxb && + jaxb.getValue() instanceof AvailabilityCondition ac + ) { + availabilityConditionsByParkingId.put(parking.getId(), ac); + } + } + } } } diff --git a/src/test/java/org/entur/netex/TestParkingAvailabilityConditions.java b/src/test/java/org/entur/netex/TestParkingAvailabilityConditions.java new file mode 100644 index 0000000..4ade7ea --- /dev/null +++ b/src/test/java/org/entur/netex/TestParkingAvailabilityConditions.java @@ -0,0 +1,74 @@ +package org.entur.netex; + +import java.io.File; +import java.nio.file.Files; +import java.util.Collection; +import org.entur.netex.index.api.NetexEntitiesIndex; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.rutebanken.netex.model.AvailabilityCondition; + +class TestParkingAvailabilityConditions { + + private static NetexEntitiesIndex index; + + @BeforeAll + static void init() { + try { + NetexParser parser = new NetexParser(); + File file = new File( + "src/test/resources/ParkingWithAvailabilityConditions.xml" + ); + index = parser.parse(Files.newInputStream(file.toPath())); + } catch (Exception e) { + Assertions.fail(e.getMessage(), e); + } + } + + @Test + void parkingWithAvailabilityConditionsIsIndexedByParkingId() { + Collection conditions = index + .getAvailabilityConditionsByParkingIdIndex() + .get("FSR:Parking:1"); + Assertions.assertEquals(2, conditions.size()); + } + + @Test + void availabilityConditionOpenDayHasCorrectFields() { + AvailabilityCondition openCondition = index + .getAvailabilityConditionsByParkingIdIndex() + .get("FSR:Parking:1") + .stream() + .filter(ac -> "FSR:AvailabilityCondition:1".equals(ac.getId())) + .findFirst() + .orElseThrow(); + + Assertions.assertTrue(openCondition.isIsAvailable()); + Assertions.assertNotNull(openCondition.getDayTypes()); + Assertions.assertFalse( + openCondition.getDayTypes().getDayTypeRefOrDayType_().isEmpty() + ); + } + + @Test + void availabilityConditionClosedDayHasIsAvailableFalse() { + AvailabilityCondition closedCondition = index + .getAvailabilityConditionsByParkingIdIndex() + .get("FSR:Parking:1") + .stream() + .filter(ac -> "FSR:AvailabilityCondition:2".equals(ac.getId())) + .findFirst() + .orElseThrow(); + + Assertions.assertFalse(closedCondition.isIsAvailable()); + } + + @Test + void parkingWithoutAvailabilityConditionsReturnsEmptyCollection() { + Collection conditions = index + .getAvailabilityConditionsByParkingIdIndex() + .get("FSR:Parking:2"); + Assertions.assertTrue(conditions.isEmpty()); + } +} diff --git a/src/test/resources/ParkingWithAvailabilityConditions.xml b/src/test/resources/ParkingWithAvailabilityConditions.xml new file mode 100644 index 0000000..2d03393 --- /dev/null +++ b/src/test/resources/ParkingWithAvailabilityConditions.xml @@ -0,0 +1,66 @@ + + + 2026-07-21T10:00:00 + FSR + + + + + Test Stop + + + 25.0 + 60.0 + + + + + + + + + true + + + + + + 06:00:00 + 22:00:00 + + + + + false + + + + + + Test Parking + + + 25.0 + 60.0 + + + urbanParking + openSpace + + + + Parking without opening hours + + + 25.1 + 60.1 + + + urbanParking + openSpace + + + + + +