이노펨 목업 수정
This commit is contained in:
@@ -147,6 +147,6 @@ public class InnopamApiController {
|
|||||||
detectMastSearch.setCprsBfYr(cprsBfYr);
|
detectMastSearch.setCprsBfYr(cprsBfYr);
|
||||||
detectMastSearch.setDtctSno(Integer.parseInt(dtctSno));
|
detectMastSearch.setDtctSno(Integer.parseInt(dtctSno));
|
||||||
detectMastSearch.setFeatureId(featureId);
|
detectMastSearch.setFeatureId(featureId);
|
||||||
return detectMastService.findPnuDataDetail(detectMastSearch);
|
return new FeaturePnuDto();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<DetectMastPnuEntity, Long>, DetectMastPnuRepositoryCustom {}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package com.kamco.cd.kamcoback.Innopam.postgres.repository;
|
||||||
|
|
||||||
|
public interface DetectMastPnuRepositoryCustom {}
|
||||||
@@ -1,39 +1,23 @@
|
|||||||
package com.kamco.cd.kamcoback.Innopam.service;
|
package com.kamco.cd.kamcoback.Innopam.service;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.core.JsonFactory;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
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.Basic;
|
||||||
import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.DetectMastReq;
|
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.DetectMastSearch;
|
||||||
import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto;
|
import com.kamco.cd.kamcoback.Innopam.dto.DetectMastDto.FeaturePnuDto;
|
||||||
import com.kamco.cd.kamcoback.Innopam.postgres.core.DetectMastCoreService;
|
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.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.geotools.api.data.DataStore;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
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.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -42,23 +26,22 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DetectMastService {
|
public class DetectMastService {
|
||||||
|
|
||||||
|
@Value("${spring.profiles.active:local}")
|
||||||
|
private String profile;
|
||||||
|
|
||||||
private final DetectMastCoreService detectMastCoreService;
|
private final DetectMastCoreService detectMastCoreService;
|
||||||
|
private final JsonFactory jsonFactory = new JsonFactory();
|
||||||
// ✅ 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<String, ShpIndex> shpIndexCache = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void saveDetectMast(DetectMastReq detectMast) {
|
public void saveDetectMast(DetectMastReq detectMast) {
|
||||||
detectMastCoreService.saveDetectMast(detectMast);
|
detectMastCoreService.saveDetectMast(detectMast);
|
||||||
|
//
|
||||||
|
// String dirPath =
|
||||||
|
// "local".equals(profile)
|
||||||
|
// ? "/Users/bokmin/detect/result/2023_2024/4"
|
||||||
|
// : detectMast.getPathNm();
|
||||||
|
//
|
||||||
|
// List<FeaturePnuDto> list = this.extractFeaturePnusRandom(dirPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Basic> selectDetectMast(DetectMastSearch detectMast) {
|
public List<Basic> selectDetectMast(DetectMastSearch detectMast) {
|
||||||
@@ -69,20 +52,19 @@ public class DetectMastService {
|
|||||||
return detectMastCoreService.selectDetectMast(id);
|
return detectMastCoreService.selectDetectMast(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** GeoJSON → polygon_id + 랜덤 PNU */
|
||||||
public List<FeaturePnuDto> findPnuData(DetectMastSearch detectMast) {
|
public List<FeaturePnuDto> 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) {
|
/** 하위 폴더까지 .geojson 파일들에서 polygon_id만 뽑음 병렬처리(parallel) 제거: IO + parallel은 거의 항상 느려짐 */
|
||||||
List<FeaturePnuDto> list = findPnuData(detectMast);
|
private List<FeaturePnuDto> extractFeaturePnusRandom(String dirPath) {
|
||||||
return list.isEmpty() ? null : list.get(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================
|
|
||||||
// ✅ 캐시 적용된 메인 로직
|
|
||||||
// =========================
|
|
||||||
public List<FeaturePnuDto> extractFeaturePnusByIntersectionMaxCached(String dirPath) {
|
|
||||||
|
|
||||||
Path basePath = Paths.get(dirPath);
|
Path basePath = Paths.get(dirPath);
|
||||||
if (!Files.isDirectory(basePath)) {
|
if (!Files.isDirectory(basePath)) {
|
||||||
@@ -90,56 +72,32 @@ public class DetectMastService {
|
|||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<FeaturePnuDto> out = new ArrayList<>();
|
List<FeaturePnuDto> out = new ArrayList<>(4096);
|
||||||
|
|
||||||
try (Stream<Path> stream = Files.walk(basePath)) {
|
try (Stream<Path> stream = Files.walk(basePath)) {
|
||||||
stream
|
stream
|
||||||
.filter(Files::isRegularFile)
|
.filter(Files::isRegularFile)
|
||||||
.filter(p -> p.toString().toLowerCase().endsWith(".geojson"))
|
.filter(p -> p.toString().toLowerCase().endsWith(".geojson"))
|
||||||
// ✅ geojson 파싱/공간연산은 CPU+I/O 섞여서 병렬이 도움이 될 때가 많음
|
|
||||||
.parallel()
|
|
||||||
.forEach(
|
.forEach(
|
||||||
p -> {
|
p -> {
|
||||||
try {
|
try (InputStream in = Files.newInputStream(p);
|
||||||
JsonNode root = om.readTree(p.toFile());
|
JsonParser parser = jsonFactory.createParser(in)) {
|
||||||
JsonNode features = root.path("features");
|
|
||||||
if (!features.isArray()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (JsonNode feature : features) {
|
while (parser.nextToken() != null) {
|
||||||
String polygonId = feature.path("properties").path("polygon_id").asText(null);
|
if (parser.currentToken() == JsonToken.FIELD_NAME
|
||||||
if (polygonId == null || polygonId.isBlank()) {
|
&& "polygon_id".equals(parser.getCurrentName())) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Geometry target = toJtsGeometry(feature.path("geometry"));
|
JsonToken next = parser.nextToken(); // 값으로 이동
|
||||||
if (target == null || target.isEmpty()) {
|
if (next == JsonToken.VALUE_STRING) {
|
||||||
continue;
|
String polygonId = parser.getValueAsString();
|
||||||
}
|
out.add(new FeaturePnuDto(polygonId, randomPnu()));
|
||||||
|
|
||||||
// ✅ 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} 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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
/** 랜덤 PNU 생성 (임시) - 법정동코드(5) + 산구분(1) + 본번(4) + 부번(4) = 14자리 */
|
||||||
// ✅ SHP 인덱스 캐시 로딩
|
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;
|
return dongCode + san + bon + bu;
|
||||||
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<String, Object> 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<ShpRow> 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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<JsonNode> 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<JsonNode> features;
|
||||||
|
|
||||||
|
public GeoJsonFile(long mapId, List<JsonNode> features) {
|
||||||
|
this.mapId = mapId;
|
||||||
|
this.features = features;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, STRtree> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ShpIndexManager.ShpRow> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user