From d18d4da086b161973a18779d1981b19be3f7f478 Mon Sep 17 00:00:00 2001 From: teddy Date: Wed, 31 Dec 2025 12:29:20 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B4=EB=85=B8=ED=8E=A8=20=EB=AA=A9?= =?UTF-8?q?=EC=97=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Innopam/InnopamApiController.java | 2 +- .../postgres/entity/DetectMastPnuEntity.java | 48 +++ .../repository/DetectMastPnuRepository.java | 7 + .../DetectMastPnuRepositoryCustom.java | 3 + .../Innopam/service/DetectMastService.java | 343 +++--------------- .../utils/GeoJsonGeometryConverter.java | 48 +++ .../Innopam/utils/GeoJsonLoader.java | 44 +++ .../kamcoback/Innopam/utils/MapIdUtils.java | 17 + .../Innopam/utils/ShpIndexManager.java | 76 ++++ .../Innopam/utils/ShpPnuMatcher.java | 42 +++ 10 files changed, 333 insertions(+), 297 deletions(-) create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java create mode 100644 src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java index f3c835da..56f027e4 100644 --- a/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/InnopamApiController.java @@ -147,6 +147,6 @@ public class InnopamApiController { detectMastSearch.setCprsBfYr(cprsBfYr); detectMastSearch.setDtctSno(Integer.parseInt(dtctSno)); detectMastSearch.setFeatureId(featureId); - return detectMastService.findPnuDataDetail(detectMastSearch); + return new FeaturePnuDto(); } } diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java new file mode 100644 index 00000000..aef47a97 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/entity/DetectMastPnuEntity.java @@ -0,0 +1,48 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; + +@Getter +@Setter +@Entity +@Table(name = "detect_mast_pnu") +public class DetectMastPnuEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "detect_mast_pnu_id_gen") + @SequenceGenerator( + name = "detect_mast_pnu_id_gen", + sequenceName = "seq_detect_mast_pnu_id", + allocationSize = 1) + @Column(name = "dtct_mst_pnu_id", nullable = false) + private Long id; + + @NotNull + @ColumnDefault("gen_random_uuid()") + @Column(name = "detect_mast_pnu_uuid", nullable = false) + private UUID detectMastPnuUuid; + + @NotNull + @Column(name = "dtct_mst_id", nullable = false) + private Long dtctMstId; + + @Size(max = 4) + @NotNull + @Column(name = "pnu", nullable = false, length = 4) + private String pnu; + + @Column(name = "polygon", length = Integer.MAX_VALUE) + private String polygon; +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java new file mode 100644 index 00000000..b44f3677 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepository.java @@ -0,0 +1,7 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +import com.kamco.cd.kamcoback.Innopam.postgres.entity.DetectMastPnuEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface DetectMastPnuRepository + extends JpaRepository, DetectMastPnuRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java new file mode 100644 index 00000000..3029c658 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/postgres/repository/DetectMastPnuRepositoryCustom.java @@ -0,0 +1,3 @@ +package com.kamco.cd.kamcoback.Innopam.postgres.repository; + +public interface DetectMastPnuRepositoryCustom {} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java index ae6f8fa8..21ea3d64 100644 --- a/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/service/DetectMastService.java @@ -1,39 +1,23 @@ package com.kamco.cd.kamcoback.Innopam.service; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.Basic; import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastReq; import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastSearch; import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto; import com.kamco.cd.kamcoback.Innopam.postgres.core.DetectMastCoreService; -import java.io.File; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Stream; import lombok.RequiredArgsConstructor; -import org.geotools.api.data.DataStore; -import org.geotools.api.data.DataStoreFinder; -import org.geotools.api.data.SimpleFeatureSource; -import org.geotools.api.feature.simple.SimpleFeature; -import org.geotools.data.simple.SimpleFeatureCollection; -import org.geotools.data.simple.SimpleFeatureIterator; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.Envelope; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.LinearRing; -import org.locationtech.jts.geom.MultiPolygon; -import org.locationtech.jts.geom.Polygon; -import org.locationtech.jts.index.strtree.STRtree; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -42,23 +26,22 @@ import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor public class DetectMastService { + @Value("${spring.profiles.active:local}") + private String profile; + private final DetectMastCoreService detectMastCoreService; - - // ✅ GeoJSON 파싱용 - private final ObjectMapper om = new ObjectMapper(); - private final GeometryFactory gf = new GeometryFactory(); - - // ✅ (임시) SHP 루트/버전/필드명 - private static final String SHP_ROOT_DIR = "/app/tmp/shp"; // TODO 환경 맞게 변경 - private static final String SHP_YYYYMM = "202512"; // TODO yml 또는 DB에서 받기 - private static final String PNU_FIELD = "PNU"; // TODO 실제 컬럼명으로 변경 - - // ✅ 시도코드별 인덱스 캐시 - private final Map shpIndexCache = new ConcurrentHashMap<>(); + private final JsonFactory jsonFactory = new JsonFactory(); @Transactional public void saveDetectMast(DetectMastReq detectMast) { detectMastCoreService.saveDetectMast(detectMast); + // + // String dirPath = + // "local".equals(profile) + // ? "/Users/bokmin/detect/result/2023_2024/4" + // : detectMast.getPathNm(); + // + // List list = this.extractFeaturePnusRandom(dirPath); } public List selectDetectMast(DetectMastSearch detectMast) { @@ -69,20 +52,19 @@ public class DetectMastService { return detectMastCoreService.selectDetectMast(id); } + /** GeoJSON → polygon_id + 랜덤 PNU */ public List findPnuData(DetectMastSearch detectMast) { - String dirPath = detectMastCoreService.findPnuData(detectMast); - return extractFeaturePnusByIntersectionMaxCached(dirPath); + + String dirPath = + "local".equals(profile) + ? "/Users/bokmin/detect/result/2023_2024/4" + : detectMastCoreService.findPnuData(detectMast); + + return extractFeaturePnusRandom(dirPath); } - public FeaturePnuDto findPnuDataDetail(DetectMastSearch detectMast) { - List list = findPnuData(detectMast); - return list.isEmpty() ? null : list.get(0); - } - - // ========================= - // ✅ 캐시 적용된 메인 로직 - // ========================= - public List extractFeaturePnusByIntersectionMaxCached(String dirPath) { + /** 하위 폴더까지 .geojson 파일들에서 polygon_id만 뽑음 병렬처리(parallel) 제거: IO + parallel은 거의 항상 느려짐 */ + private List extractFeaturePnusRandom(String dirPath) { Path basePath = Paths.get(dirPath); if (!Files.isDirectory(basePath)) { @@ -90,56 +72,32 @@ public class DetectMastService { return List.of(); } - List out = new ArrayList<>(); + List out = new ArrayList<>(4096); try (Stream stream = Files.walk(basePath)) { stream .filter(Files::isRegularFile) .filter(p -> p.toString().toLowerCase().endsWith(".geojson")) - // ✅ geojson 파싱/공간연산은 CPU+I/O 섞여서 병렬이 도움이 될 때가 많음 - .parallel() .forEach( p -> { - try { - JsonNode root = om.readTree(p.toFile()); - JsonNode features = root.path("features"); - if (!features.isArray()) { - return; - } + try (InputStream in = Files.newInputStream(p); + JsonParser parser = jsonFactory.createParser(in)) { - for (JsonNode feature : features) { - String polygonId = feature.path("properties").path("polygon_id").asText(null); - if (polygonId == null || polygonId.isBlank()) { - continue; - } + while (parser.nextToken() != null) { + if (parser.currentToken() == JsonToken.FIELD_NAME + && "polygon_id".equals(parser.getCurrentName())) { - Geometry target = toJtsGeometry(feature.path("geometry")); - if (target == null || target.isEmpty()) { - continue; - } - - // ✅ 1) target의 중심점으로 시도코드 판정 → 해당 시도 SHP만 사용 - // (시도코드 판정 방법은 프로젝트에 맞게 바꿀 수 있음) - String sidoCode = resolveSidoCodeByPoint(target.getCentroid().getCoordinate()); - if (sidoCode == null) { - continue; - } - - ShpIndex idx = getOrLoadShpIndex(sidoCode); - if (idx == null) { - continue; - } - - String pnu = pickPnuByIntersectionMax(idx.index, target); - if (pnu != null) { - synchronized (out) { - out.add(new FeaturePnuDto(polygonId, pnu)); + JsonToken next = parser.nextToken(); // 값으로 이동 + if (next == JsonToken.VALUE_STRING) { + String polygonId = parser.getValueAsString(); + out.add(new FeaturePnuDto(polygonId, randomPnu())); } } } } catch (Exception e) { - System.err.println("GeoJSON 처리 실패: " + p.getFileName() + " / " + e.getMessage()); + // 파일 단위 실패는 최소 로그 + System.err.println("GeoJSON 파싱 실패: " + p.getFileName() + " / " + e.getMessage()); } }); @@ -151,222 +109,15 @@ public class DetectMastService { return out; } - // ========================= - // ✅ SHP 인덱스 캐시 로딩 - // ========================= + /** 랜덤 PNU 생성 (임시) - 법정동코드(5) + 산구분(1) + 본번(4) + 부번(4) = 14자리 */ + private String randomPnu() { + ThreadLocalRandom r = ThreadLocalRandom.current(); - private static class ShpIndex { + String dongCode = String.format("%05d", r.nextInt(10000, 99999)); + String san = r.nextBoolean() ? "1" : "2"; + String bon = String.format("%04d", r.nextInt(1, 10000)); + String bu = String.format("%04d", r.nextInt(0, 10000)); - final String sidoCode; - final STRtree index; - - ShpIndex(String sidoCode, STRtree index) { - this.sidoCode = sidoCode; - this.index = index; - } - } - - private ShpIndex getOrLoadShpIndex(String sidoCode) { - // computeIfAbsent는 동시성 상황에서 안전하게 "1번만" 만들게 해줌 - return shpIndexCache.computeIfAbsent(sidoCode, this::loadShpIndex); - } - - private ShpIndex loadShpIndex(String sidoCode) { - String shpPath = resolveShpPathBySido(sidoCode); - - File shpFile = new File(shpPath); - if (!shpFile.exists()) { - System.err.println("SHP 파일 없음: " + shpPath); - return null; - } - - DataStore store = null; - - try { - Map params = new HashMap<>(); - params.put("url", shpFile.toURI().toURL()); - - store = DataStoreFinder.getDataStore(params); - if (store == null) { - System.err.println("SHP DataStore 생성 실패: " + shpPath); - return null; - } - - String typeName = store.getTypeNames()[0]; - SimpleFeatureSource source = store.getFeatureSource(typeName); - SimpleFeatureCollection collection = source.getFeatures(); - - STRtree index = new STRtree(10); - - try (SimpleFeatureIterator it = collection.features()) { - while (it.hasNext()) { - SimpleFeature f = it.next(); - - Object g = f.getDefaultGeometry(); - if (!(g instanceof Geometry)) { - continue; - } - - String pnu = Objects.toString(f.getAttribute(PNU_FIELD), null); - if (pnu == null || pnu.isBlank()) { - continue; - } - - Geometry geom = (Geometry) g; - index.insert(geom.getEnvelopeInternal(), new ShpRow(geom, pnu)); - } - } - - index.build(); - System.out.println("✅ SHP 인덱스 로딩 완료: sido=" + sidoCode + ", shp=" + shpFile.getName()); - return new ShpIndex(sidoCode, index); - - } catch (Exception e) { - System.err.println("SHP 인덱스 로딩 실패: sido=" + sidoCode + " / " + e.getMessage()); - return null; - } finally { - if (store != null) { - store.dispose(); - } - } - } - - private String resolveShpPathBySido(String sidoCode) { - // 파일명이 스크린샷처럼 동일 패턴이라고 가정 - // 예: /shp/LSMD_CONT_LDREG_11_202512.shp - return SHP_ROOT_DIR + "/LSMD_CONT_LDREG_" + sidoCode + "_" + SHP_YYYYMM + ".shp"; - } - - // ========================= - // ✅ intersection 면적 최대 - // ========================= - - private static class ShpRow { - - final Geometry geom; - final String pnu; - - ShpRow(Geometry geom, String pnu) { - this.geom = geom; - this.pnu = pnu; - } - } - - private String pickPnuByIntersectionMax(STRtree index, Geometry target) { - - Envelope env = target.getEnvelopeInternal(); - - @SuppressWarnings("unchecked") - List candidates = index.query(env); - if (candidates == null || candidates.isEmpty()) { - return null; - } - - double bestArea = 0.0; - String bestPnu = null; - - for (ShpRow row : candidates) { - Geometry parcel = row.geom; - - if (!parcel.intersects(target)) { - continue; - } - - Geometry inter = parcel.intersection(target); - double area = inter.getArea(); - - if (area > bestArea) { - bestArea = area; - bestPnu = row.pnu; - } - } - - return bestPnu; - } - - // ========================= - // ✅ GeoJSON geometry -> JTS - // ========================= - - private Geometry toJtsGeometry(JsonNode geomNode) { - String type = geomNode.path("type").asText(""); - JsonNode coords = geomNode.path("coordinates"); - - try { - if ("Polygon".equalsIgnoreCase(type)) { - return toPolygon(coords); - } - if ("MultiPolygon".equalsIgnoreCase(type)) { - return toMultiPolygon(coords); - } - return null; - } catch (Exception e) { - return null; - } - } - - private Polygon toPolygon(JsonNode coords) { - LinearRing shell = gf.createLinearRing(toCoordinateArray(coords.get(0))); - LinearRing[] holes = null; - - if (coords.size() > 1) { - holes = new LinearRing[coords.size() - 1]; - for (int i = 1; i < coords.size(); i++) { - holes[i - 1] = gf.createLinearRing(toCoordinateArray(coords.get(i))); - } - } - return gf.createPolygon(shell, holes); - } - - private MultiPolygon toMultiPolygon(JsonNode coords) { - Polygon[] polys = new Polygon[coords.size()]; - for (int i = 0; i < coords.size(); i++) { - polys[i] = toPolygon(coords.get(i)); - } - return gf.createMultiPolygon(polys); - } - - private Coordinate[] toCoordinateArray(JsonNode ring) { - int n = ring.size(); - Coordinate[] c = new Coordinate[n]; - - for (int i = 0; i < n; i++) { - double x = ring.get(i).get(0).asDouble(); - double y = ring.get(i).get(1).asDouble(); - c[i] = new Coordinate(x, y); - } - - // 링 닫힘 보장 - if (n >= 2 && !c[0].equals2D(c[n - 1])) { - Coordinate[] closed = Arrays.copyOf(c, n + 1); - closed[n] = new Coordinate(c[0]); - return closed; - } - return c; - } - - // ========================= - // ✅ 시도코드 판정 (임시) - // ========================= - private String resolveSidoCodeByPoint(Coordinate c) { - // ⚠️ 여기만 프로젝트 환경에 맞게 바꾸면 됩니다. - // 지금은 "임시로 하나 고정" 같은 방식 말고, - // 실제론 detectMastSearch나 map_id 기반으로 시도코드를 구하는 게 제일 안정적입니다. - - // 1) 만약 detectMastSearch에 sidoCode가 있다면 그걸 쓰는 걸 추천 - // 2) 또는 map_id에서 앞 2자리를 쓰는 방식 - - // 임시: 서울만 처리 (필요한 코드 추가하세요) - // return "11"; - - // TODO: 현재는 null 반환 -> 실제 로직으로 교체 필요 - return "11"; - } - - // ========================= - // ✅ 캐시 수동 초기화 (필요 시) - // ========================= - public void clearShpCache() { - shpIndexCache.clear(); + return dongCode + san + bon + bu; } } diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java new file mode 100644 index 00000000..c1d8842e --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonGeometryConverter.java @@ -0,0 +1,48 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Polygon; + +public class GeoJsonGeometryConverter { + + private static final GeometryFactory GF = new GeometryFactory(); + + public static Geometry toGeometry(JsonNode geomNode) { + String type = geomNode.path("type").asText(); + + if ("Polygon".equals(type)) { + return toPolygon(geomNode.path("coordinates")); + } + if ("MultiPolygon".equals(type)) { + return toMultiPolygon(geomNode.path("coordinates")); + } + return null; + } + + private static Polygon toPolygon(JsonNode coords) { + LinearRing shell = GF.createLinearRing(toCoords(coords.get(0))); + return GF.createPolygon(shell); + } + + private static MultiPolygon toMultiPolygon(JsonNode coords) { + Polygon[] polys = new Polygon[coords.size()]; + for (int i = 0; i < coords.size(); i++) { + polys[i] = toPolygon(coords.get(i)); + } + return GF.createMultiPolygon(polys); + } + + private static Coordinate[] toCoords(JsonNode ring) { + Coordinate[] c = new Coordinate[ring.size() + 1]; + for (int i = 0; i < ring.size(); i++) { + c[i] = new Coordinate(ring.get(i).get(0).asDouble(), ring.get(i).get(1).asDouble()); + } + c[c.length - 1] = c[0]; + return c; + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java new file mode 100644 index 00000000..879cdea4 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/GeoJsonLoader.java @@ -0,0 +1,44 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class GeoJsonLoader { + + private final ObjectMapper om = new ObjectMapper(); + + public GeoJsonFile load(File geoJsonFile) throws Exception { + + JsonNode root = om.readTree(geoJsonFile); + + long mapId = root.path("properties").path("map_id").asLong(-1); + if (mapId <= 0) { + throw new IllegalStateException( + "GeoJSON top-level properties.map_id 없음: " + geoJsonFile.getName()); + } + + List features = new ArrayList<>(); + root.path("features").forEach(features::add); + + return new GeoJsonFile(mapId, features); + } + + /** ✅ feature에서 polygon_id 추출 */ + public static String polygonId(JsonNode feature) { + return feature.path("properties").path("polygon_id").asText(null); + } + + public static class GeoJsonFile { + + public final long mapId; + public final List features; + + public GeoJsonFile(long mapId, List features) { + this.mapId = mapId; + this.features = features; + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java new file mode 100644 index 00000000..2ab92670 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/MapIdUtils.java @@ -0,0 +1,17 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +public class MapIdUtils { + + private MapIdUtils() { + // util class + } + + /** map_id → 시도코드 예: 34602060 → "34" */ + public static String sidoCodeFromMapId(long mapId) { + String s = String.valueOf(mapId); + if (s.length() < 2) { + throw new IllegalArgumentException("잘못된 map_id: " + mapId); + } + return s.substring(0, 2); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java new file mode 100644 index 00000000..60100995 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpIndexManager.java @@ -0,0 +1,76 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import java.io.File; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import org.geotools.api.data.DataStore; +import org.geotools.api.data.DataStoreFinder; +import org.geotools.api.data.SimpleFeatureSource; +import org.geotools.api.feature.simple.SimpleFeature; +import org.geotools.data.simple.SimpleFeatureCollection; +import org.geotools.data.simple.SimpleFeatureIterator; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.index.strtree.STRtree; + +public class ShpIndexManager { + + private static final String SHP_ROOT = "/shp"; + private static final String SHP_YYYYMM = "202512"; + private static final String PNU_FIELD = "PNU"; + + private final Map cache = new ConcurrentHashMap<>(); + + public STRtree getIndex(String sidoCode) { + return cache.computeIfAbsent(sidoCode, this::loadIndex); + } + + private STRtree loadIndex(String sidoCode) { + try { + String path = SHP_ROOT + "/LSMD_CONT_LDREG_" + sidoCode + "_" + SHP_YYYYMM + ".shp"; + + File shp = new File(path); + if (!shp.exists()) { + return null; + } + + STRtree index = new STRtree(10); + + DataStore store = DataStoreFinder.getDataStore(Map.of("url", shp.toURI().toURL())); + + String typeName = store.getTypeNames()[0]; + SimpleFeatureSource source = store.getFeatureSource(typeName); + SimpleFeatureCollection col = source.getFeatures(); + + try (SimpleFeatureIterator it = col.features()) { + while (it.hasNext()) { + SimpleFeature f = it.next(); + Geometry geom = (Geometry) f.getDefaultGeometry(); + String pnu = Objects.toString(f.getAttribute(PNU_FIELD), null); + if (geom != null && pnu != null) { + index.insert(geom.getEnvelopeInternal(), new ShpRow(geom, pnu)); + } + } + } + + index.build(); + store.dispose(); + return index; + + } catch (Exception e) { + return null; + } + } + + /** SHP 한 row */ + public static class ShpRow { + + public final Geometry geom; + public final String pnu; + + public ShpRow(Geometry geom, String pnu) { + this.geom = geom; + this.pnu = pnu; + } + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java new file mode 100644 index 00000000..f61d3852 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/Innopam/utils/ShpPnuMatcher.java @@ -0,0 +1,42 @@ +package com.kamco.cd.kamcoback.Innopam.utils; + +import java.util.List; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.prep.PreparedGeometry; +import org.locationtech.jts.geom.prep.PreparedGeometryFactory; +import org.locationtech.jts.index.strtree.STRtree; + +public class ShpPnuMatcher { + + public static String pickByIntersectionMax(STRtree index, Geometry target) { + + Envelope env = target.getEnvelopeInternal(); + + @SuppressWarnings("unchecked") + List rows = index.query(env); + + double best = 0; + String bestPnu = null; + + for (ShpIndexManager.ShpRow row : rows) { + + PreparedGeometry prep = PreparedGeometryFactory.prepare(row.geom); + + if (prep.contains(target) || prep.covers(target)) { + return row.pnu; + } + + if (!prep.intersects(target)) { + continue; + } + + double area = row.geom.intersection(target).getArea(); + if (area > best) { + best = area; + bestPnu = row.pnu; + } + } + return bestPnu; + } +}