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 types = Arrays.asList("阀门", "调压", "计量", "阴极保护", "弯头", "三通", "其它", "未知原始值", null); List 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); } }