package com.zksy.cockpit.geometry; 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.CockpitCoordinateValidation; import com.zksy.cockpit.foundation.CockpitCoordinateValidator; import com.zksy.cockpit.foundation.CockpitPipeNetworkType; import com.zksy.cockpit.foundation.CockpitReadException; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Coverage; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Feature; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Geometry; import com.zksy.cockpit.geometry.CockpitFacilityGeometryResponse.Properties; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.Clock; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @Service public class CockpitFacilityGeometryService { private final PipeNetworkBaseService networks; private final GasPipePointService gasPoints; private final ManholeBaseService manholes; private final Clock clock; public CockpitFacilityGeometryService(PipeNetworkBaseService networks, GasPipePointService gasPoints, ManholeBaseService manholes, Clock clock) { this.networks = networks; this.gasPoints = gasPoints; this.manholes = manholes; this.clock = clock; } @Transactional(readOnly = true) public CockpitFacilityGeometryResponse load() { try { List features = new ArrayList<>(); // IService.list() honours each entity's @TableLogic (del_flag=0). for (PipeNetworkBase network : Objects.requireNonNull(networks.list())) { String industry = CockpitPipeNetworkType.industryOf(network.getNetworkType()); features.add(feature("pipe_network_base", network.getNetworkId(), network.getNetworkCode(), network.getNetworkName(), industry == null ? "UNKNOWN" : industry, CockpitPipeNetworkType.typeCodeOf(network.getNetworkType()), network.getNetworkType(), null, network.getNetworkId(), "LineString", Arrays.asList( pair(network.getStartLongitude(), network.getStartLatitude()), pair(network.getEndLongitude(), network.getEndLatitude())))); } for (GasPipePoint point : Objects.requireNonNull(gasPoints.list())) { features.add(feature("gas_pipe_point", String.valueOf(point.getPointId()), point.getPointCode(), point.getPointName(), "GAS", "GAS_PIPE_POINT", null, point.getPointType(), point.getNetworkId(), "Point", Collections.singletonList(pair(point.getLongitude(), point.getLatitude())))); } for (ManholeBase manhole : Objects.requireNonNull(manholes.list())) { features.add(feature("manhole_base", manhole.getManholeId(), manhole.getManholeCode(), manhole.getManholeName(), "MANHOLE", "MANHOLE", null, null, manhole.getNetworkId(), "Point", Collections.singletonList(pair(manhole.getLongitude(), manhole.getLatitude())))); } List coverage = new ArrayList<>(); for (String industry : Arrays.asList("GAS", "WATER_SUPPLY", "DRAINAGE")) { coverage.add(coverage(features, industry, "pipe_network_base", "LineString", "TWO_ENDPOINTS_ONLY")); } coverage.add(coverage(features, "GAS", "gas_pipe_point", "Point", null)); coverage.add(new Coverage("WATER_SUPPLY", null, "Point", "UNAVAILABLE", "NO_COMPONENT_SOURCE", 0, 0, 0)); coverage.add(new Coverage("DRAINAGE", null, "Point", "UNAVAILABLE", "NO_TYPED_COMPONENT_SOURCE", 0, 0, 0)); coverage.add(coverage(features, "MANHOLE", "manhole_base", "Point", "NO_COMPONENT_TYPE")); coverage.add(new Coverage("MANHOLE", null, "LineString", "UNAVAILABLE", "NO_PIPELINE_SOURCE", 0, 0, 0)); coverage.add(coverage(features, "UNKNOWN", "pipe_network_base", "LineString", "UNKNOWN_NETWORK_TYPE")); String generatedAt = DateTimeFormatter.ISO_OFFSET_DATE_TIME.format( clock.instant().atZone(ZoneId.of("Asia/Shanghai"))); return new CockpitFacilityGeometryResponse(generatedAt, features, coverage); } catch (RuntimeException exception) { // Never turn a failed source into an empty/partial successful collection. org.slf4j.LoggerFactory.getLogger(CockpitFacilityGeometryService.class) .error("Facility geometry source read failed", exception); throw new CockpitReadException("facility geometry source unavailable"); } } private static Feature feature(String source, String sourceId, String code, String name, String industry, String typeCode, String networkType, String pointType, String networkId, String geometryType, List> coordinates) { List validations = coordinates.stream() .map(pair -> validate(pair.get(0), pair.get(1))) .collect(Collectors.toList()); CockpitCoordinateValidation overall = validations.stream() .filter(validation -> "INVALID".equals(validation.getState())) .findFirst().orElse(validations.get(0)); // Invalid GeoJSON coordinates cannot be represented as an empty or invented geometry. Geometry geometry = "INVALID".equals(overall.getState()) ? null : new Geometry(geometryType, "Point".equals(geometryType) ? coordinates.get(0) : coordinates); Properties properties = new Properties(source, sourceId, code, name, industry, typeCode, networkType, pointType, networkId, coordinates, overall, validations); return new Feature(source + ":" + sourceId, geometry, properties); } private static CockpitCoordinateValidation validate(BigDecimal longitude, BigDecimal latitude) { // Structural errors must remain distinguishable even without a verification flag. CockpitCoordinateValidation structural = CockpitCoordinateValidator.validate(longitude, latitude, true); return structural.isRenderable() ? CockpitCoordinateValidator.validate(longitude, latitude, false) : structural; } private static List pair(BigDecimal longitude, BigDecimal latitude) { return Arrays.asList(longitude, latitude); } private static Coverage coverage(List features, String industry, String source, String geometryType, String reason) { List records = features.stream().filter(feature -> industry.equals(feature.getProperties().getIndustry()) && source.equals(feature.getProperties().getSource())).collect(Collectors.toList()); int geometryCount = (int) records.stream().filter(feature -> feature.getGeometry() != null).count(); int renderableCount = (int) records.stream() .filter(feature -> feature.getProperties().getCoordinateValidation().isRenderable()).count(); return new Coverage(industry, source, geometryType, records.isEmpty() ? "EMPTY" : "AVAILABLE", reason, records.size(), geometryCount, renderableCount); } }