|
|
@@ -0,0 +1,214 @@
|
|
|
+package com.zksy.web.controller.cockpit;
|
|
|
+
|
|
|
+import com.zksy.base.domain.GasPipePoint;
|
|
|
+import com.zksy.base.domain.ManholeBase;
|
|
|
+import com.zksy.base.domain.PipeNetworkBase;
|
|
|
+import com.zksy.base.service.GasPipePointService;
|
|
|
+import com.zksy.base.service.ManholeBaseService;
|
|
|
+import com.zksy.base.service.PipeNetworkBaseService;
|
|
|
+import com.zksy.cockpit.foundation.CockpitReadException;
|
|
|
+import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse;
|
|
|
+import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Coverage;
|
|
|
+import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Feature;
|
|
|
+import com.zksy.cockpit.geometry.CockpitFacilityGeometryService;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.junit.jupiter.params.ParameterizedTest;
|
|
|
+import org.junit.jupiter.params.provider.CsvSource;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.Clock;
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+class CockpitFacilityGeometryServiceTest {
|
|
|
+ private PipeNetworkBaseService networks;
|
|
|
+ private GasPipePointService points;
|
|
|
+ private ManholeBaseService manholes;
|
|
|
+ private CockpitFacilityGeometryService service;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ networks = mock(PipeNetworkBaseService.class);
|
|
|
+ points = mock(GasPipePointService.class);
|
|
|
+ manholes = mock(ManholeBaseService.class);
|
|
|
+ when(networks.list()).thenReturn(Collections.emptyList());
|
|
|
+ when(points.list()).thenReturn(Collections.emptyList());
|
|
|
+ when(manholes.list()).thenReturn(Collections.emptyList());
|
|
|
+ service = new CockpitFacilityGeometryService(networks, points, manholes,
|
|
|
+ Clock.fixed(Instant.parse("2026-10-12T02:00:00Z"), ZoneOffset.UTC));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void readsAllThreeIndustriesAsActualTwoEndpointLinesWithoutInventingTopology() {
|
|
|
+ when(networks.list()).thenReturn(Arrays.asList(network("same-id", "0"), network("water", "2"),
|
|
|
+ network("drain", "1"), network("unknown", "3")));
|
|
|
+ GasPipePoint point = point(1L, "三通");
|
|
|
+ point.setNetworkId("same-id");
|
|
|
+ when(points.list()).thenReturn(Collections.singletonList(point));
|
|
|
+ ManholeBase manhole = new ManholeBase();
|
|
|
+ manhole.setManholeId("same-id");
|
|
|
+ manhole.setLongitude(new BigDecimal("110.5"));
|
|
|
+ manhole.setLatitude(new BigDecimal("28.5"));
|
|
|
+ when(manholes.list()).thenReturn(Collections.singletonList(manhole));
|
|
|
+
|
|
|
+ CockpitFacilityGeometryResponse response = service.load();
|
|
|
+ assertEquals("FeatureCollection", response.getType());
|
|
|
+ assertEquals("2026-10-12T10:00:00+08:00", response.getGeneratedAt());
|
|
|
+ assertEquals(6, response.getFeatures().size());
|
|
|
+ assertEquals(6, response.getFeatures().stream().map(Feature::getId).distinct().count());
|
|
|
+ assertEquals(Arrays.asList("GAS", "WATER_SUPPLY", "DRAINAGE", "UNKNOWN", "GAS", "MANHOLE"),
|
|
|
+ response.getFeatures().stream().map(item -> item.getProperties().getIndustry()).collect(Collectors.toList()));
|
|
|
+ Feature gas = response.getFeatures().get(0);
|
|
|
+ assertEquals("LineString", gas.getGeometry().getType());
|
|
|
+ assertEquals(Arrays.asList(Arrays.asList(new BigDecimal("110.4"), new BigDecimal("28.4")),
|
|
|
+ Arrays.asList(new BigDecimal("110.6"), new BigDecimal("28.6"))), gas.getGeometry().getCoordinates());
|
|
|
+ assertEquals("GAS_PIPE", gas.getProperties().getTypeCode());
|
|
|
+ assertEquals("PIPE_UNCLASSIFIED", response.getFeatures().get(3).getProperties().getTypeCode());
|
|
|
+ assertEquals("3", response.getFeatures().get(3).getProperties().getNetworkType());
|
|
|
+ Feature component = response.getFeatures().get(4);
|
|
|
+ assertEquals("Point", component.getGeometry().getType());
|
|
|
+ assertEquals("三通", component.getProperties().getPointType());
|
|
|
+ assertEquals(gas.getProperties().getNetworkId(), component.getProperties().getNetworkId());
|
|
|
+ assertEquals(1, coverage(response, "UNKNOWN", "LineString").getRecordCount());
|
|
|
+ verify(networks).list();
|
|
|
+ verify(points).list();
|
|
|
+ verify(manholes).list();
|
|
|
+ verifyNoMoreInteractions(networks, points, manholes);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void preservesEveryComponentTypeIncludingUnknownAndMissingTypesWithoutFabricatingNodes() {
|
|
|
+ List<String> types = Arrays.asList("阀门", "调压", "计量", "阴极保护", "弯头", "三通", "其它", "未知原始值", null);
|
|
|
+ List<GasPipePoint> records = new java.util.ArrayList<>();
|
|
|
+ for (int i = 0; i < types.size(); i++) {
|
|
|
+ records.add(point((long) i, types.get(i)));
|
|
|
+ }
|
|
|
+ when(points.list()).thenReturn(records);
|
|
|
+ CockpitFacilityGeometryResponse response = service.load();
|
|
|
+ assertEquals(types, response.getFeatures().stream()
|
|
|
+ .map(item -> item.getProperties().getPointType()).collect(Collectors.toList()));
|
|
|
+ assertEquals(types.size(), response.getFeatures().size());
|
|
|
+ assertTrue(response.getFeatures().stream().allMatch(item -> item.getProperties().getNetworkId() == null));
|
|
|
+ }
|
|
|
+
|
|
|
+ @ParameterizedTest
|
|
|
+ @CsvSource({",28.4,MISSING_LONGITUDE", "110.4,,MISSING_LATITUDE", "181,28.4,LONGITUDE_OUT_OF_RANGE",
|
|
|
+ "-181,28.4,LONGITUDE_OUT_OF_RANGE", "110.4,91,LATITUDE_OUT_OF_RANGE",
|
|
|
+ "110.4,-91,LATITUDE_OUT_OF_RANGE", "0,0,ZERO_COORDINATE"})
|
|
|
+ void invalidPointsStayInTheCollectionWithNullGeometryAndSpecificReason(String longitude, String latitude,
|
|
|
+ String reason) {
|
|
|
+ GasPipePoint point = point(1L, "阀门");
|
|
|
+ point.setLongitude(decimal(longitude));
|
|
|
+ point.setLatitude(decimal(latitude));
|
|
|
+ when(points.list()).thenReturn(Collections.singletonList(point));
|
|
|
+ Feature feature = service.load().getFeatures().get(0);
|
|
|
+ assertNull(feature.getGeometry());
|
|
|
+ assertEquals("INVALID", feature.getProperties().getCoordinateValidation().getState());
|
|
|
+ assertEquals(reason, feature.getProperties().getCoordinateValidation().getReason());
|
|
|
+ assertFalse(feature.getProperties().getCoordinateValidation().isRenderable());
|
|
|
+ assertEquals(Arrays.asList(decimal(longitude), decimal(latitude)),
|
|
|
+ feature.getProperties().getSourceCoordinates().get(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void invalidEndIsNotMaskedByAnUnverifiedStartAndEntireLineHasNullGeometry() {
|
|
|
+ PipeNetworkBase line = network("bad-end", "1");
|
|
|
+ line.setEndLatitude(null);
|
|
|
+ when(networks.list()).thenReturn(Collections.singletonList(line));
|
|
|
+ CockpitFacilityGeometryResponse response = service.load();
|
|
|
+ Feature feature = response.getFeatures().get(0);
|
|
|
+ assertNull(feature.getGeometry());
|
|
|
+ assertEquals("MISSING_LATITUDE", feature.getProperties().getCoordinateValidation().getReason());
|
|
|
+ assertEquals("UNVERIFIED", feature.getProperties().getCoordinateValidations().get(0).getState());
|
|
|
+ assertEquals("INVALID", feature.getProperties().getCoordinateValidations().get(1).getState());
|
|
|
+ assertEquals(1, coverage(response, "DRAINAGE", "LineString").getRecordCount());
|
|
|
+ assertEquals(0, coverage(response, "DRAINAGE", "LineString").getGeometryCount());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void numericBoundsDoNotGrantCoordinateVerificationForAnySource() {
|
|
|
+ when(networks.list()).thenReturn(Collections.singletonList(network("line", "0")));
|
|
|
+ when(points.list()).thenReturn(Collections.singletonList(point(1L, "阀门")));
|
|
|
+ ManholeBase manhole = new ManholeBase();
|
|
|
+ manhole.setManholeId("well");
|
|
|
+ manhole.setLongitude(new BigDecimal("-180"));
|
|
|
+ manhole.setLatitude(new BigDecimal("90"));
|
|
|
+ when(manholes.list()).thenReturn(Collections.singletonList(manhole));
|
|
|
+ CockpitFacilityGeometryResponse response = service.load();
|
|
|
+ for (Feature feature : response.getFeatures()) {
|
|
|
+ assertNotNull(feature.getGeometry());
|
|
|
+ assertEquals("UNVERIFIED", feature.getProperties().getCoordinateValidation().getState());
|
|
|
+ assertEquals("UNVERIFIED_COORDINATE", feature.getProperties().getCoordinateValidation().getReason());
|
|
|
+ assertEquals("WGS84", feature.getProperties().getCoordinateValidation().getCrs());
|
|
|
+ assertFalse(feature.getProperties().getCoordinateValidation().isRenderable());
|
|
|
+ }
|
|
|
+ assertTrue(response.getCoverage().stream().allMatch(item -> item.getRenderableCount() == 0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void emptySourcesAreDistinguishedFromUnsupportedIndustryGeometry() {
|
|
|
+ CockpitFacilityGeometryResponse response = service.load();
|
|
|
+ assertTrue(response.getFeatures().isEmpty());
|
|
|
+ assertEquals("EMPTY", coverage(response, "GAS", "Point").getState());
|
|
|
+ assertEquals("EMPTY", coverage(response, "WATER_SUPPLY", "LineString").getState());
|
|
|
+ assertEquals("TWO_ENDPOINTS_ONLY", coverage(response, "WATER_SUPPLY", "LineString").getReason());
|
|
|
+ assertEquals("UNAVAILABLE", coverage(response, "WATER_SUPPLY", "Point").getState());
|
|
|
+ assertEquals("NO_COMPONENT_SOURCE", coverage(response, "WATER_SUPPLY", "Point").getReason());
|
|
|
+ assertEquals("NO_TYPED_COMPONENT_SOURCE", coverage(response, "DRAINAGE", "Point").getReason());
|
|
|
+ assertEquals("NO_COMPONENT_TYPE", coverage(response, "MANHOLE", "Point").getReason());
|
|
|
+ assertEquals("NO_PIPELINE_SOURCE", coverage(response, "MANHOLE", "LineString").getReason());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void aFailedSourceNeverProducesAnEmptyOrPartialSuccess() {
|
|
|
+ when(networks.list()).thenReturn(Collections.singletonList(network("line", "0")));
|
|
|
+ when(points.list()).thenThrow(new IllegalStateException("private database detail"));
|
|
|
+ CockpitReadException failure = assertThrows(CockpitReadException.class, service::load);
|
|
|
+ assertEquals("facility geometry source unavailable", failure.getMessage());
|
|
|
+ verifyNoInteractions(manholes);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void aNullSourceIsAReadFailureRatherThanASuccessfulEmptyCollection() {
|
|
|
+ when(manholes.list()).thenReturn(null);
|
|
|
+ assertThrows(CockpitReadException.class, service::load);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Coverage coverage(CockpitFacilityGeometryResponse response, String industry, String geometry) {
|
|
|
+ return response.getCoverage().stream().filter(item -> industry.equals(item.getIndustry())
|
|
|
+ && geometry.equals(item.getGeometryType())).findFirst().orElseThrow();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static PipeNetworkBase network(String id, String type) {
|
|
|
+ PipeNetworkBase network = new PipeNetworkBase();
|
|
|
+ network.setNetworkId(id);
|
|
|
+ network.setNetworkCode("code-" + id);
|
|
|
+ network.setNetworkType(type);
|
|
|
+ network.setStartLongitude(new BigDecimal("110.4"));
|
|
|
+ network.setStartLatitude(new BigDecimal("28.4"));
|
|
|
+ network.setEndLongitude(new BigDecimal("110.6"));
|
|
|
+ network.setEndLatitude(new BigDecimal("28.6"));
|
|
|
+ return network;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static GasPipePoint point(Long id, String type) {
|
|
|
+ GasPipePoint point = new GasPipePoint();
|
|
|
+ point.setPointId(id);
|
|
|
+ point.setPointType(type);
|
|
|
+ point.setLongitude(new BigDecimal("110.4"));
|
|
|
+ point.setLatitude(new BigDecimal("28.4"));
|
|
|
+ return point;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BigDecimal decimal(String value) {
|
|
|
+ return value == null ? null : new BigDecimal(value);
|
|
|
+ }
|
|
|
+}
|