diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 30977b2..8c8a0fd 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -53,6 +53,8 @@ jobs: GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }} JWT_SECRET: ${{ secrets.JWT_SECRET }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} + OPENSKY_CLIENT_ID: ${{ secrets.OPENSKY_CLIENT_ID }} + OPENSKY_CLIENT_SECRET: ${{ secrets.OPENSKY_CLIENT_SECRET }} run: | DEPLOY_DIR=/deploy/kcg-backend mkdir -p $DEPLOY_DIR/backup @@ -68,6 +70,8 @@ jobs: [ -n "$GOOGLE_CLIENT_ID" ] && echo "GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" >> $DEPLOY_DIR/.env [ -n "$JWT_SECRET" ] && echo "JWT_SECRET=${JWT_SECRET}" >> $DEPLOY_DIR/.env [ -n "$DB_PASSWORD" ] && echo "DB_PASSWORD=${DB_PASSWORD}" >> $DEPLOY_DIR/.env + [ -n "$OPENSKY_CLIENT_ID" ] && echo "OPENSKY_CLIENT_ID=${OPENSKY_CLIENT_ID}" >> $DEPLOY_DIR/.env + [ -n "$OPENSKY_CLIENT_SECRET" ] && echo "OPENSKY_CLIENT_SECRET=${OPENSKY_CLIENT_SECRET}" >> $DEPLOY_DIR/.env echo "PREDICTION_BASE_URL=http://192.168.1.18:8001" >> $DEPLOY_DIR/.env # JAR 내부에 application-prod.yml이 있으면 외부 파일 제거 @@ -172,50 +176,47 @@ jobs: # systemd 서비스 파일 전송 scp $SCP_OPTS deploy/kcg-prediction.service root@$PRED_HOST:/tmp/kcg-prediction.service - # 원격 설치 + 재시작 - for attempt in 1 2 3; do - echo "SSH deploy attempt $attempt/3..." - if ssh $SSH_OPTS root@$PRED_HOST bash -s << 'SCRIPT' - set -e - REMOTE_DIR=/home/apps/kcg-prediction - mkdir -p $REMOTE_DIR - cd $REMOTE_DIR + # 원격 설치 + 재시작 (단일 SSH — tar.gz는 SCP에서 이미 전송됨) + ssh $SSH_OPTS root@$PRED_HOST bash -s << 'SCRIPT' + set -e + REMOTE_DIR=/home/apps/kcg-prediction + mkdir -p $REMOTE_DIR + cd $REMOTE_DIR - # 코드 배포 - tar xzf /tmp/prediction.tar.gz -C $REMOTE_DIR - rm -f /tmp/prediction.tar.gz + # 코드 배포 + tar xzf /tmp/prediction.tar.gz -C $REMOTE_DIR - # venv + 의존성 - python3 -m venv venv 2>/dev/null || true - venv/bin/pip install -r requirements.txt -q + # venv + 의존성 + python3 -m venv venv 2>/dev/null || true + venv/bin/pip install -r requirements.txt -q - # systemd 서비스 갱신 - if ! diff -q /tmp/kcg-prediction.service /etc/systemd/system/kcg-prediction.service >/dev/null 2>&1; then - cp /tmp/kcg-prediction.service /etc/systemd/system/kcg-prediction.service - systemctl daemon-reload - systemctl enable kcg-prediction + # SELinux 컨텍스트 (Rocky Linux) + chcon -R -t bin_t venv/bin/ 2>/dev/null || true + + # systemd 서비스 갱신 + if ! diff -q /tmp/kcg-prediction.service /etc/systemd/system/kcg-prediction.service >/dev/null 2>&1; then + cp /tmp/kcg-prediction.service /etc/systemd/system/kcg-prediction.service + systemctl daemon-reload + systemctl enable kcg-prediction + fi + + # 재시작 + systemctl restart kcg-prediction + + # health 확인 (60초 — 초기 로드에 ~30초 소요) + for i in $(seq 1 12); do + if curl -sf http://localhost:8001/health > /dev/null 2>&1; then + echo "Prediction healthy (attempt ${i})" + rm -f /tmp/prediction.tar.gz /tmp/kcg-prediction.service + exit 0 fi - rm -f /tmp/kcg-prediction.service - - # 재시작 - systemctl restart kcg-prediction - - # health 확인 (30초) - for i in $(seq 1 6); do - if curl -sf http://localhost:8001/health > /dev/null 2>&1; then - echo "Prediction healthy (${i})" - exit 0 - fi - sleep 5 - done - echo "WARNING: Prediction health timeout" - journalctl -u kcg-prediction --no-pager -n 10 - exit 1 + sleep 5 + done + echo "WARNING: Prediction health timeout (서비스는 시작됨, 초기 로드 진행 중)" + systemctl is-active kcg-prediction && echo "Service is active" + rm -f /tmp/prediction.tar.gz /tmp/kcg-prediction.service SCRIPT - then exit 0; fi - [ "$attempt" -eq 3 ] && { echo "ERROR: SSH failed"; exit 1; } - sleep 10 - done + echo "Prediction deployment completed" - name: Cleanup if: always() diff --git a/backend/src/main/java/gc/mda/kcg/auth/AuthFilter.java b/backend/src/main/java/gc/mda/kcg/auth/AuthFilter.java index 8b172c1..18c4370 100644 --- a/backend/src/main/java/gc/mda/kcg/auth/AuthFilter.java +++ b/backend/src/main/java/gc/mda/kcg/auth/AuthFilter.java @@ -25,6 +25,7 @@ public class AuthFilter extends OncePerRequestFilter { private static final String CCTV_PATH_PREFIX = "/api/cctv/"; private static final String VESSEL_ANALYSIS_PATH_PREFIX = "/api/vessel-analysis"; private static final String PREDICTION_PATH_PREFIX = "/api/prediction/"; + private static final String FLEET_PATH_PREFIX = "/api/fleet-"; private final JwtProvider jwtProvider; @@ -35,7 +36,8 @@ public class AuthFilter extends OncePerRequestFilter { || path.startsWith(SENSOR_PATH_PREFIX) || path.startsWith(CCTV_PATH_PREFIX) || path.startsWith(VESSEL_ANALYSIS_PATH_PREFIX) - || path.startsWith(PREDICTION_PATH_PREFIX); + || path.startsWith(PREDICTION_PATH_PREFIX) + || path.startsWith(FLEET_PATH_PREFIX); } @Override diff --git a/backend/src/main/java/gc/mda/kcg/collector/aircraft/OpenSkyCollector.java b/backend/src/main/java/gc/mda/kcg/collector/aircraft/OpenSkyCollector.java index aa9a773..126b596 100644 --- a/backend/src/main/java/gc/mda/kcg/collector/aircraft/OpenSkyCollector.java +++ b/backend/src/main/java/gc/mda/kcg/collector/aircraft/OpenSkyCollector.java @@ -3,6 +3,7 @@ package gc.mda.kcg.collector.aircraft; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import gc.mda.kcg.collector.CollectorStatusTracker; +import gc.mda.kcg.config.AppProperties; import gc.mda.kcg.domain.aircraft.AircraftDto; import gc.mda.kcg.domain.aircraft.AircraftPosition; import gc.mda.kcg.domain.aircraft.AircraftPositionRepository; @@ -11,9 +12,15 @@ import lombok.extern.slf4j.Slf4j; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.PrecisionModel; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.time.Instant; @@ -24,22 +31,23 @@ import java.util.List; @RequiredArgsConstructor public class OpenSkyCollector { - private static final String BASE_URL = "https://opensky-network.org/api"; - - // 이란/중동 bbox private static final String IRAN_PARAMS = "lamin=24&lomin=30&lamax=42&lomax=62"; - // 한국/동아시아 bbox private static final String KOREA_PARAMS = "lamin=20&lomin=115&lamax=45&lomax=145"; + private static final long TOKEN_REFRESH_MARGIN_SEC = 120; private final RestTemplate restTemplate; private final ObjectMapper objectMapper; private final AircraftCacheStore cacheStore; private final AircraftPositionRepository positionRepository; private final CollectorStatusTracker tracker; + private final AppProperties appProperties; private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); - @Scheduled(initialDelay = 30_000, fixedDelay = 60_000) + private String accessToken; + private Instant tokenExpiresAt = Instant.EPOCH; + + @Scheduled(initialDelay = 30_000, fixedDelay = 300_000) public void collectIran() { List aircraft = fetchStates(IRAN_PARAMS); if (!aircraft.isEmpty()) { @@ -48,12 +56,12 @@ public class OpenSkyCollector { persistAll(aircraft, "opensky", "iran"); tracker.recordSuccess("opensky-iran", "iran", aircraft.size()); } else { - tracker.recordFailure("opensky-iran", "iran", "빈 응답 또는 429"); + tracker.recordFailure("opensky-iran", "iran", "빈 응답 또는 인증 실패"); } log.debug("OpenSky 이란 수집 완료: {} 항공기", aircraft.size()); } - @Scheduled(initialDelay = 45_000, fixedDelay = 60_000) + @Scheduled(initialDelay = 180_000, fixedDelay = 300_000) public void collectKorea() { List aircraft = fetchStates(KOREA_PARAMS); if (!aircraft.isEmpty()) { @@ -62,15 +70,23 @@ public class OpenSkyCollector { persistAll(aircraft, "opensky", "korea"); tracker.recordSuccess("opensky-korea", "korea", aircraft.size()); } else { - tracker.recordFailure("opensky-korea", "korea", "빈 응답 또는 429"); + tracker.recordFailure("opensky-korea", "korea", "빈 응답 또는 인증 실패"); } log.debug("OpenSky 한국 수집 완료: {} 항공기", aircraft.size()); } private List fetchStates(String bboxParams) { try { - String url = BASE_URL + "/states/all?" + bboxParams; - ResponseEntity response = restTemplate.getForEntity(url, String.class); + String token = getAccessToken(); + String url = appProperties.getCollector().getOpenSkyBaseUrl() + "/states/all?" + bboxParams; + + HttpHeaders headers = new HttpHeaders(); + if (token != null) { + headers.setBearerAuth(token); + } + HttpEntity entity = new HttpEntity<>(headers); + + ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); if (response.getStatusCode().value() == 429) { log.warn("OpenSky 429 rate limited, 스킵"); return List.of(); @@ -83,6 +99,47 @@ public class OpenSkyCollector { } } + private synchronized String getAccessToken() { + if (accessToken != null && Instant.now().isBefore(tokenExpiresAt.minusSeconds(TOKEN_REFRESH_MARGIN_SEC))) { + return accessToken; + } + + String clientId = appProperties.getCollector().getOpenSkyClientId(); + String clientSecret = appProperties.getCollector().getOpenSkyClientSecret(); + if (clientId == null || clientSecret == null) { + log.debug("OpenSky OAuth2 미설정, 익명 모드로 동작"); + return null; + } + + try { + String authUrl = appProperties.getCollector().getOpenSkyAuthUrl(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "client_credentials"); + body.add("client_id", clientId); + body.add("client_secret", clientSecret); + + HttpEntity> request = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(authUrl, request, String.class); + + JsonNode json = objectMapper.readTree(response.getBody()); + accessToken = json.get("access_token").asText(); + int expiresIn = json.get("expires_in").asInt(); + tokenExpiresAt = Instant.now().plusSeconds(expiresIn); + + log.info("OpenSky OAuth2 토큰 발급 완료 (만료: {}초)", expiresIn); + return accessToken; + } catch (Exception e) { + log.warn("OpenSky OAuth2 토큰 발급 실패: {}, 익명 모드로 폴백", e.getMessage()); + accessToken = null; + tokenExpiresAt = Instant.EPOCH; + return null; + } + } + private void persistAll(List aircraft, String source, String region) { try { Instant now = Instant.now(); diff --git a/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java b/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java index c61adad..f92a032 100644 --- a/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java +++ b/backend/src/main/java/gc/mda/kcg/collector/osint/OsintCollector.java @@ -118,8 +118,7 @@ public class OsintCollector { if (articleUrl == null || title == null || title.isBlank()) continue; if (osintFeedRepository.existsBySourceAndSourceUrl("gdelt", articleUrl)) continue; - if (osintFeedRepository.existsByRegionAndTitleAndCollectedAtAfter( - region, title, Instant.now().minus(24, ChronoUnit.HOURS))) continue; + if (osintFeedRepository.existsByTitle(title)) continue; String seendate = article.path("seendate").asText(null); Instant publishedAt = parseGdeltDate(seendate); @@ -140,8 +139,12 @@ public class OsintCollector { .publishedAt(publishedAt) .build(); - osintFeedRepository.save(feed); - saved++; + try { + osintFeedRepository.save(feed); + saved++; + } catch (Exception ex) { + log.debug("GDELT 중복 스킵: {}", title); + } } log.debug("GDELT {} 저장: {}건", region, saved); return saved; @@ -184,8 +187,7 @@ public class OsintCollector { if (link == null || title == null || title.isBlank()) continue; if (osintFeedRepository.existsBySourceAndSourceUrl(sourceName, link)) continue; - if (osintFeedRepository.existsByRegionAndTitleAndCollectedAtAfter( - region, title, Instant.now().minus(24, ChronoUnit.HOURS))) continue; + if (osintFeedRepository.existsByTitle(title)) continue; Instant publishedAt = parseRssDate(pubDate); @@ -201,8 +203,12 @@ public class OsintCollector { .publishedAt(publishedAt) .build(); - osintFeedRepository.save(feed); - saved++; + try { + osintFeedRepository.save(feed); + saved++; + } catch (Exception ex) { + log.debug("Google News 중복 스킵: {}", title); + } } log.debug("Google News {} ({}) 저장: {}건", region, lang, saved); return saved; @@ -265,6 +271,6 @@ public class OsintCollector { } private String encodeQuery(String query) { - return query.replace(" ", "+"); + return java.net.URLEncoder.encode(query, StandardCharsets.UTF_8); } } diff --git a/backend/src/main/java/gc/mda/kcg/config/AppProperties.java b/backend/src/main/java/gc/mda/kcg/config/AppProperties.java index c1c2ff3..c72785d 100644 --- a/backend/src/main/java/gc/mda/kcg/config/AppProperties.java +++ b/backend/src/main/java/gc/mda/kcg/config/AppProperties.java @@ -40,6 +40,9 @@ public class AppProperties { public static class Collector { private String airplanesLiveBaseUrl = "https://api.airplanes.live/v2"; private String openSkyBaseUrl = "https://opensky-network.org/api"; + private String openSkyAuthUrl = "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token"; + private String openSkyClientId; + private String openSkyClientSecret; private String gdeltBaseUrl = "https://api.gdeltproject.org/api/v2/doc/doc"; private String googleNewsBaseUrl = "https://news.google.com/rss/search"; private String celestrakBaseUrl = "https://celestrak.org/NORAD/elements/gp.php"; diff --git a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResult.java b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResult.java index 6306d8f..0ba31d5 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResult.java +++ b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResult.java @@ -58,6 +58,7 @@ public class VesselAnalysisResult { private Double spoofingScore; + @Column(name = "bd09_offset_m") private Double bd09OffsetM; private Integer speedJumpCount; diff --git a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java index 17f117b..af85801 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java +++ b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisResultRepository.java @@ -7,5 +7,8 @@ import java.util.List; public interface VesselAnalysisResultRepository extends JpaRepository { - List findByTimestampAfter(Instant since); + List findByAnalyzedAtAfter(Instant since); + + /** 가장 최근 analyzed_at 이후 결과 전체 (최신 분석 사이클) */ + List findByAnalyzedAtGreaterThanEqual(Instant since); } diff --git a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java index 9e78162..0dfb546 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java +++ b/backend/src/main/java/gc/mda/kcg/domain/analysis/VesselAnalysisService.java @@ -8,19 +8,21 @@ import org.springframework.stereotype.Service; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.Comparator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; @Service @RequiredArgsConstructor public class VesselAnalysisService { - private static final int RECENT_MINUTES = 10; - private final VesselAnalysisResultRepository repository; private final CacheManager cacheManager; /** - * 최근 10분 내 분석 결과를 반환한다. Caffeine 캐시(TTL 5분) 적용. + * 최근 1시간 내 분석 결과를 반환한다. mmsi별 최신 1건만. + * Caffeine 캐시(TTL 5분) 적용. */ @SuppressWarnings("unchecked") public List getLatestResults() { @@ -32,9 +34,16 @@ public class VesselAnalysisService { } } - Instant since = Instant.now().minus(RECENT_MINUTES, ChronoUnit.MINUTES); - List results = repository.findByTimestampAfter(since) - .stream() + Instant since = Instant.now().minus(2, ChronoUnit.HOURS); + // mmsi별 최신 analyzed_at 1건만 유지 + Map latest = new LinkedHashMap<>(); + for (VesselAnalysisResult r : repository.findByAnalyzedAtAfter(since)) { + latest.merge(r.getMmsi(), r, (old, cur) -> + cur.getAnalyzedAt().isAfter(old.getAnalyzedAt()) ? cur : old); + } + + List results = latest.values().stream() + .sorted(Comparator.comparingInt(VesselAnalysisResult::getRiskScore).reversed()) .map(VesselAnalysisDto::from) .toList(); diff --git a/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java b/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java new file mode 100644 index 0000000..2f48b5c --- /dev/null +++ b/backend/src/main/java/gc/mda/kcg/domain/fleet/FleetCompanyController.java @@ -0,0 +1,27 @@ +package gc.mda.kcg.domain.fleet; + +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/fleet-companies") +@RequiredArgsConstructor +public class FleetCompanyController { + + private final JdbcTemplate jdbcTemplate; + + @GetMapping + public ResponseEntity>> getFleetCompanies() { + List> results = jdbcTemplate.queryForList( + "SELECT id, name_cn AS \"nameCn\", name_en AS \"nameEn\" FROM kcg.fleet_companies ORDER BY id" + ); + return ResponseEntity.ok(results); + } +} diff --git a/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java b/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java index b87a231..d3355aa 100644 --- a/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java +++ b/backend/src/main/java/gc/mda/kcg/domain/osint/OsintFeedRepository.java @@ -9,7 +9,7 @@ public interface OsintFeedRepository extends JpaRepository { boolean existsBySourceAndSourceUrl(String source, String sourceUrl); - boolean existsByRegionAndTitleAndCollectedAtAfter(String region, String title, Instant since); + boolean existsByTitle(String title); List findByRegionAndCollectedAtAfterOrderByPublishedAtDesc(String region, Instant since); } diff --git a/backend/src/main/resources/application-local.yml.example b/backend/src/main/resources/application-local.yml.example index 0fa5862..70490de 100644 --- a/backend/src/main/resources/application-local.yml.example +++ b/backend/src/main/resources/application-local.yml.example @@ -11,3 +11,6 @@ app: client-id: YOUR_GOOGLE_CLIENT_ID auth: allowed-domain: gcsc.co.kr + collector: + open-sky-client-id: YOUR_OPENSKY_CLIENT_ID + open-sky-client-secret: YOUR_OPENSKY_CLIENT_SECRET diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index ea2d3c9..89792f5 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -12,6 +12,8 @@ app: auth: allowed-domain: ${AUTH_ALLOWED_DOMAIN:gcsc.co.kr} collector: + open-sky-client-id: ${OPENSKY_CLIENT_ID:} + open-sky-client-secret: ${OPENSKY_CLIENT_SECRET:} prediction-base-url: ${PREDICTION_BASE_URL:http://192.168.1.18:8001} cors: allowed-origins: http://localhost:5173,https://kcg.gc-si.dev diff --git a/database/migration/007_fleet_registry.sql b/database/migration/007_fleet_registry.sql new file mode 100644 index 0000000..906d0f2 --- /dev/null +++ b/database/migration/007_fleet_registry.sql @@ -0,0 +1,74 @@ +-- 선단 등록 + 어망/어구 정체성 추적 시스템 + +-- 1. 소유자/회사 +CREATE TABLE IF NOT EXISTS kcg.fleet_companies ( + id SERIAL PRIMARY KEY, + name_cn TEXT NOT NULL, + name_en TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- 2. 등록 선박 (906척 참고자료 기반) +CREATE TABLE IF NOT EXISTS kcg.fleet_vessels ( + id SERIAL PRIMARY KEY, + company_id INT REFERENCES kcg.fleet_companies(id), + permit_no VARCHAR(20) NOT NULL, + name_cn TEXT NOT NULL, + name_en TEXT, + tonnage INT, + gear_code VARCHAR(10), + fleet_role VARCHAR(20), + pair_vessel_id INT REFERENCES kcg.fleet_vessels(id), + mmsi VARCHAR(15), + match_confidence REAL DEFAULT 0, + match_method VARCHAR(20), + last_seen_at TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_fleet_vessels_mmsi ON kcg.fleet_vessels(mmsi); +CREATE INDEX IF NOT EXISTS idx_fleet_vessels_name_en ON kcg.fleet_vessels(name_en); +CREATE INDEX IF NOT EXISTS idx_fleet_vessels_name_cn ON kcg.fleet_vessels(name_cn); +CREATE INDEX IF NOT EXISTS idx_fleet_vessels_company ON kcg.fleet_vessels(company_id); + +-- 3. 어망/어구 정체성 이력 +CREATE TABLE IF NOT EXISTS kcg.gear_identity_log ( + id BIGSERIAL PRIMARY KEY, + mmsi VARCHAR(15) NOT NULL, + name TEXT NOT NULL, + parent_name TEXT, + parent_mmsi VARCHAR(15), + parent_vessel_id INT REFERENCES kcg.fleet_vessels(id), + gear_index_1 INT, + gear_index_2 INT, + lat DOUBLE PRECISION, + lon DOUBLE PRECISION, + match_method VARCHAR(30), + match_confidence REAL DEFAULT 0, + first_seen_at TIMESTAMPTZ NOT NULL, + last_seen_at TIMESTAMPTZ NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_gear_identity_mmsi ON kcg.gear_identity_log(mmsi); +CREATE INDEX IF NOT EXISTS idx_gear_identity_parent ON kcg.gear_identity_log(parent_mmsi); +CREATE INDEX IF NOT EXISTS idx_gear_identity_active ON kcg.gear_identity_log(is_active) WHERE is_active = TRUE; + +-- 4. 선단 추적 스냅샷 (5분 주기) +CREATE TABLE IF NOT EXISTS kcg.fleet_tracking_snapshot ( + id BIGSERIAL PRIMARY KEY, + company_id INT REFERENCES kcg.fleet_companies(id), + snapshot_time TIMESTAMPTZ NOT NULL, + total_vessels INT, + active_vessels INT, + in_zone_vessels INT, + operating_vessels INT, + gear_count INT, + fleet_status VARCHAR(20), + center_lat DOUBLE PRECISION, + center_lon DOUBLE PRECISION, + details JSONB, + created_at TIMESTAMPTZ DEFAULT NOW() +); +CREATE INDEX IF NOT EXISTS idx_fleet_snapshot_time ON kcg.fleet_tracking_snapshot(snapshot_time DESC); +CREATE INDEX IF NOT EXISTS idx_fleet_snapshot_company ON kcg.fleet_tracking_snapshot(company_id); diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index adbe1a5..b98c071 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,26 +4,67 @@ ## [Unreleased] +## [2026-03-23.2] + ### 추가 -- 중국어선 조업분석: AIS Ship Type 30 + 선박명 패턴 분류, GC-KCG-2026-001/CSSA 기반 안강망 추가 -- 중국어선 선단 탐지: 본선-부속선 쌍, 운반선 환적, 선망 선단 -- 어구/어망 카테고리 신설 + 모선 연결선 시각화 -- 어구 SVG 아이콘 5종 (트롤/자망/안강망/선망/기본) -- 이란 주변국 시설 레이어 (MEFacilityLayer 35개소) -- 사우스파르스 가스전 피격 + 카타르 라스라판 보복 공격 반영 -- 한국 해군부대 10개소, 항만, 풍력발전단지, 북한 발사대/미사일 이벤트 레이어 -- 정부기관 건물 레이어 (GovBuildingLayer) -- CCTV 프록시 컨트롤러 +- 중국어선감시 탭: CN 어선 + 어구 패턴 선박 필터링 +- 중국어선감시 탭: 조업수역 Ⅰ~Ⅳ 폴리곤 동시 표시 +- 어구 그룹 수역 내/외 분류 (조업구역내 붉은색, 비허가 오렌지) +- 패널 3섹션 독립 접기/펴기 (선단 현황 / 조업구역내 어구 / 비허가 어구) +- 폴리곤 클릭·zoom 시 어구 행 자동 스크롤 +- localStorage 기반 레이어/필터 상태 영속화 (13개 항목) +- AI 분석 닫힘 시 위험도 마커 off ### 변경 -- 레이어 재구성: 선박(최상위) → 항공망 → 해양안전 → 국가기관망 -- 오른쪽 패널 접기/펼치기 기능 -- 센서차트 기본 숨김 -- CCTV 레이어 리팩토링 +- AI 분석 패널 위치 조정 (줌 버튼 간격 확보) +- 백엔드 vessel-analysis 조회 윈도우 1h → 2h + +### 수정 +- FleetClusterLayer 마운트 조건 완화 (clusters 의존 제거) + +## [2026-03-23] + +### 추가 +- 해외시설 레이어: 위험시설(원전/화학/연료저장) + 중국·일본 발전소/군사시설 +- 현장분석 Python 연동 + FieldAnalysisModal (어구/선단 분석 대시보드) +- 선단 선택 시 소속 선박 deck.gl 강조 (어구 그룹과 동일 패턴) +- 전 시설 kind에 리치 Popup 디자인 통합 (헤더·배지·상세정보) +- LAYERS 패널 카운트 통일 — 하드코딩→실제 데이터 기반 동적 표기 + +### 변경 +- DOM Marker → deck.gl 전환 (폴리곤 인터랙션 포함) +- 줌 레벨별 아이콘/텍스트 스케일 연동 (z4=0.8x ~ z14=4.2x) + +### 수정 +- 불법어선 탭 복원 + ShipLayer feature-state 필터 에러 수정 +- 해외시설 토글을 militaryOnly에서 분리 (선박/항공기 필터 간섭 해소) +- deck.gl 레이어 호버 시 pointer 커서 표시 +- prediction 증분 수집 버그 수정 (vessel_store.py) + +## [2026-03-20] + +### 변경 +- deck.gl 전면 전환: DOM Marker → GPU 렌더링 (WebGL) +- 정적 마커 11종 deck.gl 전환 + 줌 레벨별 스케일 + +### 추가 +- NK 미사일 궤적선 + 정적 마커 Popup + 어구 강조 +- Python 분석 결과 오버레이: 위험도 마커 + 다크베셀/GPS 스푸핑 경고 +- AI 분석 통계 패널 (우상단, 접이식): 분석 대상/위험/다크/선단 집계 +- 불법어선/다크베셀/중국어선감시 Python 분석 연동 +- Backend vessel-analysis REST API + DB 테이블 복원 +- 특정어업수역 Ⅰ~Ⅳ 실제 폴리곤 기반 수역 분류 + +### 수정 +- 해저케이블 날짜변경선 좌표 보정 + 렌더링 성능 개선 ## [2026-03-19] +### 추가 +- OpenSky OAuth2 Client Credentials 인증 (토큰 자동 갱신) + ### 변경 +- OpenSky 수집 주기 60초 → 300초 (일일 크레딧 소비 11,520 → 2,304) - 인라인 CSS 정리 — 공통 클래스 추출 + Tailwind 전환 ### 수정 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b9f283b..2c88106 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,7 +8,12 @@ "name": "kcg-monitoring", "version": "0.0.0", "dependencies": { + "@deck.gl/core": "^9.2.11", + "@deck.gl/layers": "^9.2.11", + "@deck.gl/mapbox": "^9.2.11", "@tailwindcss/vite": "^4.2.1", + "@turf/boolean-point-in-polygon": "^7.3.4", + "@turf/helpers": "^7.3.4", "@types/leaflet": "^1.9.21", "date-fns": "^4.1.0", "hls.js": "^1.6.15", @@ -291,6 +296,76 @@ "node": ">=6.9.0" } }, + "node_modules/@deck.gl/core": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.11.tgz", + "integrity": "sha512-lpdxXQuFSkd6ET7M6QxPI8QMhsLRY6vzLyk83sPGFb7JSb4OhrNHYt9sfIhcA/hxJW7bdBSMWWphf2GvQetVuA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@loaders.gl/core": "~4.3.4", + "@loaders.gl/images": "~4.3.4", + "@luma.gl/constants": "~9.2.6", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6", + "@luma.gl/shadertools": "~9.2.6", + "@luma.gl/webgl": "~9.2.6", + "@math.gl/core": "^4.1.0", + "@math.gl/sun": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "@probe.gl/env": "^4.1.1", + "@probe.gl/log": "^4.1.1", + "@probe.gl/stats": "^4.1.1", + "@types/offscreencanvas": "^2019.6.4", + "gl-matrix": "^3.0.0", + "mjolnir.js": "^3.0.0" + } + }, + "node_modules/@deck.gl/layers": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/layers/-/layers-9.2.11.tgz", + "integrity": "sha512-2FSb0Qa6YR+Rg6GWhYOGTUug3vtZ4uKcFdnrdiJoVXGyibKJMScKZIsivY0r/yQQZsaBjYqty5QuVJvdtEHxSA==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "~4.3.4", + "@loaders.gl/schema": "~4.3.4", + "@luma.gl/shadertools": "~9.2.6", + "@mapbox/tiny-sdf": "^2.0.5", + "@math.gl/core": "^4.1.0", + "@math.gl/polygon": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "earcut": "^2.2.4" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@loaders.gl/core": "~4.3.4", + "@luma.gl/core": "~9.2.6", + "@luma.gl/engine": "~9.2.6" + } + }, + "node_modules/@deck.gl/layers/node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/@deck.gl/mapbox": { + "version": "9.2.11", + "resolved": "https://registry.npmjs.org/@deck.gl/mapbox/-/mapbox-9.2.11.tgz", + "integrity": "sha512-5OaFZgjyA4Vq6WjHUdcEdl0Phi8dwj8hSCErej0NetW90mctdbxwMt0gSbqcvWBowwhyj2QAhH0P2FcITjKG/A==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "~9.2.6", + "@math.gl/web-mercator": "^4.1.0" + }, + "peerDependencies": { + "@deck.gl/core": "~9.2.0", + "@luma.gl/constants": "~9.2.6", + "@luma.gl/core": "~9.2.6", + "@math.gl/web-mercator": "^4.1.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", @@ -919,6 +994,133 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@loaders.gl/core": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.3.4.tgz", + "integrity": "sha512-cG0C5fMZ1jyW6WCsf4LoHGvaIAJCEVA/ioqKoYRwoSfXkOf+17KupK1OUQyUCw5XoRn+oWA1FulJQOYlXnb9Gw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4", + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@probe.gl/log": "^4.0.2" + } + }, + "node_modules/@loaders.gl/images": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.3.4.tgz", + "integrity": "sha512-qgc33BaNsqN9cWa/xvcGvQ50wGDONgQQdzHCKDDKhV2w/uptZoR5iofJfuG8UUV2vUMMd82Uk9zbopRx2rS4Ag==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.3.4" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/loader-utils": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.3.4.tgz", + "integrity": "sha512-tjMZvlKQSaMl2qmYTAxg+ySR6zd6hQn5n3XaU8+Ehp90TD3WzxvDKOMNDqOa72fFmIV+KgPhcmIJTpq4lAdC4Q==", + "license": "MIT", + "dependencies": { + "@loaders.gl/schema": "4.3.4", + "@loaders.gl/worker-utils": "4.3.4", + "@probe.gl/log": "^4.0.2", + "@probe.gl/stats": "^4.0.2" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/schema": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.3.4.tgz", + "integrity": "sha512-1YTYoatgzr/6JTxqBLwDiD3AVGwQZheYiQwAimWdRBVB0JAzych7s1yBuE0CVEzj4JDPKOzVAz8KnU1TiBvJGw==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.7" + }, + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@loaders.gl/worker-utils": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.3.4.tgz", + "integrity": "sha512-EbsszrASgT85GH3B7jkx7YXfQyIYo/rlobwMx6V3ewETapPUwdSAInv+89flnk5n2eu2Lpdeh+2zS6PvqbL2RA==", + "license": "MIT", + "peerDependencies": { + "@loaders.gl/core": "^4.3.0" + } + }, + "node_modules/@luma.gl/constants": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/constants/-/constants-9.2.6.tgz", + "integrity": "sha512-rvFFrJuSm5JIWbDHFuR4Q2s4eudO3Ggsv0TsGKn9eqvO7bBiPm/ANugHredvh3KviEyYuMZZxtfJvBdr3kzldg==", + "license": "MIT" + }, + "node_modules/@luma.gl/core": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.2.6.tgz", + "integrity": "sha512-d8KcH8ZZcjDAodSN/G2nueA9YE2X8kMz7Q0OxDGpCww6to1MZXM3Ydate/Jqsb5DDKVgUF6yD6RL8P5jOki9Yw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@math.gl/types": "^4.1.0", + "@probe.gl/env": "^4.0.8", + "@probe.gl/log": "^4.0.8", + "@probe.gl/stats": "^4.0.8", + "@types/offscreencanvas": "^2019.6.4" + } + }, + "node_modules/@luma.gl/engine": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.2.6.tgz", + "integrity": "sha512-1AEDs2AUqOWh7Wl4onOhXmQF+Rz1zNdPXF+Kxm4aWl92RQ42Sh2CmTvRt2BJku83VQ91KFIEm/v3qd3Urzf+Uw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@probe.gl/log": "^4.0.8", + "@probe.gl/stats": "^4.0.8" + }, + "peerDependencies": { + "@luma.gl/core": "~9.2.0", + "@luma.gl/shadertools": "~9.2.0" + } + }, + "node_modules/@luma.gl/shadertools": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.2.6.tgz", + "integrity": "sha512-4+uUbynqPUra9d/z1nQChyHmhLgmKfSMjS7kOwLB6exSnhKnpHL3+Hu9fv55qyaX50nGH3oHawhGtJ6RRvu65w==", + "license": "MIT", + "peer": true, + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0", + "wgsl_reflect": "^1.2.0" + }, + "peerDependencies": { + "@luma.gl/core": "~9.2.0" + } + }, + "node_modules/@luma.gl/webgl": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@luma.gl/webgl/-/webgl-9.2.6.tgz", + "integrity": "sha512-NGBTdxJMk7j8Ygr1zuTyAvr1Tw+EpupMIQo7RelFjEsZXg6pujFqiDMM+rgxex8voCeuhWBJc7Rs+MoSqd46UQ==", + "license": "MIT", + "dependencies": { + "@luma.gl/constants": "9.2.6", + "@math.gl/types": "^4.1.0", + "@probe.gl/env": "^4.0.8" + }, + "peerDependencies": { + "@luma.gl/core": "~9.2.0" + } + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "engines": { @@ -1002,6 +1204,66 @@ "version": "5.0.4", "license": "ISC" }, + "node_modules/@math.gl/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz", + "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==", + "license": "MIT", + "dependencies": { + "@math.gl/types": "4.1.0" + } + }, + "node_modules/@math.gl/polygon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/polygon/-/polygon-4.1.0.tgz", + "integrity": "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0" + } + }, + "node_modules/@math.gl/sun": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/sun/-/sun-4.1.0.tgz", + "integrity": "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA==", + "license": "MIT" + }, + "node_modules/@math.gl/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz", + "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==", + "license": "MIT" + }, + "node_modules/@math.gl/web-mercator": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz", + "integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0" + } + }, + "node_modules/@probe.gl/env": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.1.1.tgz", + "integrity": "sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA==", + "license": "MIT" + }, + "node_modules/@probe.gl/log": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.1.1.tgz", + "integrity": "sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg==", + "license": "MIT", + "dependencies": { + "@probe.gl/env": "4.1.1" + } + }, + "node_modules/@probe.gl/stats": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.1.1.tgz", + "integrity": "sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==", + "license": "MIT" + }, "node_modules/@react-leaflet/core": { "version": "3.0.0", "license": "Hippocratic-2.1", @@ -1628,6 +1890,49 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.3.4.tgz", + "integrity": "sha512-v/4hfyY90Vz9cDgs2GwjQf+Lft8o7mNCLJOTz/iv8SHAIgMMX0czEoIaNVOJr7tBqPqwin1CGwsncrkf5C9n8Q==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@turf/invariant": "7.3.4", + "@types/geojson": "^7946.0.10", + "point-in-polygon-hao": "^1.1.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.3.4.tgz", + "integrity": "sha512-U/S5qyqgx3WTvg4twaH0WxF3EixoTCfDsmk98g1E3/5e2YKp7JKYZdz0vivsS5/UZLJeZDEElOSFH4pUgp+l7g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.3.4.tgz", + "integrity": "sha512-88Eo4va4rce9sNZs6XiMJowWkikM3cS2TBhaCKlU+GFHdNf8PFEpiU42VDU8q5tOF6/fu21Rvlke5odgOGW4AQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.3.4", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -1739,6 +2044,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.14", "devOptional": true, @@ -3448,6 +3759,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mjolnir.js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-3.0.0.tgz", + "integrity": "sha512-siX3YCG7N2HnmN1xMH3cK4JkUZJhbkhRFJL+G5N1vH0mh1t5088rJknIoqDFWDIU6NPGvRRgLnYW3ZHjSMEBLA==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "dev": true, @@ -3579,6 +3896,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/point-in-polygon-hao": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", + "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==", + "license": "MIT", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/postcss": { "version": "8.5.8", "funding": [ @@ -3805,6 +4131,12 @@ "protocol-buffers-schema": "^3.3.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.59.0", "license": "MIT", @@ -4043,6 +4375,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -4262,6 +4600,12 @@ "node": ">=0.10.0" } }, + "node_modules/wgsl_reflect": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/wgsl_reflect/-/wgsl_reflect-1.2.3.tgz", + "integrity": "sha512-BQWBIsOn411M+ffBxmA6QRLvAOVbuz3Uk4NusxnqC1H7aeQcVLhdA3k2k/EFFFtqVjhz3z7JOOZF1a9hj2tv4Q==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index 4caaeb1..10d7a99 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@deck.gl/core": "^9.2.11", + "@deck.gl/layers": "^9.2.11", + "@deck.gl/mapbox": "^9.2.11", "@tailwindcss/vite": "^4.2.1", + "@turf/boolean-point-in-polygon": "^7.3.4", + "@turf/helpers": "^7.3.4", "@types/leaflet": "^1.9.21", "date-fns": "^4.1.0", "hls.js": "^1.6.15", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c589f9d..bdd8948 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; +import { useLocalStorage, useLocalStorageSet } from './hooks/useLocalStorage'; import { ReplayMap } from './components/iran/ReplayMap'; import type { FlyToTarget } from './components/iran/ReplayMap'; import { GlobeMap } from './components/iran/GlobeMap'; @@ -15,6 +16,7 @@ import { useMonitor } from './hooks/useMonitor'; import { useIranData } from './hooks/useIranData'; import { useKoreaData } from './hooks/useKoreaData'; import { useKoreaFilters } from './hooks/useKoreaFilters'; +import { useVesselAnalysis } from './hooks/useVesselAnalysis'; import type { GeoEvent, LayerVisibility, AppMode } from './types'; import { useTheme } from './hooks/useTheme'; import { useAuth } from './hooks/useAuth'; @@ -22,7 +24,21 @@ import { useTranslation } from 'react-i18next'; import LoginPage from './components/auth/LoginPage'; import CollectorMonitor from './components/common/CollectorMonitor'; import { FieldAnalysisModal } from './components/korea/FieldAnalysisModal'; -import { filterFacilities } from './data/meEnergyHazardFacilities'; +// 정적 데이터 카운트용 import (이미 useStaticDeckLayers에서 번들 포함됨) +import { EAST_ASIA_PORTS } from './data/ports'; +import { KOREAN_AIRPORTS } from './services/airports'; +import { MILITARY_BASES } from './data/militaryBases'; +import { GOV_BUILDINGS } from './data/govBuildings'; +import { KOREA_WIND_FARMS } from './data/windFarms'; +import { NK_LAUNCH_SITES } from './data/nkLaunchSites'; +import { NK_MISSILE_EVENTS } from './data/nkMissileEvents'; +import { COAST_GUARD_FACILITIES } from './services/coastGuard'; +import { NAV_WARNINGS } from './services/navWarning'; +import { PIRACY_ZONES } from './services/piracy'; +import { KOREA_SUBMARINE_CABLES } from './services/submarineCable'; +import { HAZARD_FACILITIES } from './data/hazardFacilities'; +import { CN_POWER_PLANTS, CN_MILITARY_FACILITIES } from './data/cnFacilities'; +import { JP_POWER_PLANTS, JP_MILITARY_FACILITIES } from './data/jpFacilities'; import './App.css'; function App() { @@ -53,9 +69,9 @@ interface AuthenticatedAppProps { function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { const [appMode, setAppMode] = useState('live'); - const [mapMode, setMapMode] = useState<'flat' | 'globe' | 'satellite'>('satellite'); - const [dashboardTab, setDashboardTab] = useState<'iran' | 'korea'>('iran'); - const [layers, setLayers] = useState({ + const [mapMode, setMapMode] = useLocalStorage<'flat' | 'globe' | 'satellite'>('mapMode', 'satellite'); + const [dashboardTab, setDashboardTab] = useLocalStorage<'iran' | 'korea'>('dashboardTab', 'iran'); + const [layers, setLayers] = useLocalStorage('iranLayers', { events: true, aircraft: true, satellites: true, @@ -67,7 +83,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { meFacilities: true, militaryOnly: false, overseasUS: false, - overseasIsrael: false, + overseasUK: false, overseasIran: false, overseasUAE: false, overseasSaudi: false, @@ -79,7 +95,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { }); // Korea tab layer visibility (lifted from KoreaMap) - const [koreaLayers, setKoreaLayers] = useState>({ + const [koreaLayers, setKoreaLayers] = useLocalStorage>('koreaLayers', { ships: true, aircraft: true, satellites: true, @@ -119,11 +135,11 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { const toggleKoreaLayer = useCallback((key: string) => { setKoreaLayers(prev => ({ ...prev, [key]: !prev[key] })); - }, []); + }, [setKoreaLayers]); // Category filter state (shared across tabs) - const [hiddenAcCategories, setHiddenAcCategories] = useState>(new Set()); - const [hiddenShipCategories, setHiddenShipCategories] = useState>(new Set()); + const [hiddenAcCategories, setHiddenAcCategories] = useLocalStorageSet('hiddenAcCategories', new Set()); + const [hiddenShipCategories, setHiddenShipCategories] = useLocalStorageSet('hiddenShipCategories', new Set()); const toggleAcCategory = useCallback((cat: string) => { setHiddenAcCategories(prev => { @@ -131,7 +147,7 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { if (next.has(cat)) { next.delete(cat); } else { next.add(cat); } return next; }); - }, []); + }, [setHiddenAcCategories]); const toggleShipCategory = useCallback((cat: string) => { setHiddenShipCategories(prev => { @@ -139,27 +155,27 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { if (next.has(cat)) { next.delete(cat); } else { next.add(cat); } return next; }); - }, []); + }, [setHiddenShipCategories]); // Nationality filter state (Korea tab) - const [hiddenNationalities, setHiddenNationalities] = useState>(new Set()); + const [hiddenNationalities, setHiddenNationalities] = useLocalStorageSet('hiddenNationalities', new Set()); const toggleNationality = useCallback((nat: string) => { setHiddenNationalities(prev => { const next = new Set(prev); if (next.has(nat)) { next.delete(nat); } else { next.add(nat); } return next; }); - }, []); + }, [setHiddenNationalities]); // Fishing vessel nationality filter state - const [hiddenFishingNats, setHiddenFishingNats] = useState>(new Set()); + const [hiddenFishingNats, setHiddenFishingNats] = useLocalStorageSet('hiddenFishingNats', new Set()); const toggleFishingNat = useCallback((nat: string) => { setHiddenFishingNats(prev => { const next = new Set(prev); if (next.has(nat)) { next.delete(nat); } else { next.add(nat); } return next; }); - }, []); + }, [setHiddenFishingNats]); const [flyToTarget, setFlyToTarget] = useState(null); @@ -214,16 +230,21 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { refreshKey, }); + // Vessel analysis (Python prediction 결과) + const vesselAnalysis = useVesselAnalysis(dashboardTab === 'korea'); + // Korea filters hook const koreaFiltersResult = useKoreaFilters( koreaData.ships, koreaData.visibleShips, currentTime, + vesselAnalysis.analysisMap, + koreaLayers.cnFishing, ); - const toggleLayer = useCallback((key: string) => { - setLayers(prev => ({ ...prev, [key]: !prev[key as keyof typeof prev] })); - }, []); + const toggleLayer = useCallback((key: keyof LayerVisibility) => { + setLayers(prev => ({ ...prev, [key]: !prev[key] })); + }, [setLayers]); // Handle event card click from timeline: fly to location on map const handleEventFlyTo = useCallback((event: GeoEvent) => { @@ -281,6 +302,15 @@ function AuthenticatedApp({ user, onLogout }: AuthenticatedAppProps) { {dashboardTab === 'korea' && (
+ + +
+ + + {/* 본문 */} + {expanded && ( +
+ {isEmpty ? ( +
+ 분석 데이터 없음 +
+ ) : ( + <> + {/* 요약 행 */} +
+ 전체 + {stats.total} +
+
+ 다크베셀 + {stats.dark} +
+
+ GPS스푸핑 + {stats.spoofing} +
+
+ 선단수 + {stats.clusterCount} +
+ {gearStats.groups > 0 && ( + <> +
+ 어구그룹 + {gearStats.groups} +
+
+ 어구수 + {gearStats.count} +
+ + )} + +
+ + {/* 위험도 카운트 행 — 클릭 가능 */} +
+ {RISK_LEVELS.map(level => { + const count = stats[level.toLowerCase() as 'critical' | 'high' | 'medium' | 'low']; + const isActive = selectedLevel === level; + return ( + + ); + })} +
+ + {/* 선박 목록 */} + {selectedLevel !== null && vesselList.length > 0 && ( + <> +
+
+ {RISK_EMOJI[selectedLevel]} {selectedLevel} — {vesselList.length}척 +
+
+ {vesselList.map(item => { + const isExpanded = selectedMmsi === item.mmsi; + const color = RISK_COLOR[selectedLevel]; + const { dto } = item; + return ( +
+ {/* 선박 행 */} +
{ void handleVesselClick(item.mmsi); }} + style={{ + display: 'flex', + alignItems: 'center', + gap: 4, + padding: '3px 4px', + cursor: 'pointer', + borderRadius: 3, + borderLeft: isExpanded ? `2px solid ${color}` : '2px solid transparent', + backgroundColor: isExpanded ? 'rgba(255,255,255,0.06)' : 'transparent', + transition: 'background-color 0.1s', + }} + onMouseEnter={e => { + if (!isExpanded) (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(255,255,255,0.04)'; + }} + onMouseLeave={e => { + if (!isExpanded) (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; + }} + > + + {item.name} + + + {item.mmsi} + + + {item.score}점 + + +
+ + {/* 근거 상세 */} + {isExpanded && ( +
+
+ 위치: {dto.algorithms.location.zone} + {' '}(기선 {dto.algorithms.location.distToBaselineNm.toFixed(1)}NM) +
+
+ 활동: {dto.algorithms.activity.state} + {' '}(UCAF {dto.algorithms.activity.ucafScore.toFixed(2)}) +
+ {dto.algorithms.darkVessel.isDark && ( +
다크: {dto.algorithms.darkVessel.gapDurationMin}분 갭
+ )} + {dto.algorithms.gpsSpoofing.spoofingScore > 0 && ( +
+ GPS: 스푸핑 {Math.round(dto.algorithms.gpsSpoofing.spoofingScore * 100)}% +
+ )} + {dto.algorithms.cluster.clusterSize > 1 && ( +
+ 선단: {dto.algorithms.fleetRole.role} + {' '}({dto.algorithms.cluster.clusterSize}척) +
+ )} +
+ )} +
+ ); + })} +
+ + )} + + {selectedLevel !== null && vesselList.length === 0 && ( + <> +
+
+ 해당 레벨 선박 없음 +
+ + )} + + )} + + {/* 범례 */} + {showLegend && ( + <> +
+
+ {LEGEND_LINES.map((line, i) => ( +
+ {line || '\u00A0'} +
+ ))} +
+ + )} +
+ )} +
+ ); +} diff --git a/frontend/src/components/korea/ChineseFishingOverlay.tsx b/frontend/src/components/korea/ChineseFishingOverlay.tsx index e91f6aa..fc935ec 100644 --- a/frontend/src/components/korea/ChineseFishingOverlay.tsx +++ b/frontend/src/components/korea/ChineseFishingOverlay.tsx @@ -1,57 +1,6 @@ import { useMemo } from 'react'; -import { Marker, Source, Layer } from 'react-map-gl/maplibre'; -import type { Ship } from '../../types'; -import { analyzeFishing, GEAR_LABELS } from '../../utils/fishingAnalysis'; -import type { FishingGearType } from '../../utils/fishingAnalysis'; -import { getMarineTrafficCategory } from '../../utils/marineTraffic'; -import { FishingNetIcon, TrawlNetIcon, GillnetIcon, StowNetIcon, PurseSeineIcon } from '../icons/FishingNetIcon'; - -/** 어구 아이콘 컴포넌트 매핑 */ -function GearIcon({ gear, size = 14 }: { gear: FishingGearType; size?: number }) { - const meta = GEAR_LABELS[gear]; - const color = meta?.color || '#888'; - switch (gear) { - case 'trawl_pair': - case 'trawl_single': - return ; - case 'gillnet': - return ; - case 'stow_net': - return ; - case 'purse_seine': - return ; - default: - return ; - } -} - -/** 선박 역할 추정 — 속도/크기/카테고리 기반 */ -function estimateRole(ship: Ship): { role: string; roleKo: string; color: string } { - const mtCat = getMarineTrafficCategory(ship.typecode, ship.category); - const speed = ship.speed; - const len = ship.length || 0; - - // 운반선: 화물선/대형/미분류 + 저속 - if (mtCat === 'cargo' || (mtCat === 'unspecified' && len > 50)) { - return { role: 'FC', roleKo: '운반', color: '#f97316' }; - } - - // 어선 분류 - if (mtCat === 'fishing' || ship.category === 'fishing') { - // 대형(>200톤급, 길이 40m+) → 본선 - if (len >= 40) { - return { role: 'PT', roleKo: '본선', color: '#ef4444' }; - } - // 소형(<30m) + 트롤 속도 → 부속선 - if (len > 0 && len < 30 && speed >= 2 && speed <= 5) { - return { role: 'PT-S', roleKo: '부속', color: '#fb923c' }; - } - // 기본 어선 - return { role: 'FV', roleKo: '어선', color: '#22c55e' }; - } - - return { role: '', roleKo: '', color: '#6b7280' }; -} +import { Source, Layer } from 'react-map-gl/maplibre'; +import type { Ship, VesselAnalysisDto } from '../../types'; /** * 어구/어망 이름에서 모선명 추출 @@ -79,68 +28,45 @@ interface GearToParentLink { interface Props { ships: Ship[]; + analysisMap?: Map; } -export function ChineseFishingOverlay({ ships }: Props) { - // 중국 어선만 필터링 - const chineseFishing = useMemo(() => { - return ships.filter(s => { - if (s.flag !== 'CN') return false; - const cat = getMarineTrafficCategory(s.typecode, s.category); - return cat === 'fishing' || s.category === 'fishing'; - }); - }, [ships]); - - // 조업 분석 결과 - const analyzed = useMemo(() => { - return chineseFishing.map(s => ({ - ship: s, - analysis: analyzeFishing(s), - role: estimateRole(s), - })); - }, [chineseFishing]); - - // 조업 중인 선박만 (어구 아이콘 표시용) - const operating = useMemo(() => analyzed.filter(a => a.analysis.isOperating), [analyzed]); - - // 어구/어망 → 모선 연결 탐지 +export function ChineseFishingOverlay({ ships, analysisMap: _analysisMap }: Props) { + // 어구/어망 → 모선 연결 탐지 (거리 제한 + 정확 매칭 우선) const gearLinks: GearToParentLink[] = useMemo(() => { - // 어구/어망 선박 (이름_숫자_ 또는 이름% 패턴) const gearPattern = /^.+_\d+_\d*$|%$/; const gearShips = ships.filter(s => gearPattern.test(s.name)); if (gearShips.length === 0) return []; - // 모선 후보 (모든 선박의 이름 → Ship 매핑) + // 모선 후보 const nameMap = new Map(); for (const s of ships) { if (!gearPattern.test(s.name) && s.name) { - // 정확한 이름 매핑 nameMap.set(s.name.trim(), s); } } + const MAX_DIST_DEG = 0.15; // ~10NM — 이 이상 떨어지면 연결 안 함 + const MAX_LINKS = 200; // 브라우저 성능 보호 + const links: GearToParentLink[] = []; for (const gear of gearShips) { + if (links.length >= MAX_LINKS) break; + const parentName = extractParentName(gear.name); - if (!parentName) continue; + if (!parentName || parentName.length < 3) continue; // 너무 짧은 이름 제외 - // 정확히 일치하는 모선 찾기 - let parent = nameMap.get(parentName); + // 정확 매칭만 (부분 매칭 제거 — 오탐 원인) + const parent = nameMap.get(parentName); + if (!parent) continue; - // 정확 매칭 없으면 부분 매칭 (앞부분이 같은 선박) - if (!parent) { - for (const [name, ship] of nameMap) { - if (name.startsWith(parentName) || parentName.startsWith(name)) { - parent = ship; - break; - } - } - } + // 거리 제한: ~10NM 이내만 연결 + const dlat = Math.abs(gear.lat - parent.lat); + const dlng = Math.abs(gear.lng - parent.lng); + if (dlat > MAX_DIST_DEG || dlng > MAX_DIST_DEG) continue; - if (parent) { - links.push({ gear, parent, parentName }); - } + links.push({ gear, parent, parentName }); } return links; }, [ships]); @@ -161,21 +87,6 @@ export function ChineseFishingOverlay({ ships }: Props) { })), }), [gearLinks]); - // 운반선 추정 (중국 화물선 중 어선 근처) - const carriers = useMemo(() => { - return ships.filter(s => { - if (s.flag !== 'CN') return false; - const cat = getMarineTrafficCategory(s.typecode, s.category); - if (cat !== 'cargo' && cat !== 'unspecified') return false; - // 어선 5NM 이내에 있는 화물선 - return chineseFishing.some(f => { - const dlat = Math.abs(s.lat - f.lat); - const dlng = Math.abs(s.lng - f.lng); - return dlat < 0.08 && dlng < 0.08; // ~5NM 근사 - }); - }).slice(0, 50); // 최대 50척 - }, [ships, chineseFishing]); - return ( <> {/* 어구/어망 → 모선 연결선 */} @@ -193,74 +104,6 @@ export function ChineseFishingOverlay({ ships }: Props) { /> )} - - {/* 어구/어망 위치 마커 (모선 연결된 것) — 최대 50개 */} - {gearLinks.slice(0, 50).map(link => ( - -
-
- -
-
- ← {link.parentName} -
-
-
- ))} - - {/* 조업 중 어선 — 어구 아이콘 — 최대 80개 */} - {operating.slice(0, 80).map(({ ship, analysis }) => { - const meta = GEAR_LABELS[analysis.gearType]; - return ( - -
- -
-
- ); - })} - - {/* 본선/부속선/어선 역할 라벨 — 최대 100개 */} - {analyzed.filter(a => a.role.role).slice(0, 100).map(({ ship, role }) => ( - -
- {role.roleKo} -
-
- ))} - - {/* 운반선 라벨 */} - {carriers.map(s => ( - -
- 운반 -
-
- ))} ); } diff --git a/frontend/src/components/korea/CoastGuardLayer.tsx b/frontend/src/components/korea/CoastGuardLayer.tsx index 623579c..4e1651f 100644 --- a/frontend/src/components/korea/CoastGuardLayer.tsx +++ b/frontend/src/components/korea/CoastGuardLayer.tsx @@ -1,7 +1,6 @@ -import { useState } from 'react'; +import { Popup } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { COAST_GUARD_FACILITIES, CG_TYPE_LABEL } from '../../services/coastGuard'; +import { CG_TYPE_LABEL } from '../../services/coastGuard'; import type { CoastGuardFacility, CoastGuardType } from '../../services/coastGuard'; const TYPE_COLOR: Record = { @@ -13,143 +12,52 @@ const TYPE_COLOR: Record = { navy: '#3b82f6', }; -const TYPE_SIZE: Record = { - hq: 24, - regional: 20, - station: 16, - substation: 13, - vts: 14, - navy: 18, -}; - -/** 해경 로고 SVG — 작은 방패+앵커 심볼 */ -function CoastGuardIcon({ type, size }: { type: CoastGuardType; size: number }) { - const color = TYPE_COLOR[type]; - const isVts = type === 'vts'; - - if (type === 'navy') { - return ( - - - - - - - ); - } - - if (isVts) { - return ( - - - - - - - - ); - } - - return ( - - - - - - - {(type === 'hq' || type === 'regional') && ( - - )} - - ); +interface Props { + selected: CoastGuardFacility | null; + onClose: () => void; } -export function CoastGuardLayer() { - const [selected, setSelected] = useState(null); +export function CoastGuardLayer({ selected, onClose }: Props) { const { t } = useTranslation(); - + if (!selected) return null; return ( - <> - {COAST_GUARD_FACILITIES.map(f => { - const size = TYPE_SIZE[f.type]; - return ( - { e.originalEvent.stopPropagation(); setSelected(f); }}> -
- - {(f.type === 'hq' || f.type === 'regional') && ( -
- {f.name.replace('해양경찰청', '').replace('지방', '').trim() || '본청'} -
- )} - {f.type === 'navy' && ( -
- {f.name.replace('해군', '').replace('사령부', '사').replace('전대', '전').replace('전단', '전').trim().slice(0, 8)} -
- )} - {f.type === 'vts' && ( -
- VTS -
- )} -
-
- ); - })} - - {selected && ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="280px" className="gl-popup"> -
-
- {selected.type === 'navy' ? ( - - ) : selected.type === 'vts' ? ( - 📡 - ) : ( - 🚔 - )} - {selected.name} -
-
- - {CG_TYPE_LABEL[selected.type]} - - - {t('coastGuard.agency')} - -
-
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
-
-
- )} - + +
+
+ {selected.type === 'navy' ? ( + + ) : selected.type === 'vts' ? ( + 📡 + ) : ( + 🚔 + )} + {selected.name} +
+
+ + {CG_TYPE_LABEL[selected.type]} + + + {t('coastGuard.agency')} + +
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+
+
); } diff --git a/frontend/src/components/korea/FieldAnalysisModal.tsx b/frontend/src/components/korea/FieldAnalysisModal.tsx index ae1ef89..7343949 100644 --- a/frontend/src/components/korea/FieldAnalysisModal.tsx +++ b/frontend/src/components/korea/FieldAnalysisModal.tsx @@ -1,8 +1,9 @@ import { useState, useMemo, useEffect, useCallback } from 'react'; -import type { Ship, ChnPrmShipInfo } from '../../types'; -import { analyzeFishing } from '../../utils/fishingAnalysis'; +import type { Ship, ChnPrmShipInfo, VesselAnalysisDto } from '../../types'; +import { classifyFishingZone } from '../../utils/fishingAnalysis'; import { getMarineTrafficCategory } from '../../utils/marineTraffic'; import { lookupPermittedShip } from '../../services/chnPrmShip'; +import type { UseVesselAnalysisResult } from '../../hooks/useVesselAnalysis'; // MarineTraffic 사진 캐시 (null = 없음, undefined = 미조회) const mtPhotoCache = new Map(); @@ -55,16 +56,8 @@ const C = { border2: '#0E2035', } as const; -// 황해 위치 기반 수역 분류 (근사값) -function classifyZone(lng: number): string { - if (lng > 124.8) return 'TERRITORIAL'; - if (lng > 124.2) return 'CONTIGUOUS'; - if (lng > 121.5) return 'EEZ'; - return 'BEYOND'; -} - -// AIS 수신 기준 선박 상태 분류 -function classifyState(ship: Ship): string { +// AIS 수신 기준 선박 상태 분류 (Python 결과 없을 때 fallback) +function classifyStateFallback(ship: Ship): string { const ageMins = (Date.now() - ship.lastSeen) / 60000; if (ageMins > 20) return 'AIS_LOSS'; if (ship.speed <= 0.5) return 'STATIONARY'; @@ -72,11 +65,11 @@ function classifyState(ship: Ship): string { return 'FISHING'; } -function getAlertLevel(zone: string, state: string): 'CRITICAL' | 'WATCH' | 'MONITOR' | 'NORMAL' { - if (zone === 'TERRITORIAL') return 'CRITICAL'; - if (state === 'AIS_LOSS') return 'WATCH'; - if (zone === 'CONTIGUOUS' && state === 'FISHING') return 'WATCH'; - if (zone === 'EEZ' && state === 'FISHING') return 'MONITOR'; +// Python RiskLevel → 경보 등급 매핑 +function riskToAlert(level: string): 'CRITICAL' | 'WATCH' | 'MONITOR' | 'NORMAL' { + if (level === 'CRITICAL') return 'CRITICAL'; + if (level === 'HIGH') return 'WATCH'; + if (level === 'MEDIUM') return 'MONITOR'; return 'NORMAL'; } @@ -94,29 +87,6 @@ function zoneLabel(z: string): string { return map[z] ?? z; } -// 근접 클러스터링 (~5NM 내 2척 이상 집단) -function buildClusters(vessels: ProcessedVessel[]): Map { - const result = new Map(); - let clusterIdx = 0; - for (let i = 0; i < vessels.length; i++) { - if (result.has(vessels[i].ship.mmsi)) continue; - const cluster: string[] = [vessels[i].ship.mmsi]; - for (let j = i + 1; j < vessels.length; j++) { - if (result.has(vessels[j].ship.mmsi)) continue; - const dlat = Math.abs(vessels[i].ship.lat - vessels[j].ship.lat); - const dlng = Math.abs(vessels[i].ship.lng - vessels[j].ship.lng); - if (dlat < 0.08 && dlng < 0.08) { - cluster.push(vessels[j].ship.mmsi); - } - } - if (cluster.length >= 2) { - clusterIdx++; - const id = `C-${String(clusterIdx).padStart(2, '0')}`; - cluster.forEach(mmsi => result.set(mmsi, id)); - } - } - return result; -} interface ProcessedVessel { ship: Ship; @@ -137,6 +107,7 @@ interface LogEntry { interface Props { ships: Ship[]; + vesselAnalysis?: UseVesselAnalysisResult; onClose: () => void; } @@ -152,7 +123,9 @@ const PIPE_STEPS = [ const ALERT_ORDER: Record = { CRITICAL: 0, WATCH: 1, MONITOR: 2, NORMAL: 3 }; -export function FieldAnalysisModal({ ships, onClose }: Props) { +export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) { + const emptyMap = useMemo(() => new Map(), []); + const analysisMap = vesselAnalysis?.analysisMap ?? emptyMap; const [activeFilter, setActiveFilter] = useState('ALL'); const [search, setSearch] = useState(''); const [selectedMmsi, setSelectedMmsi] = useState(null); @@ -167,25 +140,36 @@ export function FieldAnalysisModal({ ships, onClose }: Props) { return cat === 'fishing' || s.category === 'fishing'; }), [ships]); - // 선박 데이터 처리 + // 선박 데이터 처리 — Python 분석 결과 우선, 없으면 GeoJSON 수역 + AIS fallback const processed = useMemo((): ProcessedVessel[] => { - const baseList = cnFishing.map(ship => { - const zone = classifyZone(ship.lng); - const state = classifyState(ship); - const alert = getAlertLevel(zone, state); - const analysis = analyzeFishing(ship); - const gear = analysis.gearType; - const vtype = - (gear === 'trawl_pair' || gear === 'trawl_single') ? 'TRAWL' : - gear === 'purse_seine' ? 'PURSE' : - gear === 'gillnet' ? 'GILLNET' : - gear === 'stow_net' ? 'TRAP' : - 'TRAWL'; - return { ship, zone, state, alert, vtype, cluster: '' }; + return cnFishing.map(ship => { + const dto = analysisMap.get(ship.mmsi); + + // 수역: Python → GeoJSON 폴리곤 fallback + let zone: string; + if (dto) { + zone = dto.algorithms.location.zone; + } else { + const zoneInfo = classifyFishingZone(ship.lat, ship.lng); + zone = zoneInfo.zone === 'OUTSIDE' ? 'EEZ_OR_BEYOND' : zoneInfo.zone; + } + + // 행동 상태: Python → AIS fallback + const state = dto?.algorithms.activity.state ?? classifyStateFallback(ship); + + // 경보 등급: Python 위험도 직접 사용 + const alert = dto ? riskToAlert(dto.algorithms.riskScore.level) : 'NORMAL'; + + // 어구 분류: Python classification + const vtype = dto?.classification.vesselType ?? 'UNKNOWN'; + + // 클러스터: Python cluster ID + const clusterId = dto?.algorithms.cluster.clusterId ?? -1; + const cluster = clusterId >= 0 ? `C-${String(clusterId).padStart(2, '0')}` : '—'; + + return { ship, zone, state, alert, vtype, cluster }; }); - const clusterMap = buildClusters(baseList); - return baseList.map(v => ({ ...v, cluster: clusterMap.get(v.ship.mmsi) ?? '—' })); - }, [cnFishing]); + }, [cnFishing, analysisMap]); // 필터 + 정렬 const displayed = useMemo(() => { @@ -201,24 +185,31 @@ export function FieldAnalysisModal({ ships, onClose }: Props) { .sort((a, b) => ALERT_ORDER[a.alert] - ALERT_ORDER[b.alert]); }, [processed, activeFilter, search]); - // 통계 - const stats = useMemo(() => ({ - total: processed.length, - territorial: processed.filter(v => v.zone === 'TERRITORIAL').length, - fishing: processed.filter(v => v.state === 'FISHING').length, - aisLoss: processed.filter(v => v.state === 'AIS_LOSS').length, - gpsAnomaly: 0, - clusters: new Set(processed.filter(v => v.cluster !== '—').map(v => v.cluster)).size, - trawl: processed.filter(v => v.vtype === 'TRAWL').length, - purse: processed.filter(v => v.vtype === 'PURSE').length, - }), [processed]); + // 통계 — Python 분석 결과 기반 + const stats = useMemo(() => { + let gpsAnomaly = 0; + for (const v of processed) { + const dto = analysisMap.get(v.ship.mmsi); + if (dto && dto.algorithms.gpsSpoofing.spoofingScore > 0.5) gpsAnomaly++; + } + return { + total: processed.length, + territorial: processed.filter(v => v.zone === 'TERRITORIAL_SEA' || v.zone === 'ZONE_I').length, + fishing: processed.filter(v => v.state === 'FISHING').length, + aisLoss: processed.filter(v => v.state === 'AIS_LOSS' || v.state === 'UNKNOWN').length, + gpsAnomaly, + clusters: new Set(processed.filter(v => v.cluster !== '—').map(v => v.cluster)).size, + trawl: processed.filter(v => v.vtype === 'TRAWL').length, + purse: processed.filter(v => v.vtype === 'PURSE').length, + }; + }, [processed, analysisMap]); - // 구역별 카운트 + // 구역별 카운트 — Python zone 분류 기반 const zoneCounts = useMemo(() => ({ - terr: processed.filter(v => v.zone === 'TERRITORIAL').length, - cont: processed.filter(v => v.zone === 'CONTIGUOUS').length, - eez: processed.filter(v => v.zone === 'EEZ').length, - beyond: processed.filter(v => v.zone === 'BEYOND').length, + terr: processed.filter(v => v.zone === 'TERRITORIAL_SEA' || v.zone === 'ZONE_I').length, + cont: processed.filter(v => v.zone === 'CONTIGUOUS_ZONE' || v.zone === 'ZONE_II').length, + eez: processed.filter(v => v.zone === 'EEZ_OR_BEYOND' || v.zone === 'ZONE_III' || v.zone === 'ZONE_IV').length, + beyond: processed.filter(v => !['TERRITORIAL_SEA', 'CONTIGUOUS_ZONE', 'EEZ_OR_BEYOND', 'ZONE_I', 'ZONE_II', 'ZONE_III', 'ZONE_IV'].includes(v.zone)).length, }), [processed]); // 초기 경보 로그 생성 @@ -382,10 +373,10 @@ export function FieldAnalysisModal({ ships, onClose }: Props) { { label: '영해 침범', val: stats.territorial, color: C.red, sub: '12NM 이내' }, { label: '조업 중', val: stats.fishing, color: C.amber, sub: 'SOG 0.5–5.0kt' }, { label: 'AIS 소실', val: stats.aisLoss, color: C.red, sub: '>20분 미수신' }, - { label: 'GPS 이상', val: stats.gpsAnomaly, color: C.purple, sub: 'BD-09 의심' }, - { label: '집단 클러스터', val: stats.clusters, color: C.amber, sub: 'BIRCH 군집' }, - { label: '트롤어선', val: stats.trawl, color: C.purple, sub: '규칙 기반 분류' }, - { label: '선망어선', val: stats.purse, color: C.cyan, sub: '규칙 기반 분류' }, + { label: 'GPS 이상', val: stats.gpsAnomaly, color: C.purple, sub: 'BD-09 스푸핑 50%↑' }, + { label: '집단 클러스터', val: stats.clusters, color: C.amber, sub: 'DBSCAN 군집' }, + { label: '트롤어선', val: stats.trawl, color: C.purple, sub: 'Python 분류' }, + { label: '선망어선', val: stats.purse, color: C.cyan, sub: 'Python 분류' }, ].map(({ label, val, color, sub }) => (
20분 미수신', color: C.amber }, { label: 'GPS 조작', val: 'BD-09 좌표계', color: C.purple }, - { label: '클러스터', val: 'BIRCH 5NM', color: C.ink2 }, - { label: '선종 분류', val: '규칙 기반 (Python 연동 예정)', color: C.ink2 }, + { label: '클러스터', val: 'DBSCAN 3NM (Python)', color: C.ink2 }, + { label: '선종 분류', val: 'Python 7단계 파이프라인', color: C.green }, ].map(({ label, val, color }) => (
{label} @@ -680,7 +671,7 @@ export function FieldAnalysisModal({ ships, onClose }: Props) { ))} - AIS 4분 갱신 | Shepperson(2017) 기준 | 규칙 기반 분류 | 근접 클러스터 5NM + AIS 4분 갱신 | Python 7단계 파이프라인 | DBSCAN 3NM 클러스터 | GeoJSON 수역 분류
@@ -709,10 +700,20 @@ export function FieldAnalysisModal({ ships, onClose }: Props) { { label: '위치', val: `${selectedVessel.ship.lat.toFixed(4)}°N ${selectedVessel.ship.lng.toFixed(4)}°E`, color: C.ink }, { label: '속도 / 침로', val: `${selectedVessel.ship.speed.toFixed(1)}kt ${selectedVessel.ship.course}°`, color: C.amber }, { label: '행동 상태', val: stateLabel(selectedVessel.state), color: stateColor(selectedVessel.state) }, - { label: '추정 선종', val: selectedVessel.vtype, color: C.ink }, + { label: '선종 (Python)', val: selectedVessel.vtype, color: C.ink }, { label: '현재 구역', val: zoneLabel(selectedVessel.zone), color: zoneColor(selectedVessel.zone) }, - { label: 'BIRCH 클러스터', val: selectedVessel.cluster, color: selectedVessel.cluster !== '—' ? C.purple : C.ink3 }, - { label: '경보 등급', val: selectedVessel.alert, color: alertColor(selectedVessel.alert) }, + { label: '클러스터', val: selectedVessel.cluster, color: selectedVessel.cluster !== '—' ? C.purple : C.ink3 }, + { label: '위험도', val: selectedVessel.alert, color: alertColor(selectedVessel.alert) }, + ...(() => { + const dto = analysisMap.get(selectedVessel.ship.mmsi); + if (!dto) return [{ label: 'AI 분석', val: '미분석', color: C.ink3 }]; + return [ + { label: '위험 점수', val: `${dto.algorithms.riskScore.score}점`, color: alertColor(selectedVessel.alert) }, + { label: 'GPS 스푸핑', val: `${Math.round(dto.algorithms.gpsSpoofing.spoofingScore * 100)}%`, color: dto.algorithms.gpsSpoofing.spoofingScore > 0.5 ? C.red : C.green }, + { label: 'AIS 공백', val: dto.algorithms.darkVessel.isDark ? `${Math.round(dto.algorithms.darkVessel.gapDurationMin)}분` : '정상', color: dto.algorithms.darkVessel.isDark ? C.red : C.green }, + { label: '선단 역할', val: dto.algorithms.fleetRole.role, color: dto.algorithms.fleetRole.isLeader ? C.amber : C.ink2 }, + ]; + })(), ].map(({ label, val, color }) => (
{label} diff --git a/frontend/src/components/korea/FishingZoneLayer.tsx b/frontend/src/components/korea/FishingZoneLayer.tsx new file mode 100644 index 0000000..a7e006d --- /dev/null +++ b/frontend/src/components/korea/FishingZoneLayer.tsx @@ -0,0 +1,57 @@ +import { Source, Layer } from 'react-map-gl/maplibre'; +import fishingZonesData from '../../data/zones/fishing-zones-wgs84.json'; + +const ZONE_FILL: Record = { + ZONE_I: 'rgba(59, 130, 246, 0.15)', + ZONE_II: 'rgba(16, 185, 129, 0.15)', + ZONE_III: 'rgba(245, 158, 11, 0.15)', + ZONE_IV: 'rgba(239, 68, 68, 0.15)', +}; + +const ZONE_LINE: Record = { + ZONE_I: 'rgba(59, 130, 246, 0.6)', + ZONE_II: 'rgba(16, 185, 129, 0.6)', + ZONE_III: 'rgba(245, 158, 11, 0.6)', + ZONE_IV: 'rgba(239, 68, 68, 0.6)', +}; + + +const fillColor = [ + 'match', ['get', 'id'], + 'ZONE_I', ZONE_FILL.ZONE_I, + 'ZONE_II', ZONE_FILL.ZONE_II, + 'ZONE_III', ZONE_FILL.ZONE_III, + 'ZONE_IV', ZONE_FILL.ZONE_IV, + 'rgba(0,0,0,0)', +] as maplibregl.ExpressionSpecification; + +const lineColor = [ + 'match', ['get', 'id'], + 'ZONE_I', ZONE_LINE.ZONE_I, + 'ZONE_II', ZONE_LINE.ZONE_II, + 'ZONE_III', ZONE_LINE.ZONE_III, + 'ZONE_IV', ZONE_LINE.ZONE_IV, + 'rgba(0,0,0,0)', +] as maplibregl.ExpressionSpecification; + +export function FishingZoneLayer() { + return ( + + + + + ); +} diff --git a/frontend/src/components/korea/FleetClusterLayer.tsx b/frontend/src/components/korea/FleetClusterLayer.tsx new file mode 100644 index 0000000..76f3b10 --- /dev/null +++ b/frontend/src/components/korea/FleetClusterLayer.tsx @@ -0,0 +1,1055 @@ +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import { Source, Layer, Popup, useMap } from 'react-map-gl/maplibre'; +import type { GeoJSON } from 'geojson'; +import type { MapLayerMouseEvent } from 'maplibre-gl'; +import type { Ship, VesselAnalysisDto } from '../../types'; +import { fetchFleetCompanies } from '../../services/vesselAnalysis'; +import type { FleetCompany } from '../../services/vesselAnalysis'; +import { classifyFishingZone } from '../../utils/fishingAnalysis'; + +export interface SelectedGearGroupData { + parent: Ship | null; + gears: Ship[]; + groupName: string; +} + +export interface SelectedFleetData { + clusterId: number; + ships: Ship[]; + companyName: string; +} + +interface Props { + ships: Ship[]; + analysisMap?: Map; + clusters?: Map; + onShipSelect?: (mmsi: string) => void; + onFleetZoom?: (bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }) => void; + onSelectedGearChange?: (data: SelectedGearGroupData | null) => void; + onSelectedFleetChange?: (data: SelectedFleetData | null) => void; +} + +// 두 벡터의 외적 (2D) — Graham scan에서 왼쪽 회전 여부 판별 +function cross(o: [number, number], a: [number, number], b: [number, number]): number { + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); +} + +// Graham scan 기반 볼록 껍질 (반시계 방향) +function convexHull(points: [number, number][]): [number, number][] { + const n = points.length; + if (n < 2) return points.slice(); + const sorted = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]); + const lower: [number, number][] = []; + for (const p of sorted) { + while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) { + lower.pop(); + } + lower.push(p); + } + const upper: [number, number][] = []; + for (let i = sorted.length - 1; i >= 0; i--) { + const p = sorted[i]; + while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) { + upper.pop(); + } + upper.push(p); + } + // lower + upper (첫/끝 중복 제거) + lower.pop(); + upper.pop(); + return lower.concat(upper); +} + +// 중심에서 각 꼭짓점 방향으로 padding 확장 +function padPolygon(hull: [number, number][], padding: number): [number, number][] { + if (hull.length === 0) return hull; + const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length; + const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length; + return hull.map(([x, y]) => { + const dx = x - cx; + const dy = y - cy; + const len = Math.sqrt(dx * dx + dy * dy); + if (len === 0) return [x + padding, y + padding] as [number, number]; + const scale = (len + padding) / len; + return [cx + dx * scale, cy + dy * scale] as [number, number]; + }); +} + +// cluster_id 해시 → HSL 색상 +function clusterColor(id: number): string { + const h = (id * 137) % 360; + return `hsl(${h}, 80%, 55%)`; +} + +// HSL 문자열 → hex 근사 (MapLibre는 hsl() 지원하므로 직접 사용 가능) +// GeoJSON feature에 color 속성으로 주입 +interface ClusterPolygonFeature { + type: 'Feature'; + id: number; + properties: { clusterId: number; color: string }; + geometry: { type: 'Polygon'; coordinates: [number, number][][] }; +} + +interface ClusterLineFeature { + type: 'Feature'; + id: number; + properties: { clusterId: number; color: string }; + geometry: { type: 'LineString'; coordinates: [number, number][] }; +} + +type ClusterFeature = ClusterPolygonFeature | ClusterLineFeature; + +const EMPTY_ANALYSIS = new globalThis.Map(); +const EMPTY_CLUSTERS = new globalThis.Map(); + +export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, clusters: clustersProp, onShipSelect, onFleetZoom, onSelectedGearChange, onSelectedFleetChange }: Props) { + const analysisMap = analysisMapProp ?? EMPTY_ANALYSIS; + const clusters = clustersProp ?? EMPTY_CLUSTERS; + const [companies, setCompanies] = useState>(new Map()); + const [expandedFleet, setExpandedFleet] = useState(null); + const [sectionExpanded, setSectionExpanded] = useState>({ + fleet: true, inZone: true, outZone: true, + }); + const toggleSection = (key: string) => setSectionExpanded(prev => ({ ...prev, [key]: !prev[key] })); + const [hoveredFleetId, setHoveredFleetId] = useState(null); + const [expandedGearGroup, setExpandedGearGroup] = useState(null); + const [selectedGearGroup, setSelectedGearGroup] = useState(null); + // 폴리곤 호버 툴팁 + const [hoverTooltip, setHoverTooltip] = useState<{ lng: number; lat: number; type: 'fleet' | 'gear'; id: number | string } | null>(null); + const { current: mapRef } = useMap(); + const registeredRef = useRef(false); + // dataRef는 shipMap/gearGroupMap 선언 이후에 갱신 (아래 참조) + const dataRef = useRef<{ clusters: Map; shipMap: Map; gearGroupMap: Map; onFleetZoom: Props['onFleetZoom'] }>({ clusters, shipMap: new Map(), gearGroupMap: new Map(), onFleetZoom }); + + useEffect(() => { + fetchFleetCompanies().then(setCompanies).catch(() => {}); + }, []); + + // ── 맵 폴리곤 클릭/호버 이벤트 등록 + useEffect(() => { + const map = mapRef?.getMap(); + if (!map || registeredRef.current) return; + + const fleetLayers = ['fleet-cluster-fill-layer']; + const gearLayers = ['gear-cluster-fill-layer']; + const allLayers = [...fleetLayers, ...gearLayers]; + + const setCursor = (cursor: string) => { map.getCanvas().style.cursor = cursor; }; + + const onFleetEnter = (e: MapLayerMouseEvent) => { + setCursor('pointer'); + const feat = e.features?.[0]; + if (!feat) return; + const cid = feat.properties?.clusterId as number | undefined; + if (cid != null) { + setHoveredFleetId(cid); + setHoverTooltip({ lng: e.lngLat.lng, lat: e.lngLat.lat, type: 'fleet', id: cid }); + } + }; + const onFleetLeave = () => { + setCursor(''); + setHoveredFleetId(null); + setHoverTooltip(prev => prev?.type === 'fleet' ? null : prev); + }; + const onFleetClick = (e: MapLayerMouseEvent) => { + const feat = e.features?.[0]; + if (!feat) return; + const cid = feat.properties?.clusterId as number | undefined; + if (cid == null) return; + const d = dataRef.current; + setExpandedFleet(prev => prev === cid ? null : cid); + setExpanded(true); + const mmsiList = d.clusters.get(cid) ?? []; + if (mmsiList.length === 0) return; + let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity; + for (const mmsi of mmsiList) { + const ship = d.shipMap.get(mmsi); + if (!ship) continue; + if (ship.lat < minLat) minLat = ship.lat; + if (ship.lat > maxLat) maxLat = ship.lat; + if (ship.lng < minLng) minLng = ship.lng; + if (ship.lng > maxLng) maxLng = ship.lng; + } + if (minLat !== Infinity) d.onFleetZoom?.({ minLat, maxLat, minLng, maxLng }); + }; + + const onGearEnter = (e: MapLayerMouseEvent) => { + setCursor('pointer'); + const feat = e.features?.[0]; + if (!feat) return; + const name = feat.properties?.name as string | undefined; + if (name) { + setHoverTooltip({ lng: e.lngLat.lng, lat: e.lngLat.lat, type: 'gear', id: name }); + } + }; + const onGearLeave = () => { + setCursor(''); + setHoverTooltip(prev => prev?.type === 'gear' ? null : prev); + }; + const onGearClick = (e: MapLayerMouseEvent) => { + const feat = e.features?.[0]; + if (!feat) return; + const name = feat.properties?.name as string | undefined; + if (!name) return; + const d = dataRef.current; + setSelectedGearGroup(prev => prev === name ? null : name); + setExpandedGearGroup(name); + setSectionExpanded(prev => ({ ...prev, inZone: true, outZone: true })); + requestAnimationFrame(() => { + document.getElementById(`gear-row-${name}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + const entry = d.gearGroupMap.get(name); + if (!entry) return; + const all: Ship[] = [...entry.gears]; + if (entry.parent) all.push(entry.parent); + let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity; + for (const s of all) { + if (s.lat < minLat) minLat = s.lat; + if (s.lat > maxLat) maxLat = s.lat; + if (s.lng < minLng) minLng = s.lng; + if (s.lng > maxLng) maxLng = s.lng; + } + if (minLat !== Infinity) d.onFleetZoom?.({ minLat, maxLat, minLng, maxLng }); + }; + + const register = () => { + const ready = allLayers.every(id => map.getLayer(id)); + if (!ready) return; + registeredRef.current = true; + + for (const id of fleetLayers) { + map.on('mouseenter', id, onFleetEnter); + map.on('mouseleave', id, onFleetLeave); + map.on('click', id, onFleetClick); + } + for (const id of gearLayers) { + map.on('mouseenter', id, onGearEnter); + map.on('mouseleave', id, onGearLeave); + map.on('click', id, onGearClick); + } + }; + + register(); + if (!registeredRef.current) { + const interval = setInterval(() => { + register(); + if (registeredRef.current) clearInterval(interval); + }, 500); + return () => clearInterval(interval); + } + }, [mapRef]); + + // 선박명 → mmsi 맵 (어구 매칭용) + const gearsByParent = useMemo(() => { + const map = new Map(); // parent_mmsi → gears + const gearPattern = /^(.+?)_\d+_\d*$/; + const parentNames = new Map(); // name → mmsi + for (const s of ships) { + if (s.name && !gearPattern.test(s.name)) { + parentNames.set(s.name.trim(), s.mmsi); + } + } + for (const s of ships) { + const m = s.name?.match(gearPattern); + if (!m) continue; + const parentMmsi = parentNames.get(m[1].trim()); + if (parentMmsi) { + const arr = map.get(parentMmsi) ?? []; + arr.push(s); + map.set(parentMmsi, arr); + } + } + return map; + }, [ships]); + + // ships map (mmsi → Ship) + const shipMap = useMemo(() => { + const m = new Map(); + for (const s of ships) m.set(s.mmsi, s); + return m; + }, [ships]); + + // 비허가 어구 클러스터: parentName → { parent: Ship | null, gears: Ship[] } + const gearGroupMap = useMemo(() => { + const gearPattern = /^(.+?)_\d+_\d+_?$/; + const MAX_DIST_DEG = 0.15; // ~10NM + const STALE_MS = 60 * 60_000; + const now = Date.now(); + + const nameToShip = new Map(); + for (const s of ships) { + const nm = (s.name || '').trim(); + if (nm && !gearPattern.test(nm)) { + nameToShip.set(nm, s); + } + } + + // 1단계: 같은 모선명 어구 수집 (60분 이내만) + const rawGroups = new Map(); + for (const s of ships) { + if (now - s.lastSeen > STALE_MS) continue; + const m = (s.name || '').match(gearPattern); + if (!m) continue; + const parentName = m[1].trim(); + const arr = rawGroups.get(parentName) ?? []; + arr.push(s); + rawGroups.set(parentName, arr); + } + + // 2단계: 거리 기반 서브 클러스터링 (같은 이름이라도 멀면 분리) + const map = new Map(); + for (const [parentName, gears] of rawGroups) { + const parent = nameToShip.get(parentName) ?? null; + + // 기준점: 모선 있으면 모선 위치, 없으면 첫 어구 + const anchor = parent ?? gears[0]; + if (!anchor) continue; + + const nearby = gears.filter(g => { + const dlat = Math.abs(g.lat - anchor.lat); + const dlng = Math.abs(g.lng - anchor.lng); + return dlat <= MAX_DIST_DEG && dlng <= MAX_DIST_DEG; + }); + + if (nearby.length === 0) continue; + map.set(parentName, { parent, gears: nearby }); + } + return map; + }, [ships]); + + // stale closure 방지 — shipMap/gearGroupMap 선언 이후 갱신 + dataRef.current = { clusters, shipMap, gearGroupMap, onFleetZoom }; + + // 선택된 어구 그룹 데이터를 부모에 전달 (deck.gl 렌더링용) + useEffect(() => { + if (!selectedGearGroup) { + onSelectedGearChange?.(null); + return; + } + const entry = gearGroupMap.get(selectedGearGroup); + if (entry) { + onSelectedGearChange?.({ parent: entry.parent, gears: entry.gears, groupName: selectedGearGroup }); + } else { + onSelectedGearChange?.(null); + } + }, [selectedGearGroup, gearGroupMap, onSelectedGearChange]); + + // 선택된 선단 데이터를 부모에 전달 (deck.gl 강조 렌더링용) + useEffect(() => { + if (expandedFleet === null) { + onSelectedFleetChange?.(null); + return; + } + const mmsiList = clusters.get(expandedFleet) ?? []; + const fleetShips = mmsiList.map(mmsi => shipMap.get(mmsi)).filter((s): s is Ship => !!s); + const company = companies.get(expandedFleet); + onSelectedFleetChange?.({ + clusterId: expandedFleet, + ships: fleetShips, + companyName: company?.nameCn || `선단 #${expandedFleet}`, + }); + }, [expandedFleet, clusters, shipMap, companies, onSelectedFleetChange]); + + // 어구 그룹을 수역 내/외로 분류 + const { inZoneGearGroups, outZoneGearGroups } = useMemo(() => { + const inZone: { name: string; parent: Ship | null; gears: Ship[]; zone: string }[] = []; + const outZone: { name: string; parent: Ship | null; gears: Ship[] }[] = []; + for (const [name, { parent, gears }] of gearGroupMap) { + const anchor = parent ?? gears[0]; + if (!anchor) { outZone.push({ name, parent, gears }); continue; } + const zoneInfo = classifyFishingZone(anchor.lat, anchor.lng); + if (zoneInfo.zone !== 'OUTSIDE') { + inZone.push({ name, parent, gears, zone: zoneInfo.name }); + } else { + outZone.push({ name, parent, gears }); + } + } + inZone.sort((a, b) => b.gears.length - a.gears.length); + outZone.sort((a, b) => b.gears.length - a.gears.length); + return { inZoneGearGroups: inZone, outZoneGearGroups: outZone }; + }, [gearGroupMap]); + + // 어구 클러스터 GeoJSON (수역 내: 붉은색, 수역 외: 오렌지) + const gearClusterGeoJson = useMemo((): GeoJSON => { + const inZoneNames = new Set(inZoneGearGroups.map(g => g.name)); + const features: GeoJSON.Feature[] = []; + for (const [parentName, { parent, gears }] of gearGroupMap) { + const points: [number, number][] = gears.map(g => [g.lng, g.lat]); + if (parent) points.push([parent.lng, parent.lat]); + if (points.length < 3) continue; + const hull = convexHull(points); + const padded = padPolygon(hull, 0.01); + padded.push(padded[0]); + features.push({ + type: 'Feature', + properties: { name: parentName, gearCount: gears.length, inZone: inZoneNames.has(parentName) ? 1 : 0 }, + geometry: { type: 'Polygon', coordinates: [padded] }, + }); + } + return { type: 'FeatureCollection', features }; + }, [gearGroupMap, inZoneGearGroups]); + + const handleGearGroupZoom = useCallback((parentName: string) => { + setSelectedGearGroup(prev => prev === parentName ? null : parentName); + setExpandedGearGroup(parentName); + requestAnimationFrame(() => { + document.getElementById(`gear-row-${parentName}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }); + const entry = gearGroupMap.get(parentName); + if (!entry) return; + const all: Ship[] = [...entry.gears]; + if (entry.parent) all.push(entry.parent); + if (all.length === 0) return; + let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity; + for (const s of all) { + if (s.lat < minLat) minLat = s.lat; + if (s.lat > maxLat) maxLat = s.lat; + if (s.lng < minLng) minLng = s.lng; + if (s.lng > maxLng) maxLng = s.lng; + } + if (minLat === Infinity) return; + onFleetZoom?.({ minLat, maxLat, minLng, maxLng }); + }, [gearGroupMap, onFleetZoom]); + + // GeoJSON 피처 생성 + const polygonFeatures = useMemo((): ClusterFeature[] => { + const features: ClusterFeature[] = []; + for (const [clusterId, mmsiList] of clusters) { + const points: [number, number][] = []; + for (const mmsi of mmsiList) { + const ship = shipMap.get(mmsi); + if (ship) points.push([ship.lng, ship.lat]); + } + if (points.length < 2) continue; + + const color = clusterColor(clusterId); + + if (points.length === 2) { + features.push({ + type: 'Feature', + id: clusterId, + properties: { clusterId, color }, + geometry: { type: 'LineString', coordinates: points }, + }); + continue; + } + + const hull = convexHull(points); + const padded = padPolygon(hull, 0.02); + // 폴리곤 닫기 + const ring = [...padded, padded[0]]; + features.push({ + type: 'Feature', + id: clusterId, + properties: { clusterId, color }, + geometry: { type: 'Polygon', coordinates: [ring] }, + }); + } + return features; + }, [clusters, shipMap]); + + const polygonGeoJSON = useMemo((): GeoJSON => ({ + type: 'FeatureCollection', + features: polygonFeatures.filter(f => f.geometry.type === 'Polygon'), + }), [polygonFeatures]); + + const lineGeoJSON = useMemo((): GeoJSON => ({ + type: 'FeatureCollection', + features: polygonFeatures.filter(f => f.geometry.type === 'LineString'), + }), [polygonFeatures]); + + // 호버 하이라이트용 단일 폴리곤 + const hoveredGeoJSON = useMemo((): GeoJSON => { + if (hoveredFleetId === null) return { type: 'FeatureCollection', features: [] }; + const f = polygonFeatures.find(p => p.properties.clusterId === hoveredFleetId && p.geometry.type === 'Polygon'); + if (!f) return { type: 'FeatureCollection', features: [] }; + return { type: 'FeatureCollection', features: [f] }; + }, [hoveredFleetId, polygonFeatures]); + + const handleFleetZoom = useCallback((clusterId: number) => { + const mmsiList = clusters.get(clusterId) ?? []; + if (mmsiList.length === 0) return; + let minLat = Infinity, maxLat = -Infinity, minLng = Infinity, maxLng = -Infinity; + for (const mmsi of mmsiList) { + const ship = shipMap.get(mmsi); + if (!ship) continue; + if (ship.lat < minLat) minLat = ship.lat; + if (ship.lat > maxLat) maxLat = ship.lat; + if (ship.lng < minLng) minLng = ship.lng; + if (ship.lng > maxLng) maxLng = ship.lng; + } + if (minLat === Infinity) return; + onFleetZoom?.({ minLat, maxLat, minLng, maxLng }); + }, [clusters, shipMap, onFleetZoom]); + + const fleetList = useMemo(() => { + return Array.from(clusters.entries()) + .map(([id, mmsiList]) => ({ id, mmsiList })) + .sort((a, b) => b.mmsiList.length - a.mmsiList.length); + }, [clusters]); + + // 패널 스타일 (AnalysisStatsPanel 패턴) + const panelStyle: React.CSSProperties = { + position: 'absolute', + bottom: 60, + left: 10, + zIndex: 10, + minWidth: 220, + maxWidth: 300, + backgroundColor: 'rgba(12, 24, 37, 0.92)', + border: '1px solid rgba(99, 179, 237, 0.25)', + borderRadius: 8, + color: '#e2e8f0', + fontFamily: 'monospace, sans-serif', + fontSize: 11, + boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5)', + overflow: 'hidden', + display: 'flex', + flexDirection: 'column', + pointerEvents: 'auto', + }; + + const headerStyle: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + padding: '6px 10px', + borderBottom: 'none', + cursor: 'default', + userSelect: 'none', + flexShrink: 0, + }; + + const toggleButtonStyle: React.CSSProperties = { + background: 'none', + border: 'none', + color: '#94a3b8', + cursor: 'pointer', + fontSize: 10, + padding: '0 2px', + lineHeight: 1, + }; + + return ( + <> + {/* 선단 폴리곤 레이어 */} + + + + + + {/* 2척 선단 라인 */} + + + + + {/* 호버 하이라이트 (별도 Source) */} + + + + + {/* 선택된 어구 그룹 하이라이트 폴리곤 (deck.gl에서 어구 아이콘 + 모선 마커 표시) */} + {selectedGearGroup && (() => { + const entry = gearGroupMap.get(selectedGearGroup); + if (!entry) return null; + const points: [number, number][] = entry.gears.map(g => [g.lng, g.lat]); + if (entry.parent) points.push([entry.parent.lng, entry.parent.lat]); + + const hlFeatures: GeoJSON.Feature[] = []; + if (points.length >= 3) { + const hull = convexHull(points); + const padded = padPolygon(hull, 0.01); + padded.push(padded[0]); + hlFeatures.push({ + type: 'Feature', + properties: {}, + geometry: { type: 'Polygon', coordinates: [padded] }, + }); + } + if (hlFeatures.length === 0) return null; + const hlGeoJson: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: hlFeatures }; + + return ( + + + + + ); + })()} + + {/* 비허가 어구 클러스터 폴리곤 */} + + + + + + {/* 폴리곤 호버 툴팁 */} + {hoverTooltip && (() => { + if (hoverTooltip.type === 'fleet') { + const cid = hoverTooltip.id as number; + const mmsiList = clusters.get(cid) ?? []; + const company = companies.get(cid); + const gearCount = mmsiList.reduce((acc, mmsi) => acc + (gearsByParent.get(mmsi)?.length ?? 0), 0); + return ( + +
+
+ {company?.nameCn || `선단 #${cid}`} +
+
선박 {mmsiList.length}척 · 어구 {gearCount}개
+ {expandedFleet === cid && mmsiList.slice(0, 5).map(mmsi => { + const s = shipMap.get(mmsi); + const dto = analysisMap.get(mmsi); + const role = dto?.algorithms.fleetRole.role ?? ''; + return s ? ( +
+ {role === 'LEADER' ? '★' : '·'} {s.name || mmsi} {s.speed?.toFixed(1)}kt +
+ ) : null; + })} +
클릭하여 상세 보기
+
+
+ ); + } + if (hoverTooltip.type === 'gear') { + const name = hoverTooltip.id as string; + const entry = gearGroupMap.get(name); + if (!entry) return null; + return ( + +
+
+ {name} 어구 {entry.gears.length}개 +
+ {entry.parent && ( +
모선: {entry.parent.name || entry.parent.mmsi}
+ )} + {selectedGearGroup === name && entry.gears.slice(0, 5).map(g => ( +
+ · {g.name || g.mmsi} +
+ ))} +
클릭하여 선택/해제
+
+
+ ); + } + return null; + })()} + + {/* 선단 목록 패널 */} +
+
+ {/* ── 선단 현황 섹션 ── */} +
toggleSection('fleet')}> + + 선단 현황 ({fleetList.length}개) + + +
+ {sectionExpanded.fleet && ( +
+ {fleetList.length === 0 ? ( +
+ 선단 데이터 없음 +
+ ) : ( + fleetList.map(({ id, mmsiList }) => { + const company = companies.get(id); + const companyName = company?.nameCn ?? `선단 #${id}`; + const color = clusterColor(id); + const isOpen = expandedFleet === id; + const isHovered = hoveredFleetId === id; + + const mainVessels = mmsiList.filter(mmsi => { + const dto = analysisMap.get(mmsi); + return dto?.algorithms.fleetRole.role === 'LEADER' || dto?.algorithms.fleetRole.role === 'MEMBER'; + }); + const gearCount = mmsiList.reduce((acc, mmsi) => acc + (gearsByParent.get(mmsi)?.length ?? 0), 0); + + return ( +
+ {/* 선단 행 */} +
setHoveredFleetId(id)} + onMouseLeave={() => setHoveredFleetId(null)} + style={{ + display: 'flex', + alignItems: 'center', + gap: 4, + padding: '4px 10px', + cursor: 'pointer', + backgroundColor: isHovered ? 'rgba(255,255,255,0.06)' : 'transparent', + borderLeft: isOpen ? `2px solid ${color}` : '2px solid transparent', + transition: 'background-color 0.1s', + }} + > + {/* 펼침 토글 */} + setExpandedFleet(prev => (prev === id ? null : id))} + style={{ color: '#94a3b8', fontSize: 9, flexShrink: 0, cursor: 'pointer' }} + > + {isOpen ? '▾' : '▸'} + + {/* 색상 인디케이터 */} + + {/* 회사명 */} + setExpandedFleet(prev => (prev === id ? null : id))} + style={{ + flex: 1, + color: '#e2e8f0', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + cursor: 'pointer', + }} + title={company ? `${company.nameCn} / ${company.nameEn}` : `선단 #${id}`} + > + {companyName} + + {/* 선박 수 */} + + ({mmsiList.length}척) + + {/* zoom 버튼 */} + +
+ + {/* 선단 상세 */} + {isOpen && ( +
+ {/* 선박 목록 */} +
선박:
+ {(mainVessels.length > 0 ? mainVessels : mmsiList).map(mmsi => { + const ship = shipMap.get(mmsi); + const dto = analysisMap.get(mmsi); + const role = dto?.algorithms.fleetRole.role ?? 'MEMBER'; + const displayName = ship?.name || mmsi; + return ( +
+ + {displayName} + + + ({role === 'LEADER' ? 'MAIN' : 'SUB'}) + + +
+ ); + })} + + {/* 어구 목록 */} + {gearCount > 0 && ( + <> +
+ 어구: {gearCount}개 +
+ {mmsiList.flatMap(mmsi => gearsByParent.get(mmsi) ?? []).map(gear => ( +
+ {gear.name || gear.mmsi} +
+ ))} + + )} +
+ )} +
+ ); + }) + )} + +
+ )} + + {/* ── 조업구역내 어구 그룹 섹션 ── */} + {inZoneGearGroups.length > 0 && ( + <> +
toggleSection('inZone')}> + + 조업구역내 어구 ({inZoneGearGroups.length}개) + + +
+ {sectionExpanded.inZone && ( +
+ {inZoneGearGroups.map(({ name, parent, gears, zone }) => { + const isOpen = expandedGearGroup === name; + const accentColor = '#dc2626'; + return ( +
+
{ (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(220,38,38,0.06)'; }} + onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; }} + > + setExpandedGearGroup(prev => (prev === name ? null : name))} style={{ color: '#94a3b8', fontSize: 9, flexShrink: 0, cursor: 'pointer' }}>{isOpen ? '▾' : '▸'} + + setExpandedGearGroup(prev => (prev === name ? null : name))} style={{ flex: 1, color: '#e2e8f0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', cursor: 'pointer' }} title={`${name} — ${zone}`}>{name} + {zone} + ({gears.length}) + +
+ {isOpen && ( +
+ {parent &&
모선: {parent.name || parent.mmsi}
} +
어구 목록:
+ {gears.map(g => ( +
+ {g.name || g.mmsi} + +
+ ))} +
+ )} +
+ ); + })} +
+ )} + + )} + + {/* ── 비허가 어구 그룹 섹션 ── */} + {outZoneGearGroups.length > 0 && ( + <> +
toggleSection('outZone')}> + + 비허가 어구 ({outZoneGearGroups.length}개) + + +
+ {sectionExpanded.outZone && ( +
+ {outZoneGearGroups.map(({ name, parent, gears }) => { + const isOpen = expandedGearGroup === name; + return ( +
+
{ (e.currentTarget as HTMLDivElement).style.backgroundColor = 'rgba(255,255,255,0.04)'; }} + onMouseLeave={e => { (e.currentTarget as HTMLDivElement).style.backgroundColor = 'transparent'; }} + > + setExpandedGearGroup(prev => (prev === name ? null : name))} + style={{ color: '#94a3b8', fontSize: 9, flexShrink: 0, cursor: 'pointer' }} + > + {isOpen ? '▾' : '▸'} + + + setExpandedGearGroup(prev => (prev === name ? null : name))} + style={{ + flex: 1, + color: '#e2e8f0', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + cursor: 'pointer', + }} + title={name} + > + {name} + + + ({gears.length}개) + + +
+ + {isOpen && ( +
+ {parent && ( +
+ 모선: {parent.name || parent.mmsi} +
+ )} +
어구 목록:
+ {gears.map(g => ( +
+ + {g.name || g.mmsi} + + +
+ ))} +
+ )} +
+ ); + })} +
+ )} + + )} +
+
+ + ); +} + +export default FleetClusterLayer; diff --git a/frontend/src/components/korea/GovBuildingLayer.tsx b/frontend/src/components/korea/GovBuildingLayer.tsx index 0ce0401..ddb609b 100644 --- a/frontend/src/components/korea/GovBuildingLayer.tsx +++ b/frontend/src/components/korea/GovBuildingLayer.tsx @@ -1,6 +1,4 @@ -import { useState } from 'react'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { GOV_BUILDINGS } from '../../data/govBuildings'; +import { Popup } from 'react-map-gl/maplibre'; import type { GovBuilding } from '../../data/govBuildings'; const COUNTRY_STYLE: Record = { @@ -18,79 +16,48 @@ const TYPE_STYLE: Record defense: { icon: '🛡️', label: '국방부', color: '#dc2626' }, }; -export function GovBuildingLayer() { - const [selected, setSelected] = useState(null); +interface Props { + selected: GovBuilding | null; + onClose: () => void; +} +export function GovBuildingLayer({ selected, onClose }: Props) { + if (!selected) return null; + const cs = COUNTRY_STYLE[selected.country] || COUNTRY_STYLE.CN; + const ts = TYPE_STYLE[selected.type] || TYPE_STYLE.executive; return ( - <> - {GOV_BUILDINGS.map(g => { - const ts = TYPE_STYLE[g.type] || TYPE_STYLE.executive; - return ( - { e.originalEvent.stopPropagation(); setSelected(g); }}> -
-
- {ts.icon} -
-
- {g.nameKo.length > 10 ? g.nameKo.slice(0, 10) + '..' : g.nameKo} -
-
-
- ); - })} - - {selected && (() => { - const cs = COUNTRY_STYLE[selected.country] || COUNTRY_STYLE.CN; - const ts = TYPE_STYLE[selected.type] || TYPE_STYLE.executive; - return ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="320px" className="gl-popup"> -
-
- {cs.flag} - {ts.icon} {selected.nameKo} -
-
- - {ts.label} - - - {cs.label} - -
-
- {selected.description} -
-
- {selected.name} -
-
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
-
-
- ); - })()} - + +
+
+ {cs.flag} + {ts.icon} {selected.nameKo} +
+
+ + {ts.label} + + + {cs.label} + +
+
+ {selected.description} +
+
+ {selected.name} +
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+
+
); } diff --git a/frontend/src/components/korea/KoreaAirportLayer.tsx b/frontend/src/components/korea/KoreaAirportLayer.tsx index 8febd1d..7a37366 100644 --- a/frontend/src/components/korea/KoreaAirportLayer.tsx +++ b/frontend/src/components/korea/KoreaAirportLayer.tsx @@ -1,7 +1,5 @@ -import { useState } from 'react'; +import { Popup } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { KOREAN_AIRPORTS } from '../../services/airports'; import type { KoreanAirport } from '../../services/airports'; const COUNTRY_COLOR: Record = { @@ -12,100 +10,72 @@ const COUNTRY_COLOR: Record(null); +interface Props { + selected: KoreanAirport | null; + onClose: () => void; +} + +export function KoreaAirportLayer({ selected, onClose }: Props) { const { t } = useTranslation(); - + if (!selected) return null; + const color = getColor(selected); + const info = getCountryInfo(selected); return ( - <> - {KOREAN_AIRPORTS.map(ap => { - const color = getColor(ap); - const size = ap.intl ? 20 : 16; - return ( - { e.originalEvent.stopPropagation(); setSelected(ap); }}> -
- - - - -
- {ap.nameKo.replace('국제공항', '').replace('공항', '')} -
-
-
- ); - })} - - {selected && (() => { - const color = getColor(selected); - const info = getCountryInfo(selected); - return ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="280px" className="gl-popup"> -
-
- {info.flag} - {selected.nameKo} -
-
- {selected.intl && ( - - {t('airport.international')} - - )} - {selected.domestic && ( - - {t('airport.domestic')} - - )} - - {info.label} - -
-
-
IATA : {selected.id}
-
ICAO : {selected.icao}
-
-
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
- -
-
- ); - })()} - + +
+
+ {info.flag} + {selected.nameKo} +
+
+ {selected.intl && ( + + {t('airport.international')} + + )} + {selected.domestic && ( + + {t('airport.domestic')} + + )} + + {info.label} + +
+
+
IATA : {selected.id}
+
ICAO : {selected.icao}
+
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+ +
+
); } diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 6a79913..e608001 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -1,34 +1,40 @@ -import { useRef, useState, useEffect } from 'react'; +import { useRef, useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Map, NavigationControl, Marker, Source, Layer } from 'react-map-gl/maplibre'; +import { Map, NavigationControl, Marker, Popup, Source, Layer } from 'react-map-gl/maplibre'; import type { MapRef } from 'react-map-gl/maplibre'; +import { ScatterplotLayer, TextLayer } from '@deck.gl/layers'; +import { DeckGLOverlay } from '../layers/DeckGLOverlay'; +import { useAnalysisDeckLayers } from '../../hooks/useAnalysisDeckLayers'; +import { useStaticDeckLayers } from '../../hooks/useStaticDeckLayers'; +import type { StaticPickInfo } from '../../hooks/useStaticDeckLayers'; +import fishingZonesData from '../../data/zones/fishing-zones-wgs84.json'; import { ShipLayer } from '../layers/ShipLayer'; import { InfraLayer } from './InfraLayer'; import { SatelliteLayer } from '../layers/SatelliteLayer'; import { AircraftLayer } from '../layers/AircraftLayer'; import { SubmarineCableLayer } from './SubmarineCableLayer'; import { CctvLayer } from './CctvLayer'; -import { KoreaAirportLayer } from './KoreaAirportLayer'; -import { CoastGuardLayer } from './CoastGuardLayer'; -import { NavWarningLayer } from './NavWarningLayer'; +// 정적 레이어들은 useStaticDeckLayers로 전환됨 import { OsintMapLayer } from './OsintMapLayer'; import { EezLayer } from './EezLayer'; -import { PiracyLayer } from './PiracyLayer'; -import { WindFarmLayer } from './WindFarmLayer'; -import { PortLayer } from './PortLayer'; -import { MilitaryBaseLayer } from './MilitaryBaseLayer'; -import { GovBuildingLayer } from './GovBuildingLayer'; -import { NKLaunchLayer } from './NKLaunchLayer'; -import { NKMissileEventLayer } from './NKMissileEventLayer'; +// PiracyLayer, WindFarmLayer, PortLayer, MilitaryBaseLayer, GovBuildingLayer, +// NKLaunchLayer, NKMissileEventLayer → useStaticDeckLayers로 전환됨 import { ChineseFishingOverlay } from './ChineseFishingOverlay'; -import { HazardFacilityLayer } from './HazardFacilityLayer'; -import { CnFacilityLayer } from './CnFacilityLayer'; -import { JpFacilityLayer } from './JpFacilityLayer'; +// HazardFacilityLayer, CnFacilityLayer, JpFacilityLayer → useStaticDeckLayers로 전환됨 +import { AnalysisOverlay } from './AnalysisOverlay'; +import { FleetClusterLayer } from './FleetClusterLayer'; +import type { SelectedGearGroupData, SelectedFleetData } from './FleetClusterLayer'; +import { FishingZoneLayer } from './FishingZoneLayer'; +import { AnalysisStatsPanel } from './AnalysisStatsPanel'; +import { getMarineTrafficCategory } from '../../utils/marineTraffic'; +import { classifyFishingZone } from '../../utils/fishingAnalysis'; import { fetchKoreaInfra } from '../../services/infra'; import type { PowerFacility } from '../../services/infra'; import type { Ship, Aircraft, SatellitePosition } from '../../types'; import type { OsintItem } from '../../services/osint'; +import type { UseVesselAnalysisResult } from '../../hooks/useVesselAnalysis'; import { countryLabelsGeoJSON } from '../../data/countryLabels'; +import { useLocalStorage } from '../../hooks/useLocalStorage'; import 'maplibre-gl/dist/maplibre-gl.css'; export interface KoreaFiltersState { @@ -42,6 +48,7 @@ export interface KoreaFiltersState { interface Props { ships: Ship[]; + allShips?: Ship[]; aircraft: Aircraft[]; satellites: SatellitePosition[]; layers: Record; @@ -52,6 +59,7 @@ interface Props { cableWatchSuspects: Set; dokdoWatchSuspects: Set; dokdoAlerts: { mmsi: string; name: string; dist: number; time: number }[]; + vesselAnalysis?: UseVesselAnalysisResult; } // MarineTraffic-style: satellite + dark ocean + nautical overlay @@ -124,21 +132,356 @@ const FILTER_I18N_KEY: Record = { ferryWatch: 'filters.ferryWatchMonitor', }; -export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts }: Props) { +export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis }: Props) { const { t } = useTranslation(); const mapRef = useRef(null); const [infra, setInfra] = useState([]); + const [flyToTarget, setFlyToTarget] = useState<{ lng: number; lat: number; zoom: number } | null>(null); + const [selectedAnalysisMmsi, setSelectedAnalysisMmsi] = useState(null); + const [trackCoords, setTrackCoords] = useState<[number, number][] | null>(null); + const [selectedGearData, setSelectedGearData] = useState(null); + const [selectedFleetData, setSelectedFleetData] = useState(null); + const [zoomLevel, setZoomLevel] = useState(KOREA_MAP_ZOOM); + const [staticPickInfo, setStaticPickInfo] = useState(null); + const [analysisPanelOpen, setAnalysisPanelOpen] = useLocalStorage('analysisPanelOpen', false); useEffect(() => { fetchKoreaInfra().then(setInfra).catch(() => {}); }, []); + useEffect(() => { + if (flyToTarget && mapRef.current) { + mapRef.current.flyTo({ center: [flyToTarget.lng, flyToTarget.lat], zoom: flyToTarget.zoom, duration: 1500 }); + setFlyToTarget(null); + } + }, [flyToTarget]); + + useEffect(() => { + if (!selectedAnalysisMmsi) setTrackCoords(null); + }, [selectedAnalysisMmsi]); + + const handleAnalysisShipSelect = useCallback((mmsi: string) => { + setSelectedAnalysisMmsi(mmsi); + const ship = (allShips ?? ships).find(s => s.mmsi === mmsi); + if (ship) setFlyToTarget({ lng: ship.lng, lat: ship.lat, zoom: 12 }); + }, [allShips, ships]); + + const handleTrackLoad = useCallback((_mmsi: string, coords: [number, number][]) => { + setTrackCoords(coords); + }, []); + + const handleFleetZoom = useCallback((bounds: { minLat: number; maxLat: number; minLng: number; maxLng: number }) => { + mapRef.current?.fitBounds( + [[bounds.minLng, bounds.minLat], [bounds.maxLng, bounds.maxLat]], + { padding: 60, duration: 1500 }, + ); + }, []); + + // 줌 레벨별 아이콘/심볼 스케일 배율 — z4=1.0x 기준, 2단계씩 상향 + // 줌 레벨별 아이콘/심볼 스케일 배율 — z4=0.8x, z6=1.0x 시작, 2단계씩 상향 + const zoomScale = useMemo(() => { + if (zoomLevel <= 4) return 0.8; + if (zoomLevel <= 5) return 0.9; + if (zoomLevel <= 6) return 1.0; + if (zoomLevel <= 7) return 1.2; + if (zoomLevel <= 8) return 1.5; + if (zoomLevel <= 9) return 1.8; + if (zoomLevel <= 10) return 2.2; + if (zoomLevel <= 11) return 2.5; + if (zoomLevel <= 12) return 2.8; + if (zoomLevel <= 13) return 3.5; + return 4.2; + }, [zoomLevel]); + + // 불법어선 강조 — deck.gl ScatterplotLayer + TextLayer + const illegalFishingData = useMemo(() => { + if (!koreaFilters.illegalFishing) return []; + return (allShips ?? ships).filter(s => { + const mtCat = getMarineTrafficCategory(s.typecode, s.category); + if (mtCat !== 'fishing' || s.flag === 'KR') return false; + return classifyFishingZone(s.lat, s.lng).zone !== 'OUTSIDE'; + }).slice(0, 200); + }, [koreaFilters.illegalFishing, allShips, ships]); + + const illegalFishingLayer = useMemo(() => new ScatterplotLayer({ + id: 'illegal-fishing-highlight', + data: illegalFishingData, + getPosition: (d) => [d.lng, d.lat], + getRadius: 800 * zoomScale, + getFillColor: [239, 68, 68, 40], + getLineColor: [239, 68, 68, 200], + getLineWidth: 2, + stroked: true, + filled: true, + radiusUnits: 'meters', + lineWidthUnits: 'pixels', + }), [illegalFishingData, zoomScale]); + + const illegalFishingLabelLayer = useMemo(() => new TextLayer({ + id: 'illegal-fishing-labels', + data: illegalFishingData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.name || d.mmsi, + getSize: 10 * zoomScale, + getColor: [239, 68, 68, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 14], + fontFamily: 'monospace', + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), [illegalFishingData, zoomScale]); + + // 수역 라벨 TextLayer — illegalFishing 필터 활성 시만 표시 + const zoneLabelsLayer = useMemo(() => { + if (!koreaFilters.illegalFishing) return null; + const data = (fishingZonesData as GeoJSON.FeatureCollection).features.map(f => { + const geom = f.geometry as GeoJSON.MultiPolygon; + let sLng = 0, sLat = 0, n = 0; + for (const poly of geom.coordinates) { + for (const ring of poly) { + for (const [lng, lat] of ring) { + sLng += lng; sLat += lat; n++; + } + } + } + return { + name: (f.properties as { name: string }).name, + lng: n > 0 ? sLng / n : 0, + lat: n > 0 ? sLat / n : 0, + }; + }); + return new TextLayer({ + id: 'fishing-zone-labels', + data, + getPosition: (d: { lng: number; lat: number }) => [d.lng, d.lat], + getText: (d: { name: string }) => d.name, + getSize: 12 * zoomScale, + getColor: [255, 255, 255, 220], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 3, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }); + }, [koreaFilters.illegalFishing, zoomScale]); + + // 정적 레이어 (deck.gl) — 항구, 공항, 군사기지, 어항경고 등 + const staticDeckLayers = useStaticDeckLayers({ + ports: layers.ports ?? false, + coastGuard: layers.coastGuard ?? false, + windFarm: layers.windFarm ?? false, + militaryBases: layers.militaryBases ?? false, + govBuildings: layers.govBuildings ?? false, + airports: layers.airports ?? false, + navWarning: layers.navWarning ?? false, + nkLaunch: layers.nkLaunch ?? false, + nkMissile: layers.nkMissile ?? false, + piracy: layers.piracy ?? false, + infra: layers.infra ?? false, + infraFacilities: infra, + hazardTypes: [ + ...(layers.hazardPetrochemical ? ['petrochemical' as const] : []), + ...(layers.hazardLng ? ['lng' as const] : []), + ...(layers.hazardOilTank ? ['oilTank' as const] : []), + ...(layers.hazardPort ? ['hazardPort' as const] : []), + ...(layers.energyNuclear ? ['nuclear' as const] : []), + ...(layers.energyThermal ? ['thermal' as const] : []), + ...(layers.industryShipyard ? ['shipyard' as const] : []), + ...(layers.industryWastewater ? ['wastewater' as const] : []), + ...(layers.industryHeavy ? ['heavyIndustry' as const] : []), + ], + cnPower: !!layers.cnPower, + cnMilitary: !!layers.cnMilitary, + jpPower: !!layers.jpPower, + jpMilitary: !!layers.jpMilitary, + onPick: (info) => setStaticPickInfo(info), + sizeScale: zoomScale, + }); + + // 선택된 어구 그룹 — 어구 아이콘 + 모선 강조 (deck.gl) + const selectedGearLayers = useMemo(() => { + if (!selectedGearData) return []; + const { parent, gears, groupName } = selectedGearData; + const layers = []; + + // 어구 위치 — 주황 원형 마커 + layers.push(new ScatterplotLayer({ + id: 'selected-gear-items', + data: gears, + getPosition: (d: Ship) => [d.lng, d.lat], + getRadius: 6 * zoomScale, + getFillColor: [249, 115, 22, 180], + getLineColor: [255, 255, 255, 220], + stroked: true, + filled: true, + radiusUnits: 'pixels', + lineWidthUnits: 'pixels', + getLineWidth: 1.5, + })); + + // 어구 이름 라벨 + layers.push(new TextLayer({ + id: 'selected-gear-labels', + data: gears, + getPosition: (d: Ship) => [d.lng, d.lat], + getText: (d: Ship) => d.name || d.mmsi, + getSize: 9 * zoomScale, + getColor: [249, 115, 22, 255], + getTextAnchor: 'middle' as const, + getAlignmentBaseline: 'top' as const, + getPixelOffset: [0, 10], + fontFamily: 'monospace', + outlineWidth: 2, + outlineColor: [0, 0, 0, 220], + billboard: false, + characterSet: 'auto', + })); + + // 모선 강조 — 큰 원 + 라벨 + if (parent) { + layers.push(new ScatterplotLayer({ + id: 'selected-gear-parent', + data: [parent], + getPosition: (d: Ship) => [d.lng, d.lat], + getRadius: 14 * zoomScale, + getFillColor: [249, 115, 22, 80], + getLineColor: [249, 115, 22, 255], + stroked: true, + filled: true, + radiusUnits: 'pixels', + lineWidthUnits: 'pixels', + getLineWidth: 3, + })); + layers.push(new TextLayer({ + id: 'selected-gear-parent-label', + data: [parent], + getPosition: (d: Ship) => [d.lng, d.lat], + getText: (d: Ship) => `▼ ${d.name || groupName} (모선)`, + getSize: 11 * zoomScale, + getColor: [249, 115, 22, 255], + getTextAnchor: 'middle' as const, + getAlignmentBaseline: 'top' as const, + getPixelOffset: [0, 18], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 3, + outlineColor: [0, 0, 0, 220], + billboard: false, + characterSet: 'auto', + })); + } + + return layers; + }, [selectedGearData, zoomScale]); + + // 선택된 선단 소속 선박 강조 레이어 (deck.gl) + const selectedFleetLayers = useMemo(() => { + if (!selectedFleetData) return []; + const { ships: fleetShips, clusterId } = selectedFleetData; + if (fleetShips.length === 0) return []; + + // HSL→RGB 인라인 변환 (선단 색상) + const hue = (clusterId * 137) % 360; + const h = hue / 360; const s = 0.7; const l = 0.6; + const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1; if (t > 1) t -= 1; return t < 1/6 ? p + (q-p)*6*t : t < 1/2 ? q : t < 2/3 ? p + (q-p)*(2/3-t)*6 : p; }; + const q = l < 0.5 ? l * (1+s) : l + s - l*s; const p = 2*l - q; + const r = Math.round(hue2rgb(p, q, h + 1/3) * 255); + const g = Math.round(hue2rgb(p, q, h) * 255); + const b = Math.round(hue2rgb(p, q, h - 1/3) * 255); + const color: [number, number, number, number] = [r, g, b, 255]; + const fillColor: [number, number, number, number] = [r, g, b, 80]; + + const result: Layer[] = []; + + // 소속 선박 — 강조 원형 + result.push(new ScatterplotLayer({ + id: 'selected-fleet-items', + data: fleetShips, + getPosition: (d: Ship) => [d.lng, d.lat], + getRadius: 8 * zoomScale, + getFillColor: fillColor, + getLineColor: color, + stroked: true, + filled: true, + radiusUnits: 'pixels', + lineWidthUnits: 'pixels', + getLineWidth: 2, + })); + + // 소속 선박 이름 라벨 + result.push(new TextLayer({ + id: 'selected-fleet-labels', + data: fleetShips, + getPosition: (d: Ship) => [d.lng, d.lat], + getText: (d: Ship) => { + const dto = vesselAnalysis?.analysisMap.get(d.mmsi); + const role = dto?.algorithms.fleetRole.role; + const prefix = role === 'LEADER' ? '★ ' : ''; + return `${prefix}${d.name || d.mmsi}`; + }, + getSize: 9 * zoomScale, + getColor: color, + getTextAnchor: 'middle' as const, + getAlignmentBaseline: 'top' as const, + getPixelOffset: [0, 12], + fontFamily: 'monospace', + fontWeight: 600, + outlineWidth: 2, + outlineColor: [0, 0, 0, 220], + billboard: false, + characterSet: 'auto', + })); + + // 리더 선박 추가 강조 (큰 외곽 링) + const leaders = fleetShips.filter(s => { + const dto = vesselAnalysis?.analysisMap.get(s.mmsi); + return dto?.algorithms.fleetRole.isLeader; + }); + if (leaders.length > 0) { + result.push(new ScatterplotLayer({ + id: 'selected-fleet-leaders', + data: leaders, + getPosition: (d: Ship) => [d.lng, d.lat], + getRadius: 16 * zoomScale, + getFillColor: [0, 0, 0, 0], + getLineColor: color, + stroked: true, + filled: false, + radiusUnits: 'pixels', + lineWidthUnits: 'pixels', + getLineWidth: 3, + })); + } + + return result; + }, [selectedFleetData, zoomScale, vesselAnalysis]); + + // 분석 결과 deck.gl 레이어 + const analysisActiveFilter = koreaFilters.illegalFishing ? 'illegalFishing' + : koreaFilters.darkVessel ? 'darkVessel' + : layers.cnFishing ? 'cnFishing' + : null; + + const analysisDeckLayers = useAnalysisDeckLayers( + vesselAnalysis?.analysisMap ?? new Map(), + allShips ?? ships, + analysisActiveFilter, + zoomScale, + ); + return ( setZoomLevel(Math.floor(e.viewState.zoom))} > @@ -203,7 +546,7 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre /> - {layers.ships && } + {layers.ships && } {/* Transship suspect labels — Marker DOM, inline styles kept for dynamic border color */} {transshipSuspects.size > 0 && ships.filter(s => transshipSuspects.has(s.mmsi)).map(s => ( @@ -265,32 +608,240 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre {layers.aircraft && aircraft.length > 0 && } {layers.cables && } {layers.cctv && } - {layers.windFarm && } - {layers.ports && } - {layers.militaryBases && } - {layers.govBuildings && } - {layers.nkLaunch && } - {layers.nkMissile && } - {layers.cnFishing && } - {layers.hazardPetrochemical && } - {layers.hazardLng && } - {layers.hazardOilTank && } - {layers.hazardPort && } - {layers.energyNuclear && } - {layers.energyThermal && } - {layers.industryShipyard && } - {layers.industryWastewater && } - {layers.industryHeavy && } - {layers.cnPower && } - {layers.cnMilitary && } - {layers.jpPower && } - {layers.jpMilitary && } - {layers.airports && } - {layers.coastGuard && } - {layers.navWarning && } + {/* 정적 레이어들은 deck.gl DeckGLOverlay로 전환됨 — 아래 DOM 레이어 제거 */} + {(koreaFilters.illegalFishing || layers.cnFishing) && } + {layers.cnFishing && } + {/* HazardFacility, CnFacility, JpFacility → useStaticDeckLayers (deck.gl GPU) 전환 완료 */} + {layers.cnFishing && ( + + )} + {vesselAnalysis && vesselAnalysis.analysisMap.size > 0 && ( + + )} + + {/* deck.gl GPU 오버레이 — 불법어선 강조 + 분석 위험도 마커 */} + + {/* 정적 마커 클릭 Popup — 통합 리치 디자인 */} + {staticPickInfo && (() => { + const obj = staticPickInfo.object; + const kind = staticPickInfo.kind; + const lat = obj.lat ?? obj.launchLat ?? 0; + const lng = obj.lng ?? obj.launchLng ?? 0; + if (!lat || !lng) return null; + + // ── kind + subType 기반 메타 결정 ── + const SUB_META: Record> = { + hazard: { + petrochemical: { icon: '🏭', color: '#f97316', label: '석유화학단지' }, + lng: { icon: '🔵', color: '#06b6d4', label: 'LNG저장기지' }, + oilTank: { icon: '🛢️', color: '#eab308', label: '유류저장탱크' }, + hazardPort: { icon: '⚠️', color: '#ef4444', label: '위험물항만하역' }, + nuclear: { icon: '☢️', color: '#a855f7', label: '원자력발전소' }, + thermal: { icon: '🔥', color: '#64748b', label: '화력발전소' }, + shipyard: { icon: '🚢', color: '#0ea5e9', label: '조선소' }, + wastewater: { icon: '💧', color: '#10b981', label: '폐수처리장' }, + heavyIndustry: { icon: '⚙️', color: '#94a3b8', label: '시멘트/제철소' }, + }, + overseas: { + nuclear: { icon: '☢️', color: '#ef4444', label: '핵발전소' }, + thermal: { icon: '🔥', color: '#f97316', label: '화력발전소' }, + naval: { icon: '⚓', color: '#3b82f6', label: '해군기지' }, + airbase: { icon: '✈️', color: '#22d3ee', label: '공군기지' }, + army: { icon: '🪖', color: '#22c55e', label: '육군기지' }, + shipyard:{ icon: '🚢', color: '#94a3b8', label: '조선소' }, + }, + militaryBase: { + naval: { icon: '⚓', color: '#3b82f6', label: '해군기지' }, + airforce:{ icon: '✈️', color: '#22d3ee', label: '공군기지' }, + army: { icon: '🪖', color: '#22c55e', label: '육군기지' }, + missile: { icon: '🚀', color: '#ef4444', label: '미사일기지' }, + joint: { icon: '⭐', color: '#f59e0b', label: '합동사령부' }, + }, + govBuilding: { + executive: { icon: '🏛️', color: '#f59e0b', label: '행정부' }, + legislature: { icon: '🏛️', color: '#3b82f6', label: '입법부' }, + military_hq: { icon: '⭐', color: '#ef4444', label: '군사령부' }, + intelligence: { icon: '🔍', color: '#8b5cf6', label: '정보기관' }, + foreign: { icon: '🌐', color: '#06b6d4', label: '외교부' }, + maritime: { icon: '⚓', color: '#0ea5e9', label: '해양기관' }, + defense: { icon: '🛡️', color: '#dc2626', label: '국방부' }, + }, + nkLaunch: { + icbm: { icon: '🚀', color: '#dc2626', label: 'ICBM 발사장' }, + irbm: { icon: '🚀', color: '#ef4444', label: 'IRBM/MRBM' }, + srbm: { icon: '🎯', color: '#f97316', label: '단거리탄도미사일' }, + slbm: { icon: '🔱', color: '#3b82f6', label: 'SLBM 발사' }, + cruise: { icon: '✈️', color: '#8b5cf6', label: '순항미사일' }, + artillery: { icon: '💥', color: '#eab308', label: '해안포/장사정포' }, + mlrs: { icon: '💥', color: '#f59e0b', label: '방사포(MLRS)' }, + }, + coastGuard: { + hq: { icon: '🏢', color: '#3b82f6', label: '본청' }, + regional: { icon: '🏢', color: '#60a5fa', label: '지방청' }, + station: { icon: '🚔', color: '#22d3ee', label: '해양경찰서' }, + substation: { icon: '🏠', color: '#94a3b8', label: '파출소' }, + vts: { icon: '📡', color: '#f59e0b', label: 'VTS센터' }, + navy: { icon: '⚓', color: '#3b82f6', label: '해군부대' }, + }, + airport: { + international: { icon: '✈️', color: '#a78bfa', label: '국제공항' }, + domestic: { icon: '🛩️', color: '#60a5fa', label: '국내공항' }, + military: { icon: '✈️', color: '#ef4444', label: '군용비행장' }, + }, + navWarning: { + danger: { icon: '⚠️', color: '#ef4444', label: '위험' }, + caution: { icon: '⚠️', color: '#eab308', label: '주의' }, + info: { icon: 'ℹ️', color: '#3b82f6', label: '정보' }, + }, + piracy: { + critical: { icon: '☠️', color: '#ef4444', label: '극고위험' }, + high: { icon: '☠️', color: '#f97316', label: '고위험' }, + moderate: { icon: '☠️', color: '#eab308', label: '주의' }, + }, + }; + + const KIND_DEFAULT: Record = { + port: { icon: '⚓', color: '#3b82f6', label: '항구' }, + windFarm: { icon: '🌀', color: '#00bcd4', label: '풍력단지' }, + militaryBase: { icon: '🪖', color: '#ef4444', label: '군사시설' }, + govBuilding: { icon: '🏛️', color: '#f59e0b', label: '정부기관' }, + nkLaunch: { icon: '🚀', color: '#dc2626', label: '북한 발사장' }, + nkMissile: { icon: '💣', color: '#ef4444', label: '미사일 낙하' }, + coastGuard: { icon: '🚔', color: '#4dabf7', label: '해양경찰' }, + airport: { icon: '✈️', color: '#a78bfa', label: '공항' }, + navWarning: { icon: '⚠️', color: '#eab308', label: '항행경보' }, + piracy: { icon: '☠️', color: '#ef4444', label: '해적위험' }, + infra: { icon: '⚡', color: '#ffeb3b', label: '발전/변전' }, + hazard: { icon: '⚠️', color: '#ef4444', label: '위험시설' }, + cnFacility: { icon: '📍', color: '#ef4444', label: '중국시설' }, + jpFacility: { icon: '📍', color: '#f472b6', label: '일본시설' }, + }; + + // subType 키 결정 + const subKey = obj.type ?? obj.subType ?? obj.level ?? ''; + const subGroup = kind === 'cnFacility' || kind === 'jpFacility' ? 'overseas' : kind; + const meta = SUB_META[subGroup]?.[subKey] ?? KIND_DEFAULT[kind] ?? { icon: '📍', color: '#64748b', label: kind }; + + // 국가 플래그 + const COUNTRY_FLAG: Record = { CN: '🇨🇳', JP: '🇯🇵', KP: '🇰🇵', KR: '🇰🇷', TW: '🇹🇼' }; + const flag = kind === 'cnFacility' ? '🇨🇳' : kind === 'jpFacility' ? '🇯🇵' : COUNTRY_FLAG[obj.country] ?? ''; + const countryName = kind === 'cnFacility' ? '중국' : kind === 'jpFacility' ? '일본' + : { CN: '중국', JP: '일본', KP: '북한', KR: '한국', TW: '대만' }[obj.country] ?? ''; + + // 이름 결정 + const title = obj.nameKo || obj.name || obj.launchNameKo || obj.title || kind; + + return ( + setStaticPickInfo(null)} closeOnClick={false} + maxWidth="280px" className="gl-popup" + > +
+ {/* 컬러 헤더 */} +
+ {meta.icon} {title} +
+ {/* 배지 행 */} +
+ + {meta.label} + + {flag && ( + + {flag} {countryName} + + )} + {kind === 'hazard' && ( + ⚠️ 위험시설 + )} + {kind === 'port' && ( + + {obj.type === 'major' ? '주요항' : '중소항'} + + )} + {kind === 'airport' && obj.intl && ( + 국제선 + )} +
+ {/* 설명 */} + {obj.description && ( +
{obj.description}
+ )} + {obj.detail && ( +
{obj.detail}
+ )} + {obj.note && ( +
{obj.note}
+ )} + {/* 필드 그리드 */} +
+ {obj.operator &&
운영: {obj.operator}
} + {obj.capacity &&
규모: {obj.capacity}
} + {obj.output &&
출력: {obj.output}
} + {obj.source &&
연료: {obj.source}
} + {obj.capacityMW &&
용량: {obj.capacityMW}MW
} + {obj.turbines &&
터빈: {obj.turbines}기
} + {obj.status &&
상태: {obj.status}
} + {obj.year &&
연도: {obj.year}년
} + {obj.region &&
지역: {obj.region}
} + {obj.org &&
기관: {obj.org}
} + {obj.area &&
해역: {obj.area}
} + {obj.altitude &&
고도: {obj.altitude}
} + {obj.address &&
주소: {obj.address}
} + {obj.recentUse &&
최근 사용: {obj.recentUse}
} + {obj.recentIncidents != null &&
최근 1년: {obj.recentIncidents}건
} + {obj.icao &&
ICAO: {obj.icao}
} + {kind === 'nkMissile' && ( + <> + {obj.typeKo &&
미사일: {obj.typeKo}
} + {obj.date &&
발사일: {obj.date} {obj.time}
} + {obj.distanceKm &&
사거리: {obj.distanceKm}km
} + {obj.altitudeKm &&
최고고도: {obj.altitudeKm}km
} + {obj.flightMin &&
비행시간: {obj.flightMin}분
} + {obj.launchNameKo &&
발사지: {obj.launchNameKo}
} + + )} + {obj.name && obj.nameKo && obj.name !== obj.nameKo && ( +
영문: {obj.name}
+ )} +
+ {lat.toFixed(4)}°N, {lng.toFixed(4)}°E +
+
+
+
+ ); + })()} {layers.osint && } {layers.eez && } - {layers.piracy && } {/* Filter Status Banner */} {(() => { @@ -346,6 +897,42 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre ))}
)} + + {/* 선택된 분석 선박 항적 — tracks API 응답 기반 */} + {trackCoords && trackCoords.length > 1 && ( + + + + )} + + {/* AI Analysis Stats Panel — 항상 표시 */} + {vesselAnalysis && ( + + )} ); } diff --git a/frontend/src/components/korea/MilitaryBaseLayer.tsx b/frontend/src/components/korea/MilitaryBaseLayer.tsx index f33cd5e..c413609 100644 --- a/frontend/src/components/korea/MilitaryBaseLayer.tsx +++ b/frontend/src/components/korea/MilitaryBaseLayer.tsx @@ -1,6 +1,4 @@ -import { useState } from 'react'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { MILITARY_BASES } from '../../data/militaryBases'; +import { Popup } from 'react-map-gl/maplibre'; import type { MilitaryBase } from '../../data/militaryBases'; const COUNTRY_STYLE: Record = { @@ -18,91 +16,49 @@ const TYPE_STYLE: Record joint: { icon: '⭐', label: '합동기지', color: '#a78bfa' }, }; -function _MilIcon({ type, size = 16 }: { type: string; size?: number }) { - const ts = TYPE_STYLE[type] || TYPE_STYLE.army; - return ( - - - {ts.icon} - - ); +interface Props { + selected: MilitaryBase | null; + onClose: () => void; } -export function MilitaryBaseLayer() { - const [selected, setSelected] = useState(null); - +export function MilitaryBaseLayer({ selected, onClose }: Props) { + if (!selected) return null; + const cs = COUNTRY_STYLE[selected.country] || COUNTRY_STYLE.CN; + const ts = TYPE_STYLE[selected.type] || TYPE_STYLE.army; return ( - <> - {MILITARY_BASES.map(base => { - const _cs = COUNTRY_STYLE[base.country] || COUNTRY_STYLE.CN; - const ts = TYPE_STYLE[base.type] || TYPE_STYLE.army; - return ( - { e.originalEvent.stopPropagation(); setSelected(base); }}> -
-
- {ts.icon} -
-
- {base.nameKo.length > 12 ? base.nameKo.slice(0, 12) + '..' : base.nameKo} -
-
-
- ); - })} - - {selected && (() => { - const cs = COUNTRY_STYLE[selected.country] || COUNTRY_STYLE.CN; - const ts = TYPE_STYLE[selected.type] || TYPE_STYLE.army; - return ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="300px" className="gl-popup"> -
-
- {cs.flag} - {ts.icon} {selected.nameKo} -
-
- - {ts.label} - - - {cs.label} - -
-
- {selected.description} -
-
-
시설명 : {selected.name}
-
유형 : {ts.label}
-
-
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
-
-
- ); - })()} - + +
+
+ {cs.flag} + {ts.icon} {selected.nameKo} +
+
+ + {ts.label} + + + {cs.label} + +
+
+ {selected.description} +
+
+
시설명 : {selected.name}
+
유형 : {ts.label}
+
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+
+
); } diff --git a/frontend/src/components/korea/NKLaunchLayer.tsx b/frontend/src/components/korea/NKLaunchLayer.tsx index e4a2c73..009c24e 100644 --- a/frontend/src/components/korea/NKLaunchLayer.tsx +++ b/frontend/src/components/korea/NKLaunchLayer.tsx @@ -1,89 +1,53 @@ -import { useState } from 'react'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { NK_LAUNCH_SITES, NK_LAUNCH_TYPE_META } from '../../data/nkLaunchSites'; +import { Popup } from 'react-map-gl/maplibre'; +import { NK_LAUNCH_TYPE_META } from '../../data/nkLaunchSites'; import type { NKLaunchSite } from '../../data/nkLaunchSites'; -export function NKLaunchLayer() { - const [selected, setSelected] = useState(null); +interface Props { + selected: NKLaunchSite | null; + onClose: () => void; +} +export function NKLaunchLayer({ selected, onClose }: Props) { + if (!selected) return null; + const meta = NK_LAUNCH_TYPE_META[selected.type]; return ( - <> - {NK_LAUNCH_SITES.map(site => { - const meta = NK_LAUNCH_TYPE_META[site.type]; - const isArtillery = site.type === 'artillery' || site.type === 'mlrs'; - const size = isArtillery ? 14 : 18; - return ( - { e.originalEvent.stopPropagation(); setSelected(site); }}> -
-
- {meta.icon} -
-
- {site.nameKo.length > 10 ? site.nameKo.slice(0, 10) + '..' : site.nameKo} -
-
-
- ); - })} - - {selected && (() => { - const meta = NK_LAUNCH_TYPE_META[selected.type]; - return ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="320px" className="gl-popup"> -
-
- 🇰🇵 - {meta.icon} {selected.nameKo} -
-
- - {meta.label} - - - 북한 - -
-
- {selected.description} -
- {selected.recentUse && ( -
- 최근: {selected.recentUse} -
- )} -
- {selected.name} -
-
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
-
-
- ); - })()} - + +
+
+ 🇰🇵 + {meta.icon} {selected.nameKo} +
+
+ + {meta.label} + + + 북한 + +
+
+ {selected.description} +
+ {selected.recentUse && ( +
+ 최근: {selected.recentUse} +
+ )} +
+ {selected.name} +
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+
+
); } diff --git a/frontend/src/components/korea/NKMissileEventLayer.tsx b/frontend/src/components/korea/NKMissileEventLayer.tsx index 21fcf68..c307645 100644 --- a/frontend/src/components/korea/NKMissileEventLayer.tsx +++ b/frontend/src/components/korea/NKMissileEventLayer.tsx @@ -1,15 +1,10 @@ -import { useState, useMemo } from 'react'; -import { Marker, Popup, Source, Layer } from 'react-map-gl/maplibre'; +import { useMemo } from 'react'; +import { Popup, Source, Layer } from 'react-map-gl/maplibre'; import { NK_MISSILE_EVENTS } from '../../data/nkMissileEvents'; import type { NKMissileEvent } from '../../data/nkMissileEvents'; import type { Ship } from '../../types'; import { getMarineTrafficCategory } from '../../utils/marineTraffic'; -function isToday(dateStr: string): boolean { - const today = new Date().toISOString().slice(0, 10); - return dateStr === today; -} - function getMissileColor(type: string): string { if (type.includes('ICBM')) return '#dc2626'; if (type.includes('IRBM')) return '#ef4444'; @@ -27,11 +22,11 @@ function distKm(lat1: number, lng1: number, lat2: number, lng2: number): number interface Props { ships: Ship[]; + selected: NKMissileEvent | null; + onClose: () => void; } -export function NKMissileEventLayer({ ships }: Props) { - const [selected, setSelected] = useState(null); - +export function NKMissileEventLayer({ ships, selected, onClose }: Props) { const lineGeoJSON = useMemo(() => ({ type: 'FeatureCollection' as const, features: NK_MISSILE_EVENTS.map(ev => ({ @@ -51,7 +46,7 @@ export function NKMissileEventLayer({ ships }: Props) { return ( <> - {/* 궤적 라인 */} + {/* 궤적 라인 — MapLibre Source/Layer 유지 */} - {/* 발사 지점 (▲) */} - {NK_MISSILE_EVENTS.map(ev => { - const color = getMissileColor(ev.type); - const today = isToday(ev.date); - return ( - -
- - - -
-
- ); - })} - - {/* 낙하 지점 (✕ + 정보 라벨) */} - {NK_MISSILE_EVENTS.map(ev => { - const color = getMissileColor(ev.type); - const today = isToday(ev.date); - return ( - { e.originalEvent.stopPropagation(); setSelected(ev); }}> -
- - - - - {today && ( - - - - - )} - -
- {ev.date.slice(5)} {ev.time} ← {ev.launchNameKo} -
-
-
- ); - })} - {/* 낙하 지점 팝업 */} {selected && (() => { const color = getMissileColor(selected.type); return ( setSelected(null)} closeOnClick={false} + onClose={onClose} closeOnClick={false} anchor="bottom" maxWidth="340px" className="gl-popup">
diff --git a/frontend/src/components/korea/NavWarningLayer.tsx b/frontend/src/components/korea/NavWarningLayer.tsx index ca8bec2..0f0140b 100644 --- a/frontend/src/components/korea/NavWarningLayer.tsx +++ b/frontend/src/components/korea/NavWarningLayer.tsx @@ -1,7 +1,6 @@ -import { useState } from 'react'; +import { Popup } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { NAV_WARNINGS, NW_LEVEL_LABEL, NW_ORG_LABEL } from '../../services/navWarning'; +import { NW_LEVEL_LABEL, NW_ORG_LABEL } from '../../services/navWarning'; import type { NavWarning, NavWarningLevel, TrainingOrg } from '../../services/navWarning'; const LEVEL_COLOR: Record = { @@ -19,112 +18,68 @@ const ORG_COLOR: Record = { '국과연': '#eab308', }; -function WarningIcon({ level, org, size }: { level: NavWarningLevel; org: TrainingOrg; size: number }) { - const color = ORG_COLOR[org]; - - if (level === 'danger') { - return ( - - - - - - ); - } - - return ( - - - - - - ); +interface Props { + selected: NavWarning | null; + onClose: () => void; } -export function NavWarningLayer() { - const [selected, setSelected] = useState(null); +export function NavWarningLayer({ selected, onClose }: Props) { const { t } = useTranslation(); - + if (!selected) return null; return ( - <> - {NAV_WARNINGS.map(w => { - const color = ORG_COLOR[w.org]; - const size = w.level === 'danger' ? 16 : 14; - return ( - { e.originalEvent.stopPropagation(); setSelected(w); }}> -
- -
- {w.id} -
-
-
- ); - })} - - {selected && ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="320px" className="gl-popup"> -
-
- {selected.title} -
-
- - {NW_LEVEL_LABEL[selected.level]} - - - {NW_ORG_LABEL[selected.org]} - - - {selected.area} - -
-
- {selected.description} -
-
-
{t('navWarning.altitude')}: {selected.altitude}
-
{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E
-
{t('navWarning.source')}: {selected.source}
-
- {t('navWarning.khoaLink')} -
-
- )} - + +
+
+ {selected.title} +
+
+ + {NW_LEVEL_LABEL[selected.level]} + + + {NW_ORG_LABEL[selected.org]} + + + {selected.area} + +
+
+ {selected.description} +
+
+
{t('navWarning.altitude')}: {selected.altitude}
+
{selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E
+
{t('navWarning.source')}: {selected.source}
+
+ {t('navWarning.khoaLink')} +
+
); } diff --git a/frontend/src/components/korea/PiracyLayer.tsx b/frontend/src/components/korea/PiracyLayer.tsx index a575c53..65fd727 100644 --- a/frontend/src/components/korea/PiracyLayer.tsx +++ b/frontend/src/components/korea/PiracyLayer.tsx @@ -1,95 +1,57 @@ -import { useState } from 'react'; +import { Popup } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { PIRACY_ZONES, PIRACY_LEVEL_COLOR, PIRACY_LEVEL_LABEL } from '../../services/piracy'; +import { PIRACY_LEVEL_COLOR, PIRACY_LEVEL_LABEL } from '../../services/piracy'; import type { PiracyZone } from '../../services/piracy'; -function SkullIcon({ color, size }: { color: string; size: number }) { - return ( - - - - - - - - - - ); +interface Props { + selected: PiracyZone | null; + onClose: () => void; } -export function PiracyLayer() { - const [selected, setSelected] = useState(null); +export function PiracyLayer({ selected, onClose }: Props) { const { t } = useTranslation(); - + if (!selected) return null; return ( - <> - {PIRACY_ZONES.map(zone => { - const color = PIRACY_LEVEL_COLOR[zone.level]; - const size = zone.level === 'critical' ? 28 : zone.level === 'high' ? 24 : 20; - return ( - { e.originalEvent.stopPropagation(); setSelected(zone); }}> -
- -
- {PIRACY_LEVEL_LABEL[zone.level]} -
-
-
- ); - })} + +
+
+ ☠️ + {selected.nameKo} +
- {selected && ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="340px" className="gl-popup"> -
-
- ☠️ - {selected.nameKo} -
+
+ + {PIRACY_LEVEL_LABEL[selected.level]} + + + {selected.name} + + {selected.recentIncidents != null && ( + + {t('piracy.recentIncidents', { count: selected.recentIncidents })} + + )} +
-
- - {PIRACY_LEVEL_LABEL[selected.level]} - - - {selected.name} - - {selected.recentIncidents != null && ( - - {t('piracy.recentIncidents', { count: selected.recentIncidents })} - - )} -
- -
- {selected.description} -
-
- {selected.detail} -
-
- {selected.lat.toFixed(2)}°N, {selected.lng.toFixed(2)}°E -
-
-
- )} - +
+ {selected.description} +
+
+ {selected.detail} +
+
+ {selected.lat.toFixed(2)}°N, {selected.lng.toFixed(2)}°E +
+
+
); } diff --git a/frontend/src/components/korea/PortLayer.tsx b/frontend/src/components/korea/PortLayer.tsx index d9d3ff4..9019ef5 100644 --- a/frontend/src/components/korea/PortLayer.tsx +++ b/frontend/src/components/korea/PortLayer.tsx @@ -1,6 +1,4 @@ -import { useState } from 'react'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { EAST_ASIA_PORTS } from '../../data/ports'; +import { Popup } from 'react-map-gl/maplibre'; import type { Port } from '../../data/ports'; const COUNTRY_STYLE: Record = { @@ -15,87 +13,51 @@ function getStyle(p: Port) { return COUNTRY_STYLE[p.country] || COUNTRY_STYLE.KR; } -function AnchorIcon({ color, size = 14 }: { color: string; size?: number }) { - return ( - - - - - - - ); +interface Props { + selected: Port | null; + onClose: () => void; } -export function PortLayer() { - const [selected, setSelected] = useState(null); - +export function PortLayer({ selected, onClose }: Props) { + if (!selected) return null; + const s = getStyle(selected); return ( - <> - {EAST_ASIA_PORTS.map(p => { - const s = getStyle(p); - const size = p.type === 'major' ? 16 : 12; - return ( - { e.originalEvent.stopPropagation(); setSelected(p); }}> -
- -
- {p.nameKo.replace('항', '')} -
-
-
- ); - })} - - {selected && (() => { - const s = getStyle(selected); - return ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="280px" className="gl-popup"> -
-
- {s.flag} - ⚓ {selected.nameKo} -
-
- - {selected.type === 'major' ? '주요항만' : '항만'} - - - {s.label} - -
-
-
항구 : {selected.nameKo}
-
영문 : {selected.name}
-
-
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
- -
-
- ); - })()} - + +
+
+ {s.flag} + ⚓ {selected.nameKo} +
+
+ + {selected.type === 'major' ? '주요항만' : '항만'} + + + {s.label} + +
+
+
항구 : {selected.nameKo}
+
영문 : {selected.name}
+
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+ +
+
); } diff --git a/frontend/src/components/korea/SubmarineCableLayer.tsx b/frontend/src/components/korea/SubmarineCableLayer.tsx index 0c9fed2..1bb58b8 100644 --- a/frontend/src/components/korea/SubmarineCableLayer.tsx +++ b/frontend/src/components/korea/SubmarineCableLayer.tsx @@ -7,6 +7,26 @@ export function SubmarineCableLayer() { const [selectedCable, setSelectedCable] = useState(null); const [selectedPoint, setSelectedPoint] = useState<{ name: string; lat: number; lng: number; cables: string[] } | null>(null); + // 날짜변경선(180도) 보정: 연속 좌표가 180도를 넘으면 경도를 연속으로 만듦 + // 예: [170, lat] → [-170, lat] 를 [170, lat] → [190, lat] 로 변환 + function fixDateline(route: number[][]): number[][] { + const fixed: number[][] = []; + for (let i = 0; i < route.length; i++) { + const [lng, lat] = route[i]; + if (i === 0) { + fixed.push([lng, lat]); + continue; + } + const prevLng = fixed[i - 1][0]; + let newLng = lng; + // 이전 경도와 180도 이상 차이나면 보정 + while (newLng - prevLng > 180) newLng -= 360; + while (prevLng - newLng > 180) newLng += 360; + fixed.push([newLng, lat]); + } + return fixed; + } + // Build GeoJSON for all cables const geojson: GeoJSON.FeatureCollection = { type: 'FeatureCollection', @@ -19,7 +39,7 @@ export function SubmarineCableLayer() { }, geometry: { type: 'LineString' as const, - coordinates: cable.route, + coordinates: fixDateline(cable.route), }, })), }; diff --git a/frontend/src/components/korea/WindFarmLayer.tsx b/frontend/src/components/korea/WindFarmLayer.tsx index b40bbef..e83b55a 100644 --- a/frontend/src/components/korea/WindFarmLayer.tsx +++ b/frontend/src/components/korea/WindFarmLayer.tsx @@ -1,27 +1,7 @@ -import { useState } from 'react'; -import { Marker, Popup } from 'react-map-gl/maplibre'; -import { KOREA_WIND_FARMS } from '../../data/windFarms'; +import { Popup } from 'react-map-gl/maplibre'; import type { WindFarm } from '../../data/windFarms'; -const COLOR = '#0891b2'; - -export function WindTurbineIcon({ size = 20, color }: { size?: number; color?: string }) { - const c = color || COLOR; - return ( - - - - - - - - - - - - - ); -} +const COLOR = '#00bcd4'; const STATUS_COLOR: Record = { '운영중': '#22c55e', @@ -29,85 +9,52 @@ const STATUS_COLOR: Record = { '계획': '#64748b', }; -export function WindFarmLayer() { - const [selected, setSelected] = useState(null); +interface Props { + selected: WindFarm | null; + onClose: () => void; +} +export function WindFarmLayer({ selected, onClose }: Props) { + if (!selected) return null; return ( - <> - {KOREA_WIND_FARMS.map(wf => ( - { e.originalEvent.stopPropagation(); setSelected(wf); }}> -
- -
- {wf.name.length > 10 ? wf.name.slice(0, 10) + '..' : wf.name} -
-
-
- ))} - - {selected && ( - setSelected(null)} closeOnClick={false} - anchor="bottom" maxWidth="280px" className="gl-popup"> -
- {/* Header - full width */} -
- - {selected.name} -
- - {/* Body */} -
- {/* Tags */} -
- - {selected.status} - - - 해상풍력 - - - {selected.region} - -
- - {/* Info grid */} -
-
용량 {selected.capacityMW} MW
-
터빈 {selected.turbines}기
- {selected.year &&
준공 {selected.year}년
} -
지역 {selected.region}
-
- - {/* Coordinates */} -
- {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E -
-
-
-
- )} - + +
+
+ 🌀 + {selected.name} +
+
+ + {selected.status} + + + 해상풍력 + + + {selected.region} + +
+
+
용량 : {selected.capacityMW} MW
+
터빈 : {selected.turbines}기
+ {selected.year &&
준공 : {selected.year}년
} +
지역 : {selected.region}
+
+
+ {selected.lat.toFixed(4)}°N, {selected.lng.toFixed(4)}°E +
+
+
); } diff --git a/frontend/src/components/layers/DeckGLOverlay.tsx b/frontend/src/components/layers/DeckGLOverlay.tsx new file mode 100644 index 0000000..9e787db --- /dev/null +++ b/frontend/src/components/layers/DeckGLOverlay.tsx @@ -0,0 +1,22 @@ +import { useControl } from 'react-map-gl/maplibre'; +import { MapboxOverlay } from '@deck.gl/mapbox'; +import type { Layer } from '@deck.gl/core'; + +interface Props { + layers: Layer[]; +} + +/** + * MapLibre Map 내부에서 deck.gl 레이어를 GPU 렌더링하는 오버레이. + * interleaved 모드: MapLibre 레이어와 deck.gl 레이어가 z-order로 혼합됨. + */ +export function DeckGLOverlay({ layers }: Props) { + const overlay = useControl( + () => new MapboxOverlay({ + interleaved: true, + getCursor: ({ isHovering }) => isHovering ? 'pointer' : '', + }), + ); + overlay.setProps({ layers }); + return null; +} diff --git a/frontend/src/components/layers/ShipLayer.tsx b/frontend/src/components/layers/ShipLayer.tsx index 4d27400..dd40192 100644 --- a/frontend/src/components/layers/ShipLayer.tsx +++ b/frontend/src/components/layers/ShipLayer.tsx @@ -1,10 +1,8 @@ import { memo, useMemo, useState, useEffect, useRef, useCallback } from 'react'; import { Marker, Popup, Source, Layer, useMap } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; -import type { Ship, ShipCategory } from '../../types'; +import type { Ship, ShipCategory, VesselAnalysisDto } from '../../types'; import maplibregl from 'maplibre-gl'; -import { detectFleet } from '../../utils/fleetDetection'; -import type { FleetConnection } from '../../utils/fleetDetection'; interface Props { ships: Ship[]; @@ -13,6 +11,7 @@ interface Props { hoveredMmsi?: string | null; focusMmsi?: string | null; onFocusClear?: () => void; + analysisMap?: Map; } // ── MarineTraffic-style vessel type colors (CSS variable references) ── @@ -362,7 +361,7 @@ function ensureTriangleImage(map: maplibregl.Map) { } // ── Main layer (WebGL symbol rendering — triangles) ── -export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusMmsi, onFocusClear }: Props) { +export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusMmsi, onFocusClear, analysisMap }: Props) { const { current: map } = useMap(); const [selectedMmsi, setSelectedMmsi] = useState(null); const [imageReady, setImageReady] = useState(false); @@ -465,35 +464,48 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM const selectedShip = selectedMmsi ? filtered.find(s => s.mmsi === selectedMmsi) ?? null : null; - // 선단 탐지 (중국어선 선택 시 — 성능 최적화: 근처 선박만 전달) - const fleet: FleetConnection | null = useMemo(() => { - if (!selectedShip || selectedShip.flag !== 'CN') return null; - // 0.2도(~12NM) 이내 선박만 필터링하여 전달 - const nearby = ships.filter(s => - Math.abs(s.lat - selectedShip.lat) < 0.2 && - Math.abs(s.lng - selectedShip.lng) < 0.2 - ); - return detectFleet(selectedShip, nearby); - }, [selectedShip, ships]); + // Python 분석 결과 기반 선단 그룹 (cluster_id로 그룹핑) + const selectedFleetMembers = useMemo(() => { + if (!selectedMmsi || !analysisMap) return []; + const dto = analysisMap.get(selectedMmsi); + if (!dto) return []; + const clusterId = dto.algorithms.cluster.clusterId; + if (clusterId < 0) return []; - // 선단 연결선 GeoJSON + // 같은 cluster_id를 가진 모든 선박 + const members: { ship: Ship; role: string; roleKo: string }[] = []; + for (const [mmsi, d] of analysisMap) { + if (d.algorithms.cluster.clusterId !== clusterId) continue; + const ship = ships.find(s => s.mmsi === mmsi); + if (!ship) continue; + const isLeader = d.algorithms.fleetRole.isLeader; + members.push({ + ship, + role: d.algorithms.fleetRole.role, + roleKo: isLeader ? '본선' : '선단원', + }); + } + return members; + }, [selectedMmsi, analysisMap, ships]); + + // 선단 연결선 GeoJSON — 선택 선박과 같은 cluster 멤버 연결 const fleetLineGeoJson = useMemo(() => { - if (!fleet) return { type: 'FeatureCollection' as const, features: [] }; + if (selectedFleetMembers.length < 2) return { type: 'FeatureCollection' as const, features: [] }; + // 중심점 계산 + const cLat = selectedFleetMembers.reduce((s, m) => s + m.ship.lat, 0) / selectedFleetMembers.length; + const cLng = selectedFleetMembers.reduce((s, m) => s + m.ship.lng, 0) / selectedFleetMembers.length; return { type: 'FeatureCollection' as const, - features: fleet.members.map(m => ({ + features: selectedFleetMembers.map(m => ({ type: 'Feature' as const, properties: { role: m.role }, geometry: { type: 'LineString' as const, - coordinates: [ - [fleet.selectedShip.lng, fleet.selectedShip.lat], - [m.ship.lng, m.ship.lat], - ], + coordinates: [[cLng, cLat], [m.ship.lng, m.ship.lat]], }, })), }; - }, [fleet]); + }, [selectedFleetMembers]); // Carrier labels — only a few, so DOM markers are fine const carriers = useMemo(() => filtered.filter(s => s.category === 'carrier'), [filtered]); @@ -514,16 +526,15 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM return ( <> - {/* Hovered ship highlight ring */} + {/* Hovered ship highlight ring — feature-state는 paint에서만 지원 */} @@ -548,7 +559,7 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM layout={{ 'visibility': highlightKorean ? 'visible' : 'none', 'text-field': ['get', 'name'], - 'text-size': 9, + 'text-size': ['interpolate', ['linear'], ['zoom'], 4, 8, 6, 9, 8, 11, 10, 14, 12, 16, 13, 18, 14, 20], 'text-offset': [0, 2.2], 'text-anchor': 'top', 'text-allow-overlap': false, @@ -566,7 +577,15 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM type="symbol" layout={{ 'icon-image': 'ship-triangle', - 'icon-size': ['get', 'size'], + 'icon-size': ['interpolate', ['linear'], ['zoom'], + 4, ['*', ['get', 'size'], 0.8], + 6, ['*', ['get', 'size'], 1.0], + 8, ['*', ['get', 'size'], 1.5], + 10, ['*', ['get', 'size'], 2.2], + 12, ['*', ['get', 'size'], 2.8], + 13, ['*', ['get', 'size'], 3.5], + 14, ['*', ['get', 'size'], 4.2], + ], 'icon-rotate': ['get', 'heading'], 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, @@ -598,8 +617,8 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM ))} - {/* Fleet connection lines — 중국어선 클릭 시만 */} - {fleet && fleetLineGeoJson.features.length > 0 && selectedShip?.flag === 'CN' && ( + {/* Fleet connection lines — Python cluster 기반, 선박 클릭 시 */} + {selectedFleetMembers.length > 1 && fleetLineGeoJson.features.length > 0 && ( )} - {/* Fleet member markers — 중국어선 클릭 시만 */} - {fleet && selectedShip?.flag === 'CN' && fleet.members.map(m => ( + {/* Fleet member markers — Python cluster 기반 */} + {selectedFleetMembers.length > 1 && selectedFleetMembers.map(m => (
- {m.role === 'pair' ? 'PT' : m.role === 'carrier' ? 'FC' : m.role === 'lighting' ? '灯' : '●'} + {m.role === 'LEADER' ? 'L' : '●'}
- {m.roleKo} {m.distanceNm.toFixed(1)}NM + {m.roleKo}
))} {/* Popup for selected ship */} {selectedShip && ( - setSelectedMmsi(null)} fleet={fleet} /> + setSelectedMmsi(null)} fleetGroup={null} /> )} ); } -const ShipPopup = memo(function ShipPopup({ ship, onClose, fleet }: { ship: Ship; onClose: () => void; fleet?: FleetConnection | null }) { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const ShipPopup = memo(function ShipPopup({ ship, onClose, fleetGroup }: { ship: Ship; onClose: () => void; fleetGroup?: any }) { const { t } = useTranslation('ships'); const mtType = getMTType(ship); const color = MT_TYPE_COLORS[mtType] || MT_TYPE_COLORS.unknown; @@ -808,21 +828,20 @@ const ShipPopup = memo(function ShipPopup({ ship, onClose, fleet }: { ship: Ship
)} - {/* Fleet info (중국어선만) */} - {fleet && fleet.members.length > 0 && ( + {/* Fleet info (선단 그룹 소속 시) */} + {fleetGroup && fleetGroup.members.length > 0 && (
- 🔗 {fleet.fleetTypeKo} — {fleet.members.length}척 연결 + 🔗 {fleetGroup.fleetTypeKo} — {fleetGroup.members.length}척 연결
- {fleet.members.slice(0, 5).map(m => ( + {fleetGroup.members.slice(0, 5).map(m => (
{m.roleKo} {m.ship.name || m.ship.mmsi} - {m.distanceNm.toFixed(1)}NM
))} - {fleet.members.length > 5 && ( -
...외 {fleet.members.length - 5}척
+ {fleetGroup.members.length > 5 && ( +
...외 {fleetGroup.members.length - 5}척
)}
)} diff --git a/frontend/src/data/zones/fishing-zones-wgs84.json b/frontend/src/data/zones/fishing-zones-wgs84.json new file mode 100644 index 0000000..639cbed --- /dev/null +++ b/frontend/src/data/zones/fishing-zones-wgs84.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "features": [{"type": "Feature", "properties": {"id": "ZONE_I", "name": "수역Ⅰ(동해)"}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[131.265, 36.1666], [130.7116, 35.705], [130.656608, 35.65], [129.716194, 35.65], [129.719062, 35.663246], [129.721805, 35.679265], [129.72751, 35.691992], [129.735849, 35.718435], [129.738702, 35.732259], [129.744407, 35.74367], [129.75121, 35.7632], [129.756805, 35.779329], [129.765363, 35.795567], [129.775019, 35.819486], [129.780614, 35.843624], [129.783577, 35.874565], [129.783577, 35.892998], [129.78632, 35.901007], [129.800254, 35.920537], [129.818467, 35.95839], [129.829768, 35.991635], [129.829768, 35.993062], [129.832292, 36.004911], [129.831743, 36.023564], [129.832292, 36.04441], [129.830646, 36.068], [129.826587, 36.090163], [129.818138, 36.11101], [129.808044, 36.139866], [129.794439, 36.168393], [129.781163, 36.186168], [129.767448, 36.202187], [129.745175, 36.223253], [129.720817, 36.241686], [129.702055, 36.253645], [129.67671, 36.266702], [129.673723, 36.268056], [129.660252, 36.274162], [129.643794, 36.279539], [129.629641, 36.283708], [129.629421, 36.290511], [129.648732, 36.315527], [129.659813, 36.336702], [129.665519, 36.347784], [129.675394, 36.37291], [129.682306, 36.396938], [129.685049, 36.424258], [129.686475, 36.440168], [129.690644, 36.453883], [129.699093, 36.483397], [129.701946, 36.514118], [129.70041, 36.540232], [129.691742, 36.570843], [129.687572, 36.586862], [129.691742, 36.59937], [129.707212, 36.630092], [129.71972, 36.66191], [129.724109, 36.689121], [129.725425, 36.711723], [129.726742, 36.743541], [129.728168, 36.786771], [129.72367, 36.819467], [129.716648, 36.838668], [129.706773, 36.857978], [129.695362, 36.881787], [129.681209, 36.910095], [129.67287, 36.925785], [129.671334, 36.936098], [129.671334, 36.954092], [129.667055, 36.97439], [129.668371, 36.987996], [129.668371, 37.001491], [129.671114, 37.009501], [129.67671, 37.029908], [129.679563, 37.0682], [129.675174, 37.101006], [129.666726, 37.128107], [129.658279, 37.145001], [129.658277, 37.145004], [129.649719, 37.158499], [129.628105, 37.184393], [129.618559, 37.193171], [129.621302, 37.208751], [129.622289, 37.233218], [129.622289, 37.256149], [129.617571, 37.279959], [129.611976, 37.296965], [129.603747, 37.313971], [129.595298, 37.33021], [129.575988, 37.351605], [129.561176, 37.367953], [129.54801, 37.379035], [129.538793, 37.38869], [129.524859, 37.403393], [129.499843, 37.427092], [129.49326, 37.44333], [129.476802, 37.474381], [129.461112, 37.495886], [129.438729, 37.516403], [129.424795, 37.528253], [129.408995, 37.551843], [129.395061, 37.571153], [129.379371, 37.583661], [129.36818, 37.593207], [129.367851, 37.595511], [129.363462, 37.626562], [129.349637, 37.653772], [129.338446, 37.674399], [129.325499, 37.69349], [129.307834, 37.710497], [129.303226, 37.726625], [129.296752, 37.743632], [129.282818, 37.767112], [129.267128, 37.788397], [129.249573, 37.806172], [129.235529, 37.819338], [129.219839, 37.828994], [129.214244, 37.831846], [129.203052, 37.851705], [129.19592, 37.858344], [129.176171, 37.876721], [129.159713, 37.889888], [129.139196, 37.901628], [129.116923, 37.931032], [129.095527, 37.950892], [129.079838, 37.962522], [129.069743, 37.971299], [129.061185, 37.991049], [129.048238, 38.008055], [129.035401, 38.025062], [129.021357, 38.041081], [128.999304, 38.053479], [128.993598, 38.067962], [128.977908, 38.087053], [128.970448, 38.096379], [128.956733, 38.121395], [128.93808, 38.143339], [128.920416, 38.160236], [128.913174, 38.166051], [128.899788, 38.17417], [128.893534, 38.182838], [128.881356, 38.198967], [128.865666, 38.216522], [128.857437, 38.250864], [128.85595, 38.2544], [129.413676, 38.2544], [129.99651, 38.2544], [130.164272, 38.002769], [131.6666, 38.002784], [131.6666, 37.425], [131.632657, 37.326115], [131.632455, 37.325711], [131.631139, 37.323188], [131.629712, 37.320445], [131.628396, 37.317812], [131.628286, 37.317373], [131.627737, 37.316276], [131.626311, 37.313533], [131.625324, 37.310899], [131.624007, 37.308156], [131.6228, 37.305523], [131.621922, 37.30278], [131.620825, 37.300037], [131.619838, 37.297294], [131.61874, 37.294551], [131.617972, 37.291589], [131.617204, 37.288956], [131.616217, 37.286103], [131.615559, 37.28336], [131.6149, 37.280507], [131.614242, 37.277764], [131.613803, 37.274911], [131.613254, 37.272059], [131.612706, 37.269316], [131.612157, 37.266463], [131.612145, 37.266296], [131.5666, 37.1333], [131.427341, 37.040556], [131.1666, 36.8666], [130.375, 36.8666], [130.375, 36.1666], [131.265, 36.1666]], [[130.54053, 37.533959], [130.54042, 37.532532], [130.54031, 37.530996], [130.54031, 37.529679], [130.540091, 37.528143], [130.539871, 37.5254], [130.539871, 37.523974], [130.539871, 37.522438], [130.539762, 37.521231], [130.539762, 37.519695], [130.539762, 37.518159], [130.539762, 37.516842], [130.539762, 37.515416], [130.539871, 37.51388], [130.539871, 37.512563], [130.539871, 37.511027], [130.540091, 37.508284], [130.54031, 37.506858], [130.54031, 37.505432], [130.54042, 37.504005], [130.54053, 37.502579], [130.540859, 37.501152], [130.540968, 37.499726], [130.541188, 37.496983], [130.541627, 37.49413], [130.542175, 37.491278], [130.542614, 37.488425], [130.543053, 37.485682], [130.543821, 37.482829], [130.544479, 37.479977], [130.545138, 37.477124], [130.545796, 37.474381], [130.546674, 37.471638], [130.547552, 37.468895], [130.548429, 37.466152], [130.549307, 37.463409], [130.550404, 37.460556], [130.550514, 37.460337], [130.550733, 37.458691], [130.551172, 37.455838], [130.551721, 37.452986], [130.552489, 37.450133], [130.552928, 37.44739], [130.553696, 37.444537], [130.554464, 37.441904], [130.555122, 37.438942], [130.556, 37.436199], [130.556768, 37.433456], [130.556987, 37.433017], [130.557865, 37.430713], [130.558633, 37.42797], [130.55973, 37.425227], [130.560828, 37.422484], [130.561925, 37.419741], [130.563132, 37.417108], [130.564229, 37.414365], [130.565546, 37.411731], [130.566862, 37.409098], [130.568289, 37.406355], [130.569605, 37.403722], [130.570922, 37.401089], [130.572568, 37.398565], [130.574104, 37.396041], [130.575749, 37.393408], [130.577505, 37.390885], [130.579041, 37.388251], [130.580797, 37.385838], [130.582442, 37.383314], [130.584308, 37.3809], [130.586283, 37.378377], [130.588148, 37.376073], [130.590123, 37.373549], [130.591878, 37.371245], [130.593963, 37.368831], [130.596267, 37.366527], [130.598242, 37.364223], [130.600436, 37.361809], [130.602631, 37.359615], [130.604715, 37.35742], [130.607239, 37.355116], [130.609324, 37.353032], [130.611847, 37.350837], [130.614151, 37.348643], [130.614919, 37.347984], [130.615139, 37.347765], [130.617443, 37.34568], [130.619637, 37.343376], [130.622051, 37.341182], [130.624684, 37.339207], [130.626988, 37.337013], [130.629402, 37.335038], [130.631816, 37.332953], [130.63434, 37.330868], [130.637083, 37.328893], [130.639606, 37.326918], [130.642349, 37.325053], [130.644982, 37.323188], [130.647725, 37.321213], [130.650468, 37.319348], [130.653211, 37.317592], [130.656064, 37.315837], [130.658807, 37.314081], [130.661879, 37.312435], [130.664622, 37.31079], [130.667475, 37.308924], [130.670657, 37.307279], [130.673509, 37.305852], [130.676472, 37.304316], [130.679544, 37.30278], [130.682506, 37.301244], [130.685798, 37.299927], [130.68887, 37.298501], [130.691942, 37.297075], [130.695234, 37.295758], [130.698525, 37.294332], [130.701707, 37.293125], [130.704779, 37.291918], [130.708071, 37.290821], [130.710485, 37.289943], [130.712569, 37.289175], [130.715641, 37.287749], [130.718933, 37.286432], [130.722115, 37.285115], [130.725406, 37.284018], [130.728479, 37.282811], [130.73188, 37.281604], [130.735171, 37.280507], [130.738463, 37.27941], [130.741864, 37.278422], [130.745266, 37.277325], [130.748667, 37.276448], [130.752068, 37.27546], [130.75536, 37.274692], [130.758761, 37.273705], [130.762162, 37.273046], [130.763369, 37.272498], [130.766661, 37.271071], [130.769733, 37.269974], [130.773134, 37.268877], [130.776316, 37.26767], [130.779717, 37.266573], [130.783009, 37.265476], [130.78641, 37.264378], [130.789702, 37.263391], [130.793103, 37.262513], [130.796505, 37.261635], [130.799906, 37.260648], [130.803307, 37.25977], [130.806818, 37.259002], [130.810219, 37.258124], [130.81373, 37.257466], [130.817022, 37.256808], [130.820752, 37.256149], [130.824263, 37.255711], [130.827665, 37.255162], [130.831176, 37.254613], [130.834687, 37.254065], [130.838308, 37.253626], [130.841819, 37.253187], [130.84533, 37.252748], [130.847195, 37.252638], [130.848841, 37.252529], [130.852461, 37.25209], [130.854217, 37.25209], [130.855972, 37.25198], [130.857947, 37.25187], [130.859703, 37.251651], [130.861458, 37.251651], [130.863324, 37.251541], [130.864969, 37.251541], [130.866835, 37.251432], [130.86848, 37.251432], [130.870346, 37.251322], [130.872211, 37.251322], [130.873857, 37.251322], [130.875722, 37.251322], [130.877477, 37.251322], [130.877697, 37.251322], [130.879233, 37.251322], [130.881208, 37.251322], [130.882963, 37.251432], [130.884719, 37.251432], [130.886474, 37.251432], [130.88834, 37.251541], [130.890095, 37.251541], [130.891851, 37.251651], [130.893716, 37.25187], [130.895362, 37.25198], [130.897227, 37.25198], [130.899092, 37.25209], [130.902713, 37.252529], [130.904249, 37.252638], [130.906224, 37.252748], [130.909735, 37.253187], [130.913356, 37.253626], [130.916867, 37.254065], [130.920378, 37.254613], [130.923779, 37.255162], [130.92729, 37.255711], [130.930911, 37.256149], [130.934422, 37.256808], [130.937823, 37.257466], [130.941334, 37.258124], [130.944735, 37.259002], [130.948137, 37.25977], [130.951648, 37.260648], [130.954939, 37.261635], [130.95834, 37.262513], [130.961742, 37.263391], [130.965143, 37.264378], [130.968325, 37.265476], [130.971726, 37.266573], [130.975127, 37.26767], [130.978419, 37.268877], [130.981601, 37.269974], [130.985002, 37.271071], [130.988074, 37.272498], [130.991256, 37.273705], [130.994438, 37.275131], [130.99762, 37.276448], [131.000692, 37.277874], [131.003984, 37.27919], [131.006946, 37.280727], [131.010018, 37.282372], [131.012981, 37.283908], [131.015833, 37.285444], [131.018905, 37.28709], [131.021758, 37.288736], [131.024611, 37.290382], [131.027573, 37.292028], [131.030426, 37.293783], [131.033279, 37.295648], [131.036022, 37.297404], [131.038655, 37.299159], [131.041508, 37.301134], [131.044141, 37.303], [131.046774, 37.304975], [131.049407, 37.306949], [131.052041, 37.308924], [131.054674, 37.311009], [131.057088, 37.312984], [131.059502, 37.315069], [131.061806, 37.317263], [131.064329, 37.319238], [131.065865, 37.320664], [131.067182, 37.321542], [131.070144, 37.323188], [131.072997, 37.324943], [131.07585, 37.326589], [131.078593, 37.328345], [131.081445, 37.33021], [131.084079, 37.332075], [131.086822, 37.33394], [131.089565, 37.335806], [131.092198, 37.337781], [131.094831, 37.339756], [131.097355, 37.34173], [131.099988, 37.343815], [131.102402, 37.3459], [131.104925, 37.347765], [131.107339, 37.349959], [131.109643, 37.352154], [131.112167, 37.354238], [131.114361, 37.356433], [131.116556, 37.358627], [131.118969, 37.360931], [131.121164, 37.363235], [131.123248, 37.36554], [131.125333, 37.367734], [131.127418, 37.370148], [131.129393, 37.372452], [131.131477, 37.374756], [131.133343, 37.377279], [131.135427, 37.379584], [131.137292, 37.382107], [131.138938, 37.384521], [131.140803, 37.387044], [131.142559, 37.389568], [131.144205, 37.392092], [131.14596, 37.394615], [131.147387, 37.397248], [131.148923, 37.399772], [131.150349, 37.402295], [131.151995, 37.405038], [131.153311, 37.407562], [131.154738, 37.410305], [131.156054, 37.412938], [131.157152, 37.415681], [131.158359, 37.418314], [131.159565, 37.421057], [131.160772, 37.4238], [131.16176, 37.426434], [131.162857, 37.429177], [131.163735, 37.43192], [131.164832, 37.434663], [131.16571, 37.437515], [131.166478, 37.440258], [131.167136, 37.443111], [131.167246, 37.443989], [131.168892, 37.446293], [131.170208, 37.448926], [131.171744, 37.451559], [131.173061, 37.454083], [131.174487, 37.456716], [131.175804, 37.459459], [131.177011, 37.461983], [131.178108, 37.464726], [131.179315, 37.467469], [131.180412, 37.470212], [131.181509, 37.472955], [131.182606, 37.475588], [131.183375, 37.47855], [131.184472, 37.481184], [131.18524, 37.484036], [131.185898, 37.48667], [131.186776, 37.489522], [131.187434, 37.492265], [131.188092, 37.495008], [131.188751, 37.497971], [131.189299, 37.500714], [131.189848, 37.503566], [131.190287, 37.506309], [131.190726, 37.509272], [131.190945, 37.512015], [131.191274, 37.513551], [131.191384, 37.514867], [131.191494, 37.516294], [131.191494, 37.51783], [131.191603, 37.519146], [131.191933, 37.520683], [131.191933, 37.523425], [131.192042, 37.524852], [131.192042, 37.526278], [131.192042, 37.527595], [131.192042, 37.529131], [131.192042, 37.530557], [131.192042, 37.531984], [131.192042, 37.53341], [131.192042, 37.534836], [131.192042, 37.536263], [131.191933, 37.537799], [131.191933, 37.540542], [131.191603, 37.541858], [131.191494, 37.543394], [131.191494, 37.544821], [131.191384, 37.546137], [131.191274, 37.547673], [131.190945, 37.5491], [131.190726, 37.551843], [131.190287, 37.554695], [131.189848, 37.557438], [131.189299, 37.560401], [131.188751, 37.563144], [131.188092, 37.565997], [131.187434, 37.56874], [131.186776, 37.571702], [131.185898, 37.574335], [131.18524, 37.577188], [131.184472, 37.579821], [131.183375, 37.582674], [131.182606, 37.585307], [131.181509, 37.58816], [131.180412, 37.590793], [131.179315, 37.593536], [131.178108, 37.596169], [131.177011, 37.598912], [131.175804, 37.601546], [131.174487, 37.604289], [131.173061, 37.606922], [131.171744, 37.609555], [131.170208, 37.612188], [131.168892, 37.614822], [131.167246, 37.617455], [131.16571, 37.619869], [131.164174, 37.622502], [131.162418, 37.624916], [131.160772, 37.627439], [131.158907, 37.629853], [131.157152, 37.632486], [131.155396, 37.63479], [131.153531, 37.637314], [131.151446, 37.639618], [131.149581, 37.641922], [131.147496, 37.644446], [131.145521, 37.64664], [131.143327, 37.648944], [131.141352, 37.651248], [131.139158, 37.653552], [131.136854, 37.655747], [131.134549, 37.657941], [131.132355, 37.660136], [131.129941, 37.66233], [131.127637, 37.664415], [131.125223, 37.666499], [131.1227, 37.668584], [131.120176, 37.670778], [131.117653, 37.672753], [131.115129, 37.674728], [131.11491, 37.675057], [131.113593, 37.676155], [131.112496, 37.677142], [131.110192, 37.679337], [131.107668, 37.681421], [131.105364, 37.683506], [131.102731, 37.685481], [131.100207, 37.687456], [131.097684, 37.68954], [131.09516, 37.691406], [131.092527, 37.693381], [131.089784, 37.695356], [131.08726, 37.697221], [131.084408, 37.699086], [131.081665, 37.700841], [131.078702, 37.702597], [131.075959, 37.704243], [131.073216, 37.706108], [131.070364, 37.707863], [131.067511, 37.7094], [131.064329, 37.711045], [131.061367, 37.712691], [131.058404, 37.714117], [131.055442, 37.715544], [131.05237, 37.71708], [131.049188, 37.718506], [131.046116, 37.720042], [131.043153, 37.721359], [131.039862, 37.722785], [131.03668, 37.723992], [131.033388, 37.725199], [131.030316, 37.726516], [131.027025, 37.727723], [131.023733, 37.72882], [131.020441, 37.729917], [131.01704, 37.731014], [131.013858, 37.732111], [131.013419, 37.732221], [131.012871, 37.732441], [131.009579, 37.733538], [131.006397, 37.734745], [131.002996, 37.735842], [130.999924, 37.736939], [130.996523, 37.738036], [130.993121, 37.738914], [130.98972, 37.740011], [130.986319, 37.740999], [130.983027, 37.741767], [130.979626, 37.742754], [130.976225, 37.743522], [130.972604, 37.74429], [130.969203, 37.744949], [130.967118, 37.745387], [130.965692, 37.745607], [130.96229, 37.746375], [130.958779, 37.747033], [130.955268, 37.747582], [130.951867, 37.74813], [130.948246, 37.748679], [130.944735, 37.749008], [130.941224, 37.749557], [130.937713, 37.749776], [130.935738, 37.750105], [130.933983, 37.750215], [130.930472, 37.750435], [130.926961, 37.750764], [130.925095, 37.750873], [130.92345, 37.750983], [130.921584, 37.750983], [130.919719, 37.751203], [130.918073, 37.751312], [130.916208, 37.751312], [130.914453, 37.751422], [130.912697, 37.751422], [130.910942, 37.751422], [130.908967, 37.751422], [130.907211, 37.751422], [130.905456, 37.751532], [130.90359, 37.751532], [130.901945, 37.751422], [130.900079, 37.751422], [130.898214, 37.751422], [130.896568, 37.751422], [130.894703, 37.751312], [130.893057, 37.751312], [130.891192, 37.751203], [130.889437, 37.751203], [130.887681, 37.750983], [130.885706, 37.750873], [130.883951, 37.750764], [130.88044, 37.750435], [130.876929, 37.750215], [130.875063, 37.750105], [130.873418, 37.749776], [130.869797, 37.749557], [130.866286, 37.749008], [130.862775, 37.748679], [130.859264, 37.74813], [130.855643, 37.747582], [130.852132, 37.747033], [130.848731, 37.746375], [130.84522, 37.745607], [130.841819, 37.744949], [130.838308, 37.74429], [130.834906, 37.743522], [130.831505, 37.742754], [130.827884, 37.741767], [130.824483, 37.740999], [130.821082, 37.740011], [130.818119, 37.739682], [130.816254, 37.739463], [130.814389, 37.739353], [130.810987, 37.738914], [130.807476, 37.738585], [130.803965, 37.738146], [130.800454, 37.737597], [130.796834, 37.737049], [130.793213, 37.7365], [130.789921, 37.735842], [130.78641, 37.735184], [130.783009, 37.734525], [130.779498, 37.733648], [130.776097, 37.732989], [130.772476, 37.732111], [130.769075, 37.731343], [130.765673, 37.730356], [130.762272, 37.729368], [130.758871, 37.728491], [130.755579, 37.727613], [130.752397, 37.726516], [130.748996, 37.725419], [130.745814, 37.724102], [130.742413, 37.723114], [130.739231, 37.721908], [130.73594, 37.720701], [130.732867, 37.719274], [130.729576, 37.718177], [130.726504, 37.71686], [130.725077, 37.716312], [130.721786, 37.715434], [130.718384, 37.714666], [130.717287, 37.714337], [130.714983, 37.713788], [130.711582, 37.712911], [130.708181, 37.712033], [130.704779, 37.711045], [130.701488, 37.709948], [130.698086, 37.708851], [130.695014, 37.707863], [130.691613, 37.706766], [130.688321, 37.705559], [130.68514, 37.704243], [130.681848, 37.703036], [130.678776, 37.701939], [130.675484, 37.700512], [130.672193, 37.699196], [130.66923, 37.697769], [130.666048, 37.696453], [130.662867, 37.695026], [130.659904, 37.69349], [130.656832, 37.691954], [130.65387, 37.690528], [130.651017, 37.688882], [130.647945, 37.687346], [130.645092, 37.6857], [130.642239, 37.683835], [130.639277, 37.682189], [130.636424, 37.680543], [130.633681, 37.678678], [130.631048, 37.676813], [130.628195, 37.675057], [130.625452, 37.673083], [130.622929, 37.671327], [130.620186, 37.669352], [130.617662, 37.667377], [130.615139, 37.665402], [130.612615, 37.663317], [130.610092, 37.661233], [130.607787, 37.659368], [130.605154, 37.657173], [130.60274, 37.655089], [130.600436, 37.652894], [130.598242, 37.6507], [130.595938, 37.648396], [130.593634, 37.646201], [130.591659, 37.644007], [130.589464, 37.641703], [130.587489, 37.639508], [130.585185, 37.636985], [130.583101, 37.634681], [130.581345, 37.632267], [130.57937, 37.629853], [130.577505, 37.627439], [130.57553, 37.625025], [130.573884, 37.622612], [130.572019, 37.620198], [130.570263, 37.617674], [130.568618, 37.615041], [130.567191, 37.612517], [130.565546, 37.609994], [130.564119, 37.607361], [130.562583, 37.604837], [130.561047, 37.602094], [130.55973, 37.59968], [130.558414, 37.596937], [130.557207, 37.594414], [130.555781, 37.591671], [130.554574, 37.589038], [130.553367, 37.586295], [130.552489, 37.583661], [130.551392, 37.580809], [130.550404, 37.578175], [130.549307, 37.575432], [130.548539, 37.57258], [130.547661, 37.569837], [130.546893, 37.567094], [130.545906, 37.564241], [130.545248, 37.561498], [130.544589, 37.558755], [130.54415, 37.555902], [130.543492, 37.55305], [130.542943, 37.550307], [130.542943, 37.549868], [130.542614, 37.548112], [130.542175, 37.54526], [130.541627, 37.542407], [130.541188, 37.539554], [130.540859, 37.535275], [130.54053, 37.533959]]], [[[128.813313, 34.343917], [128.813549, 34.343982], [128.816182, 34.35243], [128.806308, 34.41486], [128.804991, 34.4266], [128.789301, 34.509548], [128.830226, 34.561884], [128.835822, 34.572417], [128.842734, 34.581853], [128.849866, 34.592276], [128.932814, 34.708908], [128.966388, 34.755429], [128.979115, 34.774081], [128.996012, 34.797342], [129.015762, 34.824113], [129.039571, 34.860211], [129.105731, 34.950729], [129.150716, 35.011184], [129.191532, 35.0599], [129.2352, 35.105214], [129.268555, 35.136923], [129.447873, 35.131455], [129.462867, 35.130998], [129.464184, 35.13813], [129.467805, 35.147456], [129.475485, 35.159086], [129.48909, 35.188052], [129.503244, 35.194525], [129.520141, 35.20451], [129.537147, 35.217237], [129.552069, 35.23183], [129.564797, 35.245654], [129.576317, 35.261235], [129.586192, 35.277692], [129.593872, 35.292066], [129.599029, 35.307207], [129.604515, 35.323994], [129.607368, 35.340342], [129.615267, 35.346048], [129.630738, 35.358226], [129.647854, 35.373368], [129.659155, 35.390813], [129.669688, 35.408807], [129.67693, 35.430202], [129.681428, 35.439748], [129.68834, 35.46619], [129.691193, 35.476723], [129.698435, 35.489341], [129.710833, 35.518197], [129.719391, 35.549248], [129.720708, 35.574593], [129.717745, 35.601035], [129.71204, 35.622979], [129.710723, 35.629891], [129.713576, 35.637901], [129.716194, 35.65], [130.656608, 35.65], [130.5683, 35.5616], [130.3883, 35.3033], [130.2733, 35.1166], [130.125, 35.1133], [129.712736, 35.071743], [129.6783, 35.0683], [129.5483, 35.02], [129.3766, 34.96], [129.3066, 34.905], [129.2633, 34.8733], [129.2166, 34.8433], [129.0516, 34.6716], [129.0133, 34.5433], [129.0133, 34.535], [129.0033, 34.4866], [128.99, 34.46], [128.8883, 34.3083], [128.887965, 34.307977], [128.813313, 34.343917]]]]}}, {"type": "Feature", "properties": {"id": "ZONE_II", "name": "수역Ⅱ(남해)"}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[126.000509, 32.1833], [126.000154, 33.128268], [126.000787, 33.127635], [126.00364, 33.124892], [126.006492, 33.122368], [126.007151, 33.12171], [126.007919, 33.119515], [126.008467, 33.11765], [126.009126, 33.116004], [126.009784, 33.114249], [126.010333, 33.112493], [126.01121, 33.110848], [126.011869, 33.109092], [126.012527, 33.107446], [126.013185, 33.105691], [126.014063, 33.104045], [126.014831, 33.102399], [126.015599, 33.100753], [126.016477, 33.098998], [126.017245, 33.097242], [126.018123, 33.095706], [126.019, 33.093951], [126.019878, 33.092415], [126.020756, 33.090659], [126.021634, 33.089123], [126.022511, 33.087477], [126.023609, 33.085941], [126.024596, 33.084186], [126.025474, 33.08265], [126.026461, 33.081004], [126.027559, 33.079468], [126.028546, 33.077822], [126.029533, 33.076176], [126.030631, 33.07464], [126.03107, 33.073104], [126.031508, 33.071568], [126.032057, 33.069813], [126.032386, 33.068057], [126.032935, 33.066302], [126.033483, 33.064546], [126.034032, 33.06279], [126.034581, 33.060925], [126.035239, 33.059279], [126.035787, 33.057524], [126.036446, 33.055878], [126.037104, 33.054013], [126.037653, 33.052367], [126.038311, 33.050612], [126.039079, 33.048856], [126.039847, 33.04721], [126.040505, 33.045455], [126.041383, 33.043809], [126.042041, 33.042054], [126.04281, 33.040408], [126.043687, 33.038762], [126.044455, 33.037006], [126.045223, 33.035361], [126.046211, 33.033715], [126.046979, 33.032069], [126.047966, 33.030423], [126.048954, 33.028887], [126.049722, 33.027132], [126.050819, 33.025596], [126.051697, 33.02384], [126.052684, 33.022304], [126.053672, 33.020658], [126.054769, 33.019013], [126.055647, 33.017476], [126.056744, 33.015831], [126.057841, 33.014404], [126.058938, 33.012759], [126.060035, 33.011222], [126.061133, 33.009686], [126.06234, 33.00815], [126.063546, 33.006614], [126.064644, 33.005078], [126.065851, 33.003542], [126.067057, 33.002116], [126.069471, 32.999153], [126.071885, 32.996191], [126.074628, 32.993229], [126.077152, 32.990376], [126.079895, 32.987523], [126.082528, 32.98478], [126.085381, 32.982037], [126.088233, 32.979294], [126.091196, 32.976551], [126.094158, 32.974028], [126.09535, 32.973041], [126.09734, 32.971394], [126.100302, 32.968871], [126.103594, 32.966567], [126.106776, 32.964043], [126.107105, 32.963824], [126.107434, 32.963495], [126.110616, 32.961081], [126.113798, 32.958667], [126.117199, 32.956363], [126.1206, 32.954059], [126.124002, 32.951974], [126.127293, 32.94978], [126.130804, 32.947585], [126.134535, 32.945501], [126.138046, 32.943526], [126.139801, 32.942538], [126.141667, 32.941551], [126.143422, 32.940673], [126.145287, 32.939686], [126.147043, 32.938808], [126.149018, 32.93782], [126.150883, 32.937052], [126.152748, 32.936065], [126.154613, 32.935187], [126.156588, 32.934419], [126.158454, 32.933432], [126.160319, 32.932663], [126.162184, 32.931895], [126.164269, 32.931018], [126.166134, 32.93025], [126.167999, 32.929482], [126.170084, 32.928823], [126.171949, 32.927946], [126.174034, 32.927287], [126.176009, 32.926629], [126.177874, 32.925971], [126.179959, 32.925203], [126.181933, 32.924544], [126.184018, 32.923886], [126.185993, 32.923337], [126.188078, 32.922679], [126.190053, 32.922021], [126.192137, 32.921472], [126.194112, 32.920924], [126.196197, 32.920375], [126.198172, 32.919826], [126.200366, 32.919168], [126.202341, 32.918729], [126.204426, 32.918181], [126.206401, 32.917742], [126.208595, 32.917303], [126.21068, 32.916974], [126.212765, 32.916425], [126.214849, 32.915986], [126.217044, 32.915547], [126.219019, 32.915218], [126.221213, 32.914889], [126.223188, 32.91456], [126.225382, 32.914231], [126.227577, 32.913902], [126.229552, 32.913572], [126.231746, 32.913243], [126.23394, 32.913024], [126.236025, 32.912695], [126.238219, 32.912475], [126.240194, 32.912365], [126.242389, 32.912146], [126.244583, 32.911927], [126.246778, 32.911817], [126.248752, 32.911597], [126.251057, 32.911488], [126.253141, 32.911378], [126.255226, 32.911268], [126.25742, 32.911159], [126.259615, 32.911049], [126.261699, 32.911049], [126.263894, 32.911049], [126.266088, 32.911049], [126.268283, 32.910939], [126.270367, 32.910939], [126.272452, 32.911049], [126.274646, 32.911049], [126.276731, 32.911049], [126.278925, 32.911159], [126.28112, 32.911268], [126.283204, 32.911378], [126.285399, 32.911488], [126.287593, 32.911597], [126.289787, 32.911817], [126.291762, 32.911927], [126.293957, 32.912146], [126.296041, 32.912256], [126.298236, 32.912475], [126.30043, 32.912695], [126.302405, 32.913024], [126.3046, 32.913243], [126.306794, 32.913572], [126.308988, 32.913902], [126.310963, 32.914231], [126.313158, 32.91456], [126.315242, 32.914889], [126.317327, 32.915218], [126.319521, 32.915547], [126.321496, 32.915986], [126.323691, 32.916425], [126.325666, 32.916974], [126.32786, 32.917303], [126.329945, 32.917742], [126.33192, 32.918181], [126.334114, 32.918729], [126.336089, 32.919168], [126.338174, 32.919826], [126.340149, 32.920375], [126.342233, 32.920924], [126.344318, 32.921472], [126.346403, 32.922021], [126.348378, 32.922679], [126.350462, 32.923337], [126.352437, 32.923886], [126.354412, 32.924544], [126.356387, 32.925203], [126.358472, 32.925971], [126.360447, 32.926629], [126.362312, 32.927287], [126.364397, 32.927946], [126.366371, 32.928823], [126.368346, 32.929482], [126.370212, 32.93025], [126.372187, 32.931018], [126.374162, 32.931895], [126.376027, 32.932663], [126.377892, 32.933432], [126.379977, 32.934419], [126.381732, 32.935187], [126.383597, 32.936065], [126.385463, 32.937052], [126.387328, 32.93782], [126.389303, 32.938808], [126.391058, 32.939686], [126.392924, 32.940673], [126.394789, 32.941551], [126.396544, 32.942538], [126.39841, 32.943526], [126.401921, 32.945501], [126.405541, 32.947585], [126.409052, 32.94978], [126.412344, 32.951974], [126.415855, 32.954059], [126.419146, 32.956363], [126.422548, 32.958667], [126.42573, 32.961081], [126.429021, 32.963495], [126.432093, 32.965908], [126.435275, 32.968432], [126.438347, 32.971065], [126.4412, 32.973698], [126.444162, 32.976332], [126.447125, 32.979075], [126.449868, 32.981818], [126.452721, 32.984561], [126.455244, 32.987413], [126.457987, 32.990266], [126.460511, 32.993229], [126.463144, 32.996191], [126.465558, 32.999153], [126.467862, 33.002116], [126.469069, 33.003652], [126.470166, 33.005188], [126.471373, 33.006724], [126.47247, 33.00826], [126.473457, 33.009796], [126.474555, 33.011332], [126.475652, 33.012868], [126.476749, 33.014514], [126.478614, 33.017476], [126.480699, 33.017257], [126.482784, 33.016928], [126.484978, 33.016708], [126.487063, 33.016489], [126.489147, 33.01616], [126.491342, 33.01594], [126.493536, 33.015831], [126.495511, 33.015611], [126.497815, 33.015502], [126.4999, 33.015282], [126.501984, 33.015172], [126.504179, 33.015063], [126.506373, 33.015063], [126.508348, 33.014953], [126.510652, 33.014953], [126.512737, 33.014843], [126.514931, 33.014843], [126.517016, 33.014843], [126.51921, 33.014843], [126.521405, 33.014843], [126.523489, 33.014953], [126.525684, 33.014953], [126.527878, 33.015063], [126.530073, 33.015063], [126.532048, 33.015172], [126.534242, 33.015282], [126.536436, 33.015502], [126.538521, 33.015611], [126.540715, 33.015831], [126.54291, 33.01594], [126.544885, 33.01616], [126.547079, 33.016489], [126.549273, 33.016708], [126.551248, 33.016928], [126.553443, 33.017257], [126.555637, 33.017476], [126.557722, 33.017806], [126.559806, 33.018025], [126.561891, 33.018354], [126.563976, 33.018683], [126.56617, 33.019013], [126.568145, 33.019561], [126.57034, 33.01989], [126.572314, 33.020329], [126.574509, 33.020768], [126.576594, 33.021207], [126.578678, 33.021536], [126.580763, 33.022085], [126.582738, 33.022633], [126.584932, 33.023182], [126.586907, 33.023621], [126.588992, 33.024169], [126.590967, 33.024718], [126.593051, 33.025376], [126.595026, 33.025925], [126.595904, 33.026144], [126.596124, 33.026035], [126.598318, 33.025596], [126.600293, 33.025047], [126.602378, 33.024608], [126.604572, 33.024279], [126.606547, 33.02384], [126.608741, 33.023511], [126.610936, 33.023182], [126.612911, 33.022853], [126.615105, 33.022524], [126.61719, 33.022085], [126.619274, 33.021865], [126.621469, 33.021536], [126.623553, 33.021317], [126.625638, 33.020987], [126.627832, 33.020768], [126.629917, 33.020549], [126.632111, 33.020439], [126.634306, 33.020219], [126.636281, 33.02011], [126.638475, 33.01989], [126.64067, 33.019781], [126.642754, 33.019671], [126.644949, 33.019561], [126.647143, 33.019451], [126.649118, 33.019342], [126.651312, 33.019342], [126.653507, 33.019342], [126.655701, 33.019122], [126.657786, 33.019122], [126.65998, 33.019122], [126.662175, 33.019342], [126.664259, 33.019342], [126.666454, 33.019342], [126.668648, 33.019451], [126.670623, 33.019561], [126.672817, 33.019671], [126.675012, 33.019781], [126.677096, 33.01989], [126.679291, 33.02011], [126.681485, 33.020219], [126.68346, 33.020439], [126.685654, 33.020549], [126.687849, 33.020768], [126.689824, 33.020987], [126.692018, 33.021317], [126.694213, 33.021536], [126.696297, 33.021865], [126.698492, 33.022085], [126.700576, 33.022524], [126.702661, 33.022853], [126.704746, 33.023182], [126.70694, 33.023511], [126.708915, 33.02384], [126.711109, 33.024279], [126.713084, 33.024608], [126.715279, 33.025047], [126.717254, 33.025596], [126.719448, 33.026035], [126.721533, 33.026473], [126.723508, 33.027022], [126.725702, 33.027461], [126.727677, 33.02801], [126.729762, 33.028668], [126.731737, 33.029107], [126.733821, 33.029655], [126.735796, 33.030204], [126.737881, 33.030862], [126.739856, 33.031521], [126.74194, 33.032179], [126.743915, 33.032727], [126.746, 33.033386], [126.747865, 33.034044], [126.74995, 33.034812], [126.751925, 33.03547], [126.75379, 33.036129], [126.755875, 33.036897], [126.75774, 33.037775], [126.759715, 33.038433], [126.76169, 33.039201], [126.763555, 33.039969], [126.76542, 33.040847], [126.767505, 33.041615], [126.76937, 33.042492], [126.771235, 33.04337], [126.773101, 33.044248], [126.775076, 33.045126], [126.776831, 33.046003], [126.778696, 33.046991], [126.780562, 33.047869], [126.782317, 33.048746], [126.784292, 33.049734], [126.786048, 33.050612], [126.787913, 33.051599], [126.789668, 33.052696], [126.793179, 33.054671], [126.79669, 33.056756], [126.800201, 33.05895], [126.803603, 33.061035], [126.807004, 33.063339], [126.810295, 33.065643], [126.813587, 33.067947], [126.816879, 33.070361], [126.82006, 33.072775], [126.823023, 33.075189], [126.826205, 33.077712], [126.829167, 33.080346], [126.832239, 33.082979], [126.835092, 33.085722], [126.837945, 33.088245], [126.840797, 33.090988], [126.843431, 33.093841], [126.845076, 33.095597], [126.848039, 33.096694], [126.850124, 33.097462], [126.851989, 33.09834], [126.853854, 33.099108], [126.855939, 33.099876], [126.857804, 33.100863], [126.859669, 33.101631], [126.861534, 33.102509], [126.863509, 33.103387], [126.865375, 33.104374], [126.86713, 33.105252], [126.868995, 33.10613], [126.870751, 33.107117], [126.872726, 33.108105], [126.874481, 33.108982], [126.87514, 33.109311], [126.876895, 33.109531], [126.87898, 33.10997], [126.881064, 33.110299], [126.883259, 33.110628], [126.885234, 33.110957], [126.887428, 33.111396], [126.889403, 33.111725], [126.891597, 33.112164], [126.893792, 33.112603], [126.895767, 33.113152], [126.897851, 33.11359], [126.899826, 33.114029], [126.902021, 33.114468], [126.903996, 33.114907], [126.90608, 33.115456], [126.908055, 33.116114], [126.91025, 33.116663], [126.912334, 33.117211], [126.914309, 33.11776], [126.916394, 33.118308], [126.918369, 33.118857], [126.920454, 33.119625], [126.922429, 33.120283], [126.924294, 33.120942], [126.926378, 33.12149], [126.928353, 33.122368], [126.930328, 33.123026], [126.932303, 33.123685], [126.934388, 33.124343], [126.936253, 33.125221], [126.938118, 33.125989], [126.940203, 33.126757], [126.942068, 33.127525], [126.943933, 33.128403], [126.946018, 33.129171], [126.947883, 33.130048], [126.949749, 33.130816], [126.951614, 33.131804], [126.953589, 33.132572], [126.955344, 33.13345], [126.95721, 33.134437], [126.959075, 33.135315], [126.96083, 33.136302], [126.962695, 33.13718], [126.964451, 33.138168], [126.966426, 33.139155], [126.968181, 33.140143], [126.971692, 33.142227], [126.975203, 33.144312], [126.978714, 33.146287], [126.982116, 33.148591], [126.985407, 33.150785], [126.988809, 33.153089], [126.9921, 33.155394], [126.995282, 33.157807], [126.998574, 33.160221], [127.001646, 33.162745], [127.004828, 33.165268], [127.004937, 33.165488], [127.005815, 33.166036], [127.008997, 33.16834], [127.012179, 33.170864], [127.01547, 33.173278], [127.018433, 33.175911], [127.021505, 33.178435], [127.024358, 33.181068], [127.02732, 33.183701], [127.030173, 33.186444], [127.033025, 33.189187], [127.035768, 33.19204], [127.038402, 33.194783], [127.041035, 33.197745], [127.043449, 33.200598], [127.046082, 33.20356], [127.048496, 33.206523], [127.049703, 33.208059], [127.0508, 33.209595], [127.052007, 33.211021], [127.053104, 33.212667], [127.054311, 33.214203], [127.055408, 33.215739], [127.056396, 33.217275], [127.057493, 33.218921], [127.05848, 33.220457], [127.059578, 33.221993], [127.060675, 33.223529], [127.061552, 33.225285], [127.06254, 33.226821], [127.063637, 33.228467], [127.064515, 33.230003], [127.065502, 33.231648], [127.06649, 33.233294], [127.067258, 33.23494], [127.068245, 33.236586], [127.069013, 33.238232], [127.069781, 33.239877], [127.070769, 33.241633], [127.071537, 33.243279], [127.072415, 33.244924], [127.073073, 33.24657], [127.07417, 33.247448], [127.077242, 33.249862], [127.080424, 33.252276], [127.083387, 33.254909], [127.086459, 33.257542], [127.089421, 33.260066], [127.092274, 33.262699], [127.095127, 33.265442], [127.09787, 33.268185], [127.100503, 33.271038], [127.103246, 33.27389], [127.105769, 33.276743], [127.108403, 33.279705], [127.110816, 33.282558], [127.113121, 33.285521], [127.114327, 33.287166], [127.115534, 33.288593], [127.116632, 33.290238], [127.117838, 33.291665], [127.118936, 33.293311], [127.120033, 33.294847], [127.12113, 33.296383], [127.122117, 33.297919], [127.123105, 33.299565], [127.124202, 33.301101], [127.12508, 33.302746], [127.126177, 33.304283], [127.127165, 33.305928], [127.128152, 33.307464], [127.12903, 33.30911], [127.131334, 33.311524], [127.133967, 33.314486], [127.136381, 33.317449], [127.138685, 33.320411], [127.139892, 33.322057], [127.141099, 33.323483], [127.142196, 33.325129], [127.143293, 33.326556], [127.1445, 33.328092], [127.145597, 33.329737], [127.146585, 33.331164], [127.147572, 33.33281], [127.14867, 33.334346], [127.149767, 33.335991], [127.150644, 33.337527], [127.151632, 33.339173], [127.152729, 33.340709], [127.153717, 33.342355], [127.154594, 33.344001], [127.155582, 33.345647], [127.15635, 33.347183], [127.157337, 33.348938], [127.158105, 33.350474], [127.159093, 33.35223], [127.159861, 33.353876], [127.160739, 33.355521], [127.161507, 33.357167], [127.162275, 33.358923], [127.163043, 33.360568], [127.163811, 33.362214], [127.164469, 33.36386], [127.165127, 33.365616], [127.165786, 33.367261], [127.166554, 33.369017], [127.167212, 33.370772], [127.16787, 33.372528], [127.168529, 33.374174], [127.169077, 33.375929], [127.169626, 33.377685], [127.170284, 33.37944], [127.170723, 33.381086], [127.171272, 33.382951], [127.17182, 33.384707], [127.172369, 33.386462], [127.173027, 33.389315], [127.173795, 33.390193], [127.174892, 33.391729], [127.17599, 33.393265], [127.177197, 33.394801], [127.178294, 33.396337], [127.179391, 33.397873], [127.180488, 33.399519], [127.181366, 33.401055], [127.182463, 33.402701], [127.183451, 33.404237], [127.184548, 33.405773], [127.185425, 33.407419], [127.186413, 33.409064], [127.187291, 33.4106], [127.188278, 33.412356], [127.189266, 33.413892], [127.190034, 33.415538], [127.190802, 33.417184], [127.191789, 33.418829], [127.192557, 33.420475], [127.193435, 33.422231], [127.194203, 33.423876], [127.195081, 33.425632], [127.195739, 33.427168], [127.196507, 33.428924], [127.197165, 33.430569], [127.198043, 33.432325], [127.198702, 33.433971], [127.19936, 33.435726], [127.200018, 33.437372], [127.200676, 33.439237], [127.201225, 33.440883], [127.201774, 33.442638], [127.202322, 33.444284], [127.202981, 33.446149], [127.203529, 33.447795], [127.204078, 33.449551], [127.204517, 33.451306], [127.205065, 33.453171], [127.205504, 33.454817], [127.205943, 33.456573], [127.206382, 33.458328], [127.206821, 33.460194], [127.20704, 33.461291], [127.207163, 33.461721], [127.207698, 33.463595], [127.208247, 33.465241], [127.208796, 33.467106], [127.209235, 33.468861], [127.209673, 33.470617], [127.210003, 33.472372], [127.210441, 33.474128], [127.21088, 33.475883], [127.211319, 33.477749], [127.211648, 33.479394], [127.211978, 33.48126], [127.212307, 33.483015], [127.212636, 33.484771], [127.212855, 33.486526], [127.213184, 33.488391], [127.213294, 33.490147], [127.213514, 33.491902], [127.213843, 33.493658], [127.213952, 33.495523], [127.214062, 33.497388], [127.214282, 33.499144], [127.214391, 33.501009], [127.214501, 33.502655], [127.214501, 33.50452], [127.214611, 33.506276], [127.214611, 33.508141], [127.21483, 33.509896], [127.21483, 33.511762], [127.21483, 33.513517], [127.21483, 33.515382], [127.214611, 33.517138], [127.214501, 33.518893], [127.214501, 33.520759], [127.214391, 33.522514], [127.214282, 33.52427], [127.214282, 33.526135], [127.213952, 33.52789], [127.213843, 33.529756], [127.213514, 33.531511], [127.213514, 33.532169], [127.213404, 33.533376], [127.210332, 33.551041], [127.204736, 33.568706], [127.197933, 33.585273], [127.189924, 33.601951], [127.178733, 33.617531], [127.156898, 33.641998], [127.132541, 33.662845], [127.129469, 33.664381], [127.1016, 33.682046], [127.082948, 33.690823], [127.063637, 33.698065], [127.043778, 33.70388], [127.021944, 33.708049], [127.000878, 33.711121], [126.9921, 33.71167], [126.977727, 33.721435], [126.948542, 33.739648], [126.918808, 33.752705], [126.904983, 33.757313], [126.922429, 33.759837], [126.939874, 33.763019], [126.957319, 33.766091], [126.964122, 33.767627], [126.978385, 33.77026], [126.995831, 33.771906], [127.012618, 33.773442], [127.029405, 33.775417], [127.04685, 33.777063], [127.064295, 33.779038], [127.081083, 33.780135], [127.098528, 33.782219], [127.115315, 33.784304], [127.132541, 33.78584], [127.149986, 33.787486], [127.156898, 33.787925], [127.174344, 33.79001], [127.191679, 33.792094], [127.208467, 33.79363], [127.22657, 33.795276], [127.242589, 33.797251], [127.260035, 33.798787], [127.265411, 33.799665], [127.273201, 33.800433], [127.321477, 33.80537], [127.369972, 33.809199], [127.413203, 33.812612], [127.622766, 33.829508], [127.653927, 33.833787], [127.709444, 33.849587], [127.749273, 33.867032], [127.785699, 33.891719], [127.835731, 33.948554], [127.897942, 34.013069], [127.996251, 34.117631], [127.999323, 34.118509], [128.0, 34.118697], [128.75, 34.326406], [128.813549, 34.343982], [128.813313, 34.343917], [128.887965, 34.307977], [128.8883, 34.3083], [128.7933, 34.2166], [128.75, 34.183619], [128.6883, 34.1366], [128.435, 33.84], [128.425, 33.79], [128.3616, 33.7516], [128.0, 33.396458], [127.922434, 33.320087], [127.8716, 33.27], [127.86, 33.2283], [127.805, 33.145], [127.6983, 32.9583], [127.685, 32.95], [127.15, 32.5666], [126.535888, 32.1833], [126.000509, 32.1833]]]]}}, {"type": "Feature", "properties": {"id": "ZONE_III", "name": "수역Ⅲ(서남해)"}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[124.125526, 35.000703], [125.18669, 35.000703], [125.185796, 35.0], [125.080023, 34.916826], [125.03065, 34.87612], [125.006621, 34.855273], [124.994223, 34.84112], [124.977326, 34.819066], [124.963172, 34.793502], [124.95615, 34.772435], [124.94079, 34.734253], [124.935194, 34.714284], [124.932232, 34.691024], [124.915664, 34.596775], [124.90173, 34.515144], [124.887686, 34.427588], [124.859268, 34.25862], [124.834143, 34.112474], [124.846651, 34.040389], [124.858171, 34.003962], [124.878689, 33.970498], [124.904582, 33.937801], [124.944959, 33.90719], [124.983361, 33.881954], [125.012985, 33.865606], [125.061152, 33.852111], [125.395356, 33.801969], [125.578823, 33.774791], [125.867039, 33.732078], [125.875597, 33.730761], [125.884265, 33.729444], [125.892823, 33.728238], [125.901381, 33.726921], [125.90994, 33.725714], [125.918607, 33.724397], [125.927165, 33.723081], [125.935724, 33.721764], [125.944282, 33.720557], [125.952949, 33.71935], [125.960959, 33.718143], [125.969078, 33.716937], [125.977636, 33.7154], [125.986304, 33.713864], [125.993436, 33.712767], [126.001994, 33.711012], [126.010662, 33.709256], [126.014283, 33.708598], [126.022841, 33.706842], [126.031508, 33.705087], [126.040067, 33.703441], [126.048625, 33.701686], [126.061242, 33.699162], [126.060803, 33.697187], [126.060584, 33.695212], [126.060255, 33.692469], [126.060035, 33.690604], [126.059816, 33.688848], [126.059706, 33.687093], [126.059487, 33.685228], [126.059487, 33.683472], [126.059158, 33.681717], [126.059158, 33.679851], [126.059048, 33.678096], [126.059048, 33.676231], [126.059048, 33.674475], [126.059048, 33.67272], [126.058938, 33.670854], [126.059048, 33.669099], [126.059048, 33.667343], [126.059048, 33.665478], [126.059158, 33.663723], [126.059158, 33.661857], [126.059377, 33.660102], [126.059487, 33.658237], [126.059706, 33.656591], [126.059816, 33.654726], [126.060035, 33.65297], [126.060255, 33.651215], [126.060365, 33.649349], [126.060694, 33.647704], [126.060913, 33.645838], [126.061242, 33.644083], [126.061462, 33.642218], [126.061901, 33.640572], [126.06212, 33.638707], [126.062559, 33.637061], [126.062998, 33.635196], [126.063327, 33.63344], [126.063766, 33.631685], [126.064205, 33.629929], [126.064753, 33.628064], [126.065083, 33.626418], [126.065631, 33.624663], [126.06618, 33.622907], [126.066728, 33.621152], [126.067277, 33.619506], [126.067825, 33.617641], [126.068374, 33.615995], [126.069032, 33.614239], [126.069691, 33.612484], [126.070239, 33.610728], [126.070898, 33.609083], [126.071666, 33.607437], [126.072324, 33.605681], [126.072982, 33.604035], [126.07375, 33.60228], [126.074628, 33.600634], [126.075286, 33.598879], [126.076054, 33.597343], [126.076932, 33.595587], [126.0777, 33.593941], [126.078688, 33.592295], [126.079456, 33.59065], [126.080333, 33.589004], [126.081211, 33.587358], [126.082199, 33.585822], [126.083076, 33.584176], [126.083625, 33.583189], [126.082418, 33.58253], [126.079017, 33.580336], [126.075725, 33.578032], [126.072324, 33.575838], [126.068923, 33.573533], [126.065631, 33.57112], [126.062449, 33.568816], [126.059377, 33.566402], [126.056195, 33.563768], [126.053123, 33.561355], [126.05027, 33.558721], [126.047308, 33.556088], [126.044346, 33.553565], [126.041493, 33.550822], [126.03875, 33.547969], [126.036117, 33.545226], [126.033374, 33.542373], [126.03074, 33.539521], [126.028327, 33.536668], [126.025913, 33.533705], [126.023499, 33.530743], [126.022292, 33.529207], [126.021195, 33.527671], [126.019988, 33.526135], [126.018891, 33.524599], [126.017684, 33.523063], [126.016587, 33.521527], [126.015489, 33.51999], [126.014392, 33.518454], [126.013514, 33.516809], [126.012417, 33.515273], [126.01143, 33.513627], [126.010333, 33.512091], [126.009455, 33.510445], [126.008467, 33.508909], [126.00748, 33.507153], [126.006712, 33.505617], [126.005724, 33.503971], [126.004847, 33.502326], [126.003969, 33.50079], [126.003091, 33.499034], [126.002213, 33.497388], [126.001445, 33.495743], [126.000677, 33.494097], [125.999909, 33.492341], [125.999032, 33.490586], [125.998154, 33.489927], [125.994972, 33.487623], [125.9919, 33.48521], [125.988608, 33.482686], [125.985536, 33.480272], [125.982354, 33.477749], [125.979392, 33.475115], [125.976539, 33.472372], [125.973577, 33.469849], [125.970834, 33.467106], [125.968091, 33.464363], [125.965348, 33.46151], [125.962824, 33.458657], [125.960081, 33.455695], [125.957667, 33.452842], [125.955034, 33.44988], [125.95273, 33.446917], [125.951523, 33.445272], [125.950426, 33.443845], [125.949219, 33.442419], [125.948122, 33.440773], [125.947134, 33.439237], [125.946037, 33.437701], [125.94494, 33.436165], [125.943843, 33.434519], [125.942746, 33.432983], [125.941868, 33.431337], [125.940771, 33.429801], [125.939783, 33.428156], [125.938796, 33.426619], [125.937918, 33.424864], [125.93693, 33.423328], [125.936053, 33.421792], [125.935065, 33.420036], [125.934297, 33.4185], [125.93331, 33.416745], [125.932432, 33.415209], [125.931664, 33.413453], [125.930896, 33.411807], [125.930018, 33.410052], [125.92925, 33.408406], [125.928592, 33.406651], [125.927714, 33.405005], [125.927056, 33.403249], [125.926397, 33.401603], [125.925739, 33.399848], [125.925081, 33.398202], [125.924422, 33.396447], [125.923654, 33.394801], [125.923106, 33.393045], [125.922448, 33.3914], [125.921899, 33.389534], [125.92135, 33.387779], [125.920911, 33.386023], [125.920363, 33.384268], [125.919814, 33.382293], [125.918607, 33.37955], [125.917839, 33.377794], [125.917181, 33.376149], [125.916523, 33.374393], [125.915864, 33.372747], [125.915206, 33.370992], [125.914548, 33.369346], [125.913999, 33.36759], [125.913451, 33.365835], [125.912792, 33.364079], [125.912244, 33.362434], [125.911695, 33.360568], [125.911146, 33.358923], [125.910708, 33.357057], [125.910159, 33.355412], [125.90961, 33.353546], [125.909281, 33.351901], [125.908842, 33.350035], [125.908403, 33.34828], [125.908184, 33.346524], [125.907745, 33.344659], [125.907306, 33.343013], [125.907087, 33.341148], [125.906758, 33.339393], [125.906429, 33.337527], [125.906099, 33.335882], [125.90599, 33.334016], [125.90566, 33.332261], [125.905441, 33.330396], [125.905331, 33.32864], [125.905002, 33.326775], [125.904892, 33.32491], [125.904783, 33.323264], [125.904673, 33.321399], [125.904673, 33.319643], [125.904454, 33.317778], [125.904454, 33.316022], [125.904344, 33.314157], [125.904344, 33.312402], [125.904344, 33.310646], [125.904344, 33.308781], [125.904454, 33.307025], [125.904454, 33.30516], [125.904454, 33.305051], [125.904673, 33.303405], [125.904673, 33.30154], [125.904783, 33.299784], [125.904892, 33.297919], [125.905002, 33.296163], [125.905331, 33.294298], [125.905441, 33.292652], [125.90566, 33.290787], [125.90599, 33.288922], [125.906099, 33.287166], [125.906429, 33.285301], [125.906648, 33.283546], [125.907087, 33.28179], [125.907306, 33.280035], [125.907745, 33.278279], [125.907965, 33.276524], [125.908403, 33.274658], [125.908842, 33.272903], [125.909391, 33.271147], [125.90972, 33.269282], [125.910159, 33.267636], [125.910708, 33.265771], [125.911146, 33.264125], [125.911695, 33.26226], [125.912244, 33.260614], [125.912792, 33.258749], [125.913451, 33.257103], [125.913999, 33.255348], [125.914548, 33.253592], [125.915206, 33.251946], [125.915864, 33.250081], [125.916523, 33.248435], [125.917181, 33.24668], [125.917839, 33.245034], [125.918607, 33.243279], [125.919375, 33.241633], [125.920034, 33.239877], [125.920911, 33.238341], [125.921679, 33.236586], [125.922448, 33.23494], [125.923325, 33.233184], [125.924203, 33.231648], [125.925081, 33.229893], [125.925849, 33.228357], [125.926836, 33.226601], [125.927714, 33.224956], [125.928702, 33.223419], [125.929689, 33.221664], [125.930567, 33.220128], [125.931554, 33.218482], [125.932651, 33.216946], [125.933529, 33.2153], [125.934626, 33.213764], [125.935724, 33.212118], [125.936821, 33.210582], [125.937918, 33.209046], [125.939015, 33.20751], [125.940112, 33.205864], [125.941319, 33.204438], [125.942307, 33.202792], [125.943623, 33.201366], [125.94483, 33.19994], [125.947244, 33.196867], [125.949658, 33.194015], [125.952181, 33.191052], [125.954815, 33.1882], [125.957448, 33.185347], [125.960191, 33.182494], [125.96063, 33.182165], [125.961837, 33.179532], [125.962824, 33.177886], [125.963702, 33.17624], [125.96458, 33.174485], [125.965457, 33.172949], [125.966445, 33.171303], [125.967323, 33.169657], [125.96831, 33.168011], [125.969298, 33.166475], [125.970395, 33.164829], [125.971273, 33.163293], [125.97237, 33.161648], [125.973467, 33.160111], [125.974564, 33.158466], [125.975661, 33.157039], [125.976649, 33.155394], [125.977746, 33.153967], [125.978953, 33.152321], [125.98005, 33.150785], [125.982464, 33.147823], [125.984768, 33.14486], [125.987401, 33.141898], [125.989925, 33.138936], [125.992558, 33.136083], [125.995191, 33.13323], [125.997934, 33.130487], [126.000154, 33.128268], [126.000509, 32.1833], [125.4166, 32.1833], [125.291527, 32.296033], [125.291527, 32.296033], [124.170389, 33.300272], [124.1333, 33.3333], [124.0083, 34.0], [124.125, 35.0], [124.125, 35.0], [124.125526, 35.000703]]]]}}, {"type": "Feature", "properties": {"id": "ZONE_IV", "name": "수역Ⅳ(서해)"}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[124.5, 35.5], [124.5, 36.149546], [124.5, 36.75], [124.3333, 37.0], [124.727778, 37.0], [124.727778, 37.00285], [125.228908, 37.002852], [125.486775, 37.002853], [125.483132, 36.996554], [125.428382, 36.902634], [125.408742, 36.867524], [125.388993, 36.834718], [125.388133, 36.833333], [125.388133, 36.833333], [125.380544, 36.821113], [125.349823, 36.764388], [125.31186, 36.697459], [125.301985, 36.671346], [125.293537, 36.640625], [125.293537, 36.629214], [125.29222, 36.616816], [125.295073, 36.587191], [125.299352, 36.573586], [125.309227, 36.545169], [125.324807, 36.515654], [125.35871, 36.471328], [125.448022, 36.366656], [125.56959, 36.226544], [125.587914, 36.206027], [125.6, 36.192052], [125.636358, 36.15], [125.73779, 36.032561], [125.758966, 35.997121], [125.791443, 35.926023], [125.809876, 35.883672], [125.822713, 35.845819], [125.832697, 35.814878], [125.838183, 35.788435], [125.848387, 35.720629], [125.848833, 35.716667], [125.851349, 35.694296], [125.852666, 35.683983], [125.848387, 35.66555], [125.81218, 35.565486], [125.785408, 35.496143], [125.775643, 35.465093], [125.768511, 35.450061], [125.746603, 35.433333], [125.686771, 35.387631], [125.501126, 35.244228], [125.392723, 35.160732], [125.37429, 35.148004], [125.261667, 35.059608], [125.229351, 35.034225], [125.185796, 35.0], [125.18669, 35.000703], [124.125526, 35.000703], [124.5, 35.5]]]]}}]} \ No newline at end of file diff --git a/frontend/src/data/zones/특정어업수역Ⅰ.json b/frontend/src/data/zones/특정어업수역Ⅰ.json new file mode 100644 index 0000000..f0454ef --- /dev/null +++ b/frontend/src/data/zones/특정어업수역Ⅰ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed1", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2160", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[14612352.95900835, 4323569.555957972], [14550748.752774281, 4260105.381317261], [14544627.066163512, 4252568.169285575], [14439940.71936106, 4252568.1692174645], [14440259.902536998, 4254382.900417306], [14440565.249736432, 4256577.976660408], [14441200.37191117, 4258322.323996074], [14442128.627396706, 4261947.246114864], [14442446.188484557, 4263842.912458916], [14443081.310658677, 4265407.837348879], [14443838.571713375, 4268086.787104008], [14444461.480000762, 4270299.674084436], [14445414.16326165, 4272528.068283305], [14446488.98540456, 4275811.262361098], [14447111.893690424, 4279125.58051958], [14447441.668665636, 4283375.409280503], [14447441.668666717, 4285908.011073243], [14447747.015866045, 4287008.670616378], [14449298.17963799, 4289692.937620907], [14451325.68504222, 4294897.478106175], [14452583.715503562, 4299470.4800484385], [14452583.715504179, 4299666.724555172], [14452864.634927392, 4301297.200756734], [14452803.565487338, 4303864.187591692], [14452864.634926995, 4306733.892102277], [14452681.426607274, 4309982.105854211], [14452229.512752429, 4313034.803597244], [14451289.043378348, 4315906.938650241], [14450165.365684237, 4319883.836509038], [14448650.843575954, 4323816.808519151], [14447172.963130116, 4326268.076469068], [14445646.22713406, 4328477.720500556], [14443166.807874365, 4331384.242207033], [14440455.324744267, 4333928.090897115], [14438366.74990012, 4335578.885027033], [14435545.341778146, 4337381.416707463], [14435212.858055448, 4337568.367409996], [14433713.258582642, 4338411.570116835], [14431881.17538587, 4339153.947573591], [14430305.583837628, 4339729.704068462], [14430281.156061549, 4340669.162281877], [14432430.800344449, 4344124.648762426], [14433664.40302976, 4347050.554454509], [14434299.525204673, 4348582.044935347], [14435398.775122227, 4352055.241843149], [14436168.250064695, 4355377.820309184], [14436473.59726421, 4359156.794673912], [14436632.377808044, 4361358.014828757], [14437096.505551115, 4363255.98064219], [14438036.97492467, 4367341.541713113], [14438354.536012871, 4371595.823810485], [14438183.541581359, 4375213.2915699165], [14437218.644430902, 4379455.492532148], [14436754.516687542, 4381676.095802414], [14437218.644430652, 4383410.311934654], [14438940.802635401, 4387670.983955953], [14440333.18586435, 4392085.61379955], [14440821.741384165, 4395862.331199512], [14440968.308038985, 4399000.459396788], [14441114.874694768, 4403419.77764905], [14441273.655239271, 4409426.891672738], [14440772.885832038, 4413972.594613021], [14439991.197000964, 4416642.956092686], [14438891.94708353, 4419329.253028348], [14437621.702733777, 4422642.269553811], [14436046.111184767, 4426582.592213162], [14435117.855699727, 4428767.199310407], [14434946.861267168, 4430203.479647634], [14434946.86126783, 4432709.795922216], [14434470.519636003, 4435537.753788483], [14434617.086292291, 4437433.672085769], [14434617.08629216, 4439314.635756483], [14434922.433492135, 4440431.137925814], [14435545.341778962, 4443276.456208943], [14435862.902865293, 4448617.316293129], [14435374.347346198, 4453195.1551512005], [14434433.877972556, 4456978.343560426], [14433493.582733309, 4459337.341378763], [14433493.408598367, 4459337.778245751], [14432540.725337084, 4461222.640432275], [14430134.5894049, 4464840.066412149], [14429071.98115172, 4466066.593934474], [14429377.32835093, 4468244.031514878], [14429487.253342932, 4471664.436166196], [14429487.253342314, 4474871.114707357], [14428962.056159778, 4478201.569995016], [14428339.14787256, 4480581.111676515], [14427423.10627465, 4482961.1914115725], [14426482.636900885, 4485234.2862444], [14424332.992617503, 4488229.992534473], [14422684.117740594, 4490519.594851016], [14421218.451183844, 4492071.890874874], [14420192.484594172, 4493424.573415368], [14418641.32082132, 4495484.674666911], [14415856.554362778, 4498806.286043997], [14415123.721083565, 4501082.812019949], [14413291.637888283, 4505437.2749514375], [14411545.051908031, 4508454.147134834], [14409053.418760045, 4511333.299393252], [14407502.25498812, 4512996.485903551], [14405743.455119297, 4516308.245790257], [14404192.291346865, 4519020.04815391], [14402445.705366325, 4520776.931720719], [14401199.88879329, 4522117.909890488], [14401163.247128874, 4522441.620044785], [14400674.691610316, 4526804.977928812], [14399135.74172542, 4530630.196696397], [14397889.92515128, 4533530.893432845], [14396448.686370509, 4536216.299453886], [14394482.250406215, 4538609.053820163], [14393969.267111044, 4540878.818581772], [14393248.647720557, 4543272.6434066], [14391697.483947853, 4546578.5701886], [14389950.897967545, 4549576.439029636], [14387996.67589144, 4552080.475715166], [14386433.298231108, 4553935.706287525], [14384686.712251175, 4555296.4190314105], [14384063.803963741, 4555698.481870257], [14382817.987391062, 4558497.888716806], [14382023.990793852, 4559433.839678707], [14379825.584836785, 4562025.286767265], [14377993.501640612, 4563882.293207642], [14375709.504589545, 4565538.404034689], [14373230.085329972, 4569687.579878523], [14370848.377174843, 4572490.760700769], [14369101.79119466, 4574132.75433843], [14367978.113501683, 4575372.167021386], [14367025.430239363, 4578161.387517195], [14365584.191457871, 4580563.818041922], [14364155.16656508, 4582966.805917671], [14362591.788904771, 4585230.77563233], [14360136.797420906, 4586983.366182029], [14359501.67524686, 4589031.016082314], [14357755.089266032, 4591730.810433944], [14356924.544884088, 4593049.931812809], [14355397.808887746, 4596589.112554951], [14353321.447931753, 4599694.655522917], [14351355.011967713, 4602086.560165967], [14350548.895361273, 4602909.876326975], [14349058.80102805, 4604059.521970236], [14348362.609413402, 4605286.987737952], [14347006.867848393, 4607571.395078686], [14345260.281868543, 4610058.400036733], [14344344.240270587, 4614925.339107906], [14344178.729650684, 4615426.598601238], [14406264.563155375, 4615426.598601238], [14471145.302268442, 4615426.598601238], [14489820.50078106, 4579817.049246806], [14657058.866457367, 4579819.039140932], [14657058.866471501, 4498513.035634587], [14653280.330118885, 4484660.955595197], [14653257.89496764, 4484604.528273547], [14653111.328311926, 4484251.265963233], [14652952.547767457, 4483867.298646529], [14652805.981111629, 4483498.703204842], [14652793.767223712, 4483437.271887497], [14652732.697783664, 4483283.695161308], [14652573.91724023, 4482899.763155043], [14652463.992248593, 4482531.201607391], [14652317.425592588, 4482147.2970551355], [14652183.072824666, 4481778.761862163], [14652085.361720739, 4481394.884757528], [14651963.222840805, 4481011.021654676], [14651853.297849245, 4480627.172552061], [14651731.158969458, 4480243.337446124], [14651645.661754325, 4479828.811251124], [14651560.164537868, 4479460.357225606], [14651450.239546517, 4479061.213248133], [14651376.956218421, 4478677.435232205], [14651303.672890497, 4478278.320934065], [14651230.38956299, 4477894.571451181], [14651181.534010744, 4477495.486823346], [14651120.464571165, 4477096.417317753], [14651059.395131014, 4476712.710899764], [14650998.325691152, 4476313.671051835], [14650996.99554685, 4476290.271738564], [14645926.917379338, 4457703.411105691], [14630424.731020536, 4444761.216899179], [14601399.121065676, 4420528.823331998], [14513278.61218791, 4420528.823356817], [14513278.612126667, 4323569.5559741575], [14612352.95900835, 4323569.555957972]], [[14531705.281810218, 4513797.373626424], [14531693.067921922, 4513597.145949486], [14531680.854034334, 4513381.520424651], [14531680.854033662, 4513196.702081734], [14531656.42625728, 4512981.084804853], [14531631.99848197, 4512596.064997208], [14531631.998481335, 4512395.860290817], [14531631.998482231, 4512180.259503853], [14531619.784594133, 4512010.861999429], [14531619.784593917, 4511795.269138], [14531619.784594417, 4511579.680716071], [14531619.784594513, 4511394.89417161], [14531619.784594564, 4511194.712427556], [14531631.998481907, 4510979.136366602], [14531631.99848219, 4510794.36041791], [14531631.998481516, 4510578.792596463], [14531656.426257836, 4510193.861093197], [14531680.854033632, 4509993.7023007], [14531680.854033832, 4509793.547333112], [14531693.067922262, 4509593.396189629], [14531705.28180977, 4509393.248870632], [14531741.923473405, 4509193.105373775], [14531754.137361716, 4508992.965701181], [14531778.565137729, 4508608.0924608875], [14531827.420689235, 4508207.83928627], [14531888.490129804, 4507807.60139917], [14531937.345681723, 4507407.378797934], [14531986.201233227, 4507022.563787677], [14532071.698448502, 4506622.371163723], [14532144.981777469, 4506222.193819813], [14532218.26510484, 4505822.031752334], [14532291.54843238, 4505437.274939068], [14532389.259536454, 4505052.532246742], [14532486.970639894, 4504667.803672876], [14532584.681744624, 4504283.089215654], [14532682.3928476, 4503898.388874675], [14532804.5317276, 4503498.31548994], [14532816.74561534, 4503467.54124596], [14532841.173391888, 4503236.737298468], [14532890.028944094, 4502836.689154722], [14532951.098383617, 4502436.656270566], [14533036.595599566, 4502036.638641951], [14533085.451150997, 4501652.020693331], [14533170.94836721, 4501252.032985861], [14533256.445583222, 4500882.827099174], [14533329.728910444, 4500467.486007207], [14533427.440014655, 4500082.925582424], [14533512.937230268, 4499698.379251896], [14533537.365005817, 4499636.85314637], [14533635.07610988, 4499313.847011714], [14533720.573326208, 4498929.328863146], [14533842.71220557, 4498544.824803407], [14533964.851085538, 4498160.334828932], [14534086.989965722, 4497775.8589393], [14534221.342733555, 4497406.77533439], [14534343.481613029, 4497022.3270443855], [14534490.048267843, 4496653.269931789], [14534636.614923632, 4496284.225792352], [14534795.395467635, 4495899.818607236], [14534941.962123044, 4495530.800950158], [14535088.528779598, 4495161.796260659], [14535271.737098787, 4494808.178934617], [14535442.73153039, 4494454.573515013], [14535625.939850742, 4494085.606640488], [14535821.362058103, 4493732.025548209], [14535992.356489455, 4493363.084053765], [14536187.77869705, 4493024.899068225], [14536370.987016352, 4492671.353679356], [14536578.623112632, 4492333.190962355], [14536798.473096136, 4491979.668849295], [14537006.10919119, 4491656.8981793765], [14537225.959175108, 4491303.398821744], [14537421.381382758, 4490980.648924496], [14537653.44525381, 4490642.540617242], [14537909.936901638, 4490319.811016954], [14538129.786885347, 4489997.091326332], [14538374.0646448, 4489659.014660098], [14538618.342404164, 4489351.681672938], [14538850.406276468, 4489044.357672096], [14539131.325698882, 4488721.677141822], [14539363.389571583, 4488429.736623869], [14539644.308994643, 4488122.43957564], [14539900.800642235, 4487815.151508973], [14539986.297858382, 4487722.966840681], [14540010.725633759, 4487692.23879659], [14540267.217281476, 4487400.32686548], [14540511.495040976, 4487077.696790041], [14540780.200575706, 4486770.43925529], [14541073.333887419, 4486493.915150635], [14541329.825535271, 4486186.674672224], [14541598.531071173, 4485910.165917298], [14541867.23660634, 4485618.303450893], [14542148.156029876, 4485326.449084601], [14542453.503228514, 4485049.962943723], [14542734.422652928, 4484773.484071729], [14543039.76985148, 4484512.371808684], [14543332.903163565, 4484251.266027703], [14543638.250362527, 4483974.808145135], [14543943.59756212, 4483713.715705612], [14544248.944761822, 4483467.987564921], [14544566.505848715, 4483222.26516165], [14544871.85304831, 4482976.548497614], [14545213.841912381, 4482746.194336815], [14545519.189110842, 4482515.845218762], [14545836.750198007, 4482254.788980392], [14546190.952949973, 4482024.450618634], [14546508.514037313, 4481824.828116437], [14546838.28901309, 4481609.854272898], [14547180.27787654, 4481394.88481907], [14547510.052850928, 4481179.919756588], [14547876.469490254, 4480995.667482875], [14548218.458354343, 4480796.06449436], [14548560.447216801, 4480596.465289407], [14548926.863856547, 4480412.223228773], [14549293.280496065, 4480212.631302457], [14549647.483247736, 4480043.748782027], [14549989.472111017, 4479874.868972281], [14550355.88875037, 4479721.344221679], [14550624.594284926, 4479598.526033944], [14550856.658157144, 4479491.061295501], [14551198.647020763, 4479291.48683264], [14551565.063659478, 4479107.26761045], [14551919.266411318, 4478923.051610395], [14552285.683050882, 4478769.540739722], [14552627.671913499, 4478600.681367721], [14553006.302441431, 4478431.8247021455], [14553372.719079891, 4478278.320992562], [14553739.135719031, 4478124.8195214365], [14554117.766246844, 4477986.670110905], [14554496.39677429, 4477833.172889764], [14554875.02730134, 4477710.376725644], [14555253.657828972, 4477572.232751319], [14555620.074468583, 4477464.788689573], [14555998.704995206, 4477326.647937234], [14556377.335522415, 4477234.555108875], [14556511.688290423, 4477157.811699739], [14556878.10492996, 4476958.281453992], [14557220.093792727, 4476804.799222015], [14557598.724319693, 4476651.319227265], [14557952.92707147, 4476482.493814672], [14558331.557598298, 4476329.018516568], [14558697.974237733, 4476175.54545325], [14559076.604765655, 4476022.0746261515], [14559443.02140455, 4475883.952793535], [14559821.65193224, 4475761.179352402], [14560200.282459686, 4475638.407340859], [14560578.912986, 4475500.290538945], [14560957.54351379, 4475377.521568426], [14561348.38792862, 4475270.099892532], [14561727.01845645, 4475147.333604416], [14562117.862871993, 4475055.259828022], [14562484.27951158, 4474963.186855402], [14562899.551702326, 4474871.1146875555], [14563290.396117546, 4474809.733688274], [14563669.026644476, 4474733.007943433], [14564059.871060286, 4474656.282757424], [14564450.7154752, 4474579.558129849], [14564853.773778029, 4474518.178830546], [14565244.618193747, 4474456.79988746], [14565635.462609466, 4474395.4213030925], [14565843.09870468, 4474380.076712145], [14566026.307024052, 4474364.732145027], [14566429.365327647, 4474303.354096425], [14566624.787535438, 4474303.354096845], [14566820.209743189, 4474288.009639743], [14567040.059725929, 4474272.665205484], [14567235.481933901, 4474241.97640493], [14567430.904141523, 4474241.9764056895], [14567638.540236901, 4474226.632038639], [14567821.748556953, 4474226.632038577], [14568029.384652914, 4474211.287694249], [14568212.592971867, 4474211.287693342], [14568420.229068192, 4474195.943371697], [14568627.865163937, 4474195.943371153], [14568811.073482776, 4474195.943371623], [14569018.709579367, 4474195.94337142], [14569214.131786728, 4474195.943370581], [14569238.559562922, 4474195.943371392], [14569409.553994717, 4474195.943371547], [14569629.403978188, 4474195.943370894], [14569824.826185605, 4474211.287692922], [14570020.248392954, 4474211.287693488], [14570215.670600649, 4474211.287693272], [14570423.306696696, 4474226.632037414], [14570618.728903888, 4474226.632037795], [14570814.151111197, 4474241.97640376], [14571021.787206706, 4474272.665204512], [14571204.995527001, 4474288.009638165], [14571412.631621836, 4474288.009637828], [14571620.267718533, 4474303.354094652], [14572023.326021364, 4474364.732141971], [14572194.32045334, 4474380.076710127], [14572414.170436617, 4474395.421300662], [14572805.01485221, 4474456.799885332], [14573208.073155506, 4474518.178827347], [14573598.917569762, 4474579.558126053], [14573989.761985833, 4474656.282754001], [14574368.392513085, 4474733.007939714], [14574759.236928629, 4474809.733685015], [14575162.295230972, 4474871.114683236], [14575553.139646066, 4474963.186850651], [14575931.770173518, 4475055.259823188], [14576322.614588926, 4475147.3335995795], [14576701.245116178, 4475270.0998873925], [14577079.875643862, 4475377.521562786], [14577470.720059728, 4475500.290533416], [14577837.13669783, 4475638.407335261], [14578215.767225962, 4475761.179345143], [14578594.39775321, 4475883.95278683], [14578973.028280452, 4476022.074619254], [14579327.231032163, 4476175.54544557], [14579705.861558419, 4476329.018508264], [14580084.492086556, 4476482.493807283], [14580450.908725094, 4476651.3192181215], [14580805.111476777, 4476804.799213971], [14581183.742004093, 4476958.281445817], [14581525.730867168, 4477157.811690843], [14581879.933618782, 4477326.647927245], [14582234.136370221, 4477526.185151994], [14582588.3391218, 4477710.376715044], [14582930.327984763, 4477909.9212099165], [14583296.744624889, 4478094.119485319], [14583626.51959986, 4478309.021544471], [14583968.508463632, 4478539.278618473], [14584298.28343856, 4478754.189764326], [14584615.844526524, 4478969.105295983], [14584957.833389819, 4479199.376805741], [14585275.39447627, 4479429.653352519], [14585592.955563627, 4479659.934935787], [14585922.730539948, 4479890.221556971], [14586240.291627208, 4480135.866171305], [14586557.8527144, 4480396.869855059], [14586863.199913831, 4480642.526293688], [14587156.33322539, 4480888.188466244], [14587473.894312968, 4481164.565263089], [14587767.027623786, 4481425.59445718], [14588060.160935674, 4481701.985366741], [14588353.294247115, 4481978.383535463], [14588646.427559184, 4482254.788964908], [14588939.56086965, 4482546.558126468], [14589208.266406132, 4482822.978480075], [14589476.971941242, 4483114.763399049], [14589733.46358897, 4483421.914160338], [14590014.383012347, 4483698.35751204], [14590185.377443707, 4483898.0155623555], [14590331.944099901, 4484020.883937888], [14590661.719074445, 4484251.266009965], [14590979.280162634, 4484497.0124473], [14591296.841249231, 4484727.404948229], [14591602.18844864, 4484973.162509527], [14591919.749536166, 4485234.28621192], [14592212.882847624, 4485495.41639687], [14592518.23004711, 4485756.553064018], [14592823.577246739, 4486017.696217524], [14593116.710558899, 4486294.207799355], [14593409.843869546, 4486570.726653404], [14593690.763293965, 4486847.252777424], [14593983.89660544, 4487139.149354413], [14594252.602139814, 4487431.054034741], [14594533.52156419, 4487692.238776791], [14594802.227099039, 4487999.523250918], [14595058.718747094, 4488306.8167062495], [14595339.638170302, 4488598.753809731], [14595583.915929569, 4488906.064781649], [14595828.19368928, 4489213.384739871], [14596096.899225544, 4489536.080365369], [14596341.176984914, 4489858.785899267], [14596573.240855644, 4490181.501342655], [14596805.304727584, 4490488.858596873], [14597037.368599355, 4490826.961959009], [14597257.21858321, 4491149.707135566], [14597489.282454617, 4491472.462225323], [14597696.918549843, 4491825.967271368], [14597928.98242226, 4492148.743134879], [14598136.618516896, 4492502.270936908], [14598319.826836484, 4492840.439096991], [14598527.462932773, 4493193.990176503], [14598722.885140764, 4493547.553156926], [14598906.093460135, 4493901.128039849], [14599101.515668057, 4494254.714825081], [14599260.296211885, 4494623.687640952], [14599431.290642768, 4494977.2987543205], [14599590.07118727, 4495330.921775762], [14599773.279505912, 4495715.308132935], [14599919.846161587, 4496068.956009824], [14600078.626705699, 4496453.369387598], [14600225.193361096, 4496822.419472711], [14600347.33224078, 4497206.860440532], [14600481.685009632, 4497575.937016725], [14600616.037777228, 4497960.4055835055], [14600750.39054488, 4498344.888232693], [14600860.31553649, 4498714.004829062], [14600982.454416526, 4499098.51508797], [14601080.165519364, 4499483.0394360265], [14601202.304399468, 4499867.577874515], [14601300.01550408, 4500267.512801375], [14601385.512719708, 4500652.079992499], [14601458.796047695, 4501052.044824842], [14601471.009935707, 4501175.113995761], [14601654.218255237, 4501498.177436593], [14601800.784910476, 4501867.404980572], [14601971.779342758, 4502236.64552286], [14602118.34599798, 4502590.513240334], [14602277.126541303, 4502959.779238895], [14602423.693198, 4503344.4451490035], [14602558.045964886, 4503698.350247777], [14602680.184845533, 4504083.043249815], [14602814.537612794, 4504467.750366676], [14602936.676492875, 4504852.471599208], [14603058.815372452, 4505237.206950171], [14603180.954252187, 4505606.56617041], [14603266.451468613, 4506022.110849317], [14603388.590348229, 4506391.497726757], [14603474.087563867, 4506791.681535702], [14603547.37089101, 4507161.095535717], [14603645.08199488, 4507561.308730727], [14603718.365323769, 4507946.143524667], [14603791.648650708, 4508330.992452457], [14603864.931978848, 4508746.64517058], [14603926.001419289, 4509131.523501303], [14603987.070858913, 4509531.8119634325], [14604035.92641036, 4509916.719140585], [14604084.781962857, 4510332.43477583], [14604109.20973883, 4510717.37137208], [14604145.85140303, 4510932.942045082], [14604158.065290203, 4511117.720440137], [14604170.279178778, 4511317.900712452], [14604170.279177956, 4511533.483745455], [14604182.493066223, 4511718.272735513], [14604219.134730231, 4511933.864012004], [14604219.134729845, 4512318.859471839], [14604231.348618282, 4512519.062705031], [14604231.348618418, 4512719.269766486], [14604231.348617738, 4512904.079682778], [14604231.348618373, 4513119.695373501], [14604231.348618187, 4513319.913919597], [14604231.348618748, 4513520.136295258], [14604231.348618748, 4513720.362499544], [14604231.348618232, 4513920.592533981], [14604231.348618407, 4514120.826397525], [14604219.134730808, 4514336.467148453], [14604219.134730032, 4514721.550967597], [14604182.493066857, 4514906.396233077], [14604170.27917791, 4515122.053169531], [14604170.279178878, 4515322.310016387], [14604158.065290527, 4515507.165891377], [14604145.851402044, 4515722.835206429], [14604109.209738161, 4515923.103548852], [14604084.781962737, 4516308.245749079], [14604035.926410299, 4516708.808674816], [14603987.070858993, 4517093.9797944045], [14603926.00141845, 4517509.9805284925], [14603864.931978678, 4517895.181143359], [14603791.64865124, 4518295.804826711], [14603718.365322556, 4518681.034375972], [14603645.081995493, 4519097.098223365], [14603547.370891701, 4519466.946648656], [14603474.08756368, 4519867.630533778], [14603388.59034769, 4520237.506207204], [14603266.451468341, 4520638.219615408], [14603180.95425151, 4521008.12254481], [14603058.815372169, 4521408.865484192], [14602936.676491957, 4521778.79567707], [14602814.537612109, 4522164.153545156], [14602680.184844451, 4522534.110462632], [14602558.045964777, 4522919.496172441], [14602423.693197738, 4523289.47982265], [14602277.126541242, 4523674.893382433], [14602118.345997736, 4524044.903771376], [14601971.779341936, 4524414.927259076], [14601800.784909926, 4524784.963848068], [14601654.218254454, 4525155.013540586], [14601471.009935107, 4525525.076335961], [14601300.015503855, 4525864.3120796885], [14601129.021072082, 4526234.399998049], [14600933.598864602, 4526573.658772662], [14600750.390544403, 4526928.350179163], [14600542.754449246, 4527267.63148988], [14600347.332241392, 4527637.769124864], [14600151.910033902, 4527961.6503126025], [14599944.273938052, 4528316.388852858], [14599712.210066758, 4528640.29108562], [14599504.573970841, 4528964.203363799], [14599272.510098685, 4529318.975958536], [14599052.660115397, 4529627.483662931], [14598808.382356219, 4529951.426563321], [14598588.532372601, 4530275.379513526], [14598344.254612468, 4530599.342514882], [14598087.762964793, 4530907.88805286], [14597831.271317031, 4531216.442710067], [14597586.993558556, 4531525.006485532], [14597318.288022641, 4531833.57938278], [14597061.796375385, 4532126.732084111], [14596793.090839129, 4532419.893019322], [14596512.171415407, 4532713.062188578], [14596231.251992663, 4533021.670209015], [14595950.332568703, 4533299.42523091], [14595669.413144821, 4533577.187645228], [14595644.985369092, 4533623.48209964], [14595498.41871387, 4533777.7984313145], [14595376.279834235, 4533916.685081554], [14595119.788186248, 4534225.328697573], [14594838.868762594, 4534518.548590586], [14594582.377115823, 4534811.776722892], [14594289.243803782, 4535089.579398674], [14594008.324380705, 4535367.389470016], [14593727.404956257, 4535660.64146122], [14593446.48553373, 4535923.031808229], [14593153.352221377, 4536200.864075813], [14592848.005022287, 4536478.703743617], [14592567.08559909, 4536741.114669866], [14592249.524511898, 4537003.532198354], [14591944.177311765, 4537250.519432793], [14591614.402336147, 4537497.512515288], [14591309.055137279, 4537729.073843565], [14591003.707938092, 4537991.5162313925], [14590686.146850547, 4538238.5268653], [14590368.585762527, 4538454.665970274], [14590014.383011634, 4538686.248553525], [14589684.608036457, 4538917.836279545], [14589354.833060294, 4539118.549804093], [14589025.058085084, 4539319.267192334], [14588683.069222417, 4539535.42869948], [14588328.866470784, 4539736.154114145], [14587986.877606917, 4539952.324265979], [14587657.102632334, 4540137.616535028], [14587290.685992181, 4540338.353544246], [14586936.483240709, 4540508.210955269], [14586570.066601887, 4540678.071133686], [14586228.077738127, 4540863.376303522], [14585861.661099326, 4541033.242270953], [14585495.244460465, 4541187.668278762], [14585128.827820107, 4541342.096575501], [14584750.197293801, 4541496.527160034], [14584395.994541308, 4541650.960032226], [14584347.138989441, 4541666.403445655], [14584286.06955062, 4541697.29033999], [14583919.652910553, 4541851.726188135], [14583565.450158978, 4542021.60826263], [14583186.819632547, 4542176.048916555], [14582844.83076899, 4542330.491858846], [14582466.20024182, 4542484.937089604], [14582087.569714688, 4542608.49492355], [14581708.939186862, 4542762.94427407], [14581330.308658957, 4542901.950648047], [14580963.892020464, 4543010.067999098], [14580585.261492236, 4543149.07766965], [14580206.630964926, 4543257.1975837825], [14579803.57266225, 4543365.318620336], [14579424.942134961, 4543457.994687216], [14579192.878263632, 4543519.779190642], [14579034.09771989, 4543550.671579929], [14578655.467192937, 4543658.795661787], [14578264.622776985, 4543751.474338827], [14577873.77836218, 4543828.707200758], [14577495.147835061, 4543905.940633789], [14577092.089531014, 4543983.174640503], [14576701.245115522, 4544029.515319117], [14576310.400701316, 4544106.750241843], [14575919.556285223, 4544137.6443707235], [14575699.706302246, 4544183.98573635], [14575504.284093758, 4544199.432905103], [14575113.439679246, 4544230.327308818], [14574722.595264157, 4544276.669086714], [14574514.959168296, 4544292.1163917575], [14574331.750849022, 4544307.563720322], [14574124.11475319, 4544307.563719708], [14573916.478657782, 4544338.458445055], [14573733.270338044, 4544353.905841484], [14573525.634242143, 4544353.905841748], [14573330.212034652, 4544369.353261512], [14573134.789826233, 4544369.353261464], [14572939.367619064, 4544369.353262303], [14572719.517635329, 4544369.353261675], [14572524.095428342, 4544369.353261389], [14572328.673220538, 4544384.800704624], [14572121.037124906, 4544384.800704819], [14571937.828804424, 4544369.353261591], [14571730.192709187, 4544369.35326217], [14571522.556614075, 4544369.353262826], [14571339.34829391, 4544369.353262385], [14571131.712198837, 4544353.905842261], [14570948.503878202, 4544353.905843028], [14570740.867782762, 4544338.45844611], [14570545.44557518, 4544338.458445838], [14570350.023367973, 4544307.563721146], [14570130.173384072, 4544292.116393631], [14569934.751176836, 4544276.669089307], [14569543.906761209, 4544230.327311454], [14569153.062345682, 4544199.432906603], [14568945.42625058, 4544183.985738961], [14568762.21793041, 4544137.6443743445], [14568359.15962792, 4544106.750244441], [14567968.315212548, 4544029.515323136], [14567577.470797084, 4543983.174644471], [14567186.6263823, 4543905.940638149], [14566783.568079067, 4543828.707204437], [14566392.72366387, 4543751.474343973], [14566014.093135444, 4543658.795666206], [14565623.24872121, 4543550.671584686], [14565244.618193153, 4543457.994693514], [14564853.773778267, 4543365.3186267475], [14564475.14325144, 4543257.19758965], [14564096.512723364, 4543149.07767554], [14563693.454420516, 4543010.068005312], [14563314.823893422, 4542901.950654995], [14562936.193365432, 4542762.944282519], [14562606.418390313, 4542716.609237178], [14562398.782294482, 4542685.71932119], [14562191.146199709, 4542670.274397315], [14561812.51567194, 4542608.494931122], [14561421.671257151, 4542562.160572617], [14561030.826841386, 4542500.381747122], [14560639.982426064, 4542423.158731613], [14560236.924122458, 4542345.936288569], [14559833.86581993, 4542268.714416368], [14559467.449180512, 4542176.048925813], [14559076.604765026, 4542083.384260142], [14558697.97423732, 4541990.720416876], [14558307.129823012, 4541867.169908754], [14557928.499294864, 4541774.507987912], [14557525.440991675, 4541650.960043525], [14557146.81046419, 4541542.856792612], [14556768.179936875, 4541403.868545181], [14556389.549410133, 4541264.882152807], [14556010.918883575, 4541141.3402487645], [14555644.502244113, 4541017.7998076], [14555290.299492834, 4540863.376315873], [14554911.668964645, 4540708.95511347], [14554557.466213938, 4540523.652687981], [14554178.835686168, 4540384.678031769], [14553824.632935008, 4540214.822632615], [14553458.216296038, 4540044.970003144], [14553116.227432424, 4539844.238643887], [14552749.810793048, 4539689.83253557], [14552407.821930347, 4539504.548224409], [14552249.041385714, 4539427.347399758], [14551882.624746233, 4539303.827270621], [14551503.99422004, 4539195.748356934], [14551381.855340248, 4539149.4291654825], [14551125.36369234, 4539072.230970899], [14550746.73316546, 4538948.715047952], [14550368.102638047, 4538825.200587222], [14549989.472110914, 4538686.24856897], [14549623.055470902, 4538531.8596091075], [14549244.42494422, 4538377.472935194], [14548902.436080279, 4538238.526881961], [14548523.805553462, 4538084.144551083], [14548157.38891405, 4537914.3266261555], [14547803.186162714, 4537729.073861541], [14547436.769523405, 4537559.261718393], [14547094.7806594, 4537404.8894423675], [14546728.364020523, 4537204.208898579], [14546361.947381083, 4537018.9687477425], [14546032.172405548, 4536818.295628578], [14545677.969654717, 4536633.062331327], [14545323.76690356, 4536432.396637806], [14544993.991928555, 4536216.299437661], [14544652.003064753, 4536000.206715432], [14544322.228089612, 4535799.553195227], [14544004.667001592, 4535568.034697714], [14543662.678138765, 4535351.9554023], [14543345.117051568, 4535120.44683895], [14543027.555963451, 4534858.076675749], [14542697.780988807, 4534626.579069244], [14542380.219901314, 4534395.086599067], [14542074.87270228, 4534132.734675581], [14541781.73939042, 4533870.389347416], [14541464.178302774, 4533623.482121268], [14541158.831103068, 4533345.718475012], [14540877.911680002, 4533098.82366223], [14540572.564480469, 4532821.073979021], [14540291.645057205, 4532543.331687368], [14540010.725633612, 4532265.5967861395], [14539729.806210512, 4531972.440185714], [14539448.886786574, 4531679.291817728], [14539192.395139629, 4531417.008149412], [14538899.261827474, 4531108.447566406], [14538630.55629233, 4530815.323457989], [14538374.064645067, 4530506.780656056], [14538129.786885653, 4530198.2469714265], [14537873.295237642, 4529874.296414274], [14537616.803589916, 4529565.781417542], [14537396.953606762, 4529257.275535442], [14537152.67584749, 4528933.354167178], [14536932.825863078, 4528624.866966413], [14536676.334216729, 4528270.117950614], [14536444.27034455, 4527946.227197342], [14536248.848136436, 4527606.923846032], [14536028.998153169, 4527267.631515431], [14535821.362057582, 4526928.3502052585], [14535601.512074055, 4526589.079913595], [14535418.30375431, 4526249.820638423], [14535210.667658564, 4525910.572378158], [14535015.245451855, 4525555.915520818], [14534832.037131598, 4525185.851631445], [14534673.256587537, 4524831.219369989], [14534490.048268745, 4524476.599140016], [14534331.267724525, 4524106.573469703], [14534160.273292877, 4523751.977827433], [14533989.278861778, 4523366.561424789], [14533842.71220565, 4523027.406744612], [14533696.145549532, 4522642.01705591], [14533561.79278178, 4522287.47108967], [14533403.01223859, 4521902.108676457], [14533268.659471177, 4521532.174121515], [14533134.306703577, 4521146.839544288], [14533036.59559936, 4520776.931708183], [14532914.45671886, 4520376.21298474], [14532804.531727992, 4520006.332405602], [14532682.392847996, 4519621.054045723], [14532596.895632427, 4519220.379606621], [14532499.184527747, 4518835.130195337], [14532413.687312467, 4518449.894972147], [14532303.762320925, 4518049.265387172], [14532230.478993248, 4517664.059100537], [14532157.19566512, 4517278.866996087], [14532108.340113258, 4516878.282249036], [14532035.056785649, 4516477.712836037], [14531973.987345573, 4516092.564399366], [14531973.987345243, 4516030.941964629], [14531937.345681304, 4515784.455855288], [14531888.490129516, 4515383.928306678], [14531827.420689756, 4514983.416085525], [14531778.56513731, 4514582.919189623], [14531741.923473246, 4513982.2025730265], [14531705.281810218, 4513797.373626424]]], [[[14339432.408530401, 4075075.6362608722], [14339458.685080042, 4075084.437250298], [14339751.818391822, 4076223.538724107], [14338652.568473613, 4084644.61678336], [14338506.00181847, 4086228.895738871], [14336759.41583827, 4097428.8401910467], [14341315.196051706, 4104501.207588504], [14341938.104338527, 4105925.114876204], [14342707.57928172, 4107200.8516809675], [14343501.482000086, 4108610.2642060244], [14352735.181309098, 4124392.929620267], [14356472.63102952, 4130694.3846049826], [14357889.442034634, 4133221.908237402], [14359770.380782299, 4136374.679519459], [14361968.880617706, 4140004.4420440258], [14364619.294308105, 4144900.5395199214], [14371984.268757481, 4157187.447747604], [14376991.962826528, 4165401.170055259], [14381535.529153243, 4172024.2679253733], [14386396.65656731, 4178188.490732939], [14390109.678512042, 4182503.992319043], [14410071.323094087, 4181759.7407464916], [14411740.474114683, 4181697.507984717], [14411887.04077094, 4182668.28335924], [14412290.099073624, 4183937.8871818185], [14413145.071231768, 4185521.361705531], [14414659.593340822, 4189466.094009724], [14416235.184889486, 4190347.874119261], [14418116.123638438, 4191708.045624967], [14420009.276274431, 4193442.1331010573], [14421670.365039108, 4195430.688718817], [14423087.176044246, 4197314.91349072], [14424369.634281721, 4199438.789978044], [14425468.884199308, 4201682.765014417], [14426323.856356785, 4203642.876257398], [14426897.90909205, 4205708.102752736], [14427508.603490569, 4207998.262274081], [14427826.164578045, 4210229.005413773], [14428705.564512718, 4211007.626033115], [14430427.722717127, 4212669.865587721], [14432333.089241156, 4214736.783326548], [14433591.119701806, 4217118.712887043], [14434763.65294765, 4219576.085746894], [14435569.76955409, 4222498.676121303], [14436070.538960757, 4223802.851526747], [14436840.01390352, 4227416.375070025], [14437157.574990707, 4228856.1177854985], [14437963.691597, 4230581.057929868], [14439343.860938719, 4234526.939401433], [14440296.544200161, 4238774.472618673], [14440443.110856375, 4242242.7597973915], [14440113.33588104, 4245862.359133215], [14439478.213706143, 4248867.084183436], [14439331.647050746, 4249813.743335828], [14439649.208137836, 4250910.768227175], [14439940.719361056, 4252568.169217453], [14544627.066163512, 4252568.169285575], [14534796.669683114, 4240464.677659911], [14514759.161462417, 4205175.173351611], [14501957.419936124, 4179737.9296527705], [14485448.739516629, 4179288.840961339], [14439555.743282635, 4173635.0297790095], [14435722.322889304, 4173166.719291172], [14421250.789188549, 4166599.3964159037], [14402137.232618976, 4158446.631542469], [14394344.868232908, 4150978.506075684], [14389524.734276652, 4146676.4225404835], [14384326.1140242, 4142606.5776094557], [14365958.39800865, 4119341.9682434443], [14361694.86160705, 4101989.3419181844], [14361694.861568779, 4100867.6858379836], [14360581.666636346, 4094329.1704151263], [14359101.117467742, 4090737.3044364187], [14347779.925183792, 4070274.6906909533], [14347742.667809354, 4070231.176358625], [14339432.408530401, 4075075.6362608722]]]]}}]} \ No newline at end of file diff --git a/frontend/src/data/zones/특정어업수역Ⅱ.json b/frontend/src/data/zones/특정어업수역Ⅱ.json new file mode 100644 index 0000000..5f3cea7 --- /dev/null +++ b/frontend/src/data/zones/특정어업수역Ⅱ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed2", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2161", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[14026312.49388151, 3787395.72363925], [14026272.928939708, 3912341.809856742], [14026343.45295978, 3912257.596201178], [14026661.014047539, 3911892.988733094], [14026978.575133963, 3911557.559921697], [14027051.858461797, 3911470.0583396605], [14027137.355677672, 3911178.3911333913], [14027198.425118214, 3910930.4797375146], [14027271.708445255, 3910711.7387614], [14027344.991773328, 3910478.419569922], [14027406.061213229, 3910245.105041409], [14027503.77231727, 3910026.376906358], [14027577.055645473, 3909793.071410766], [14027650.338973064, 3909574.351742198], [14027723.622300655, 3909341.0552779627], [14027821.333404718, 3909122.34407479], [14027906.830620117, 3908903.6369685275], [14027992.327835856, 3908684.9339581975], [14028090.038940514, 3908451.65526086], [14028175.536155386, 3908218.3812227794], [14028273.247259233, 3908014.2702603256], [14028370.958363216, 3907781.0049572694], [14028468.669467552, 3907576.9016377437], [14028566.380571473, 3907343.6450677463], [14028664.091674816, 3907139.5493900776], [14028761.8027788, 3906920.8794053984], [14028883.941658128, 3906716.7911130013], [14028993.866650797, 3906483.5517138042], [14029091.577753998, 3906279.4710601526], [14029201.502746, 3906060.81717149], [14029323.641625034, 3905856.7438994534], [14029433.566617623, 3905638.097921009], [14029543.491609104, 3905419.4560323786], [14029665.630488753, 3905215.393960793], [14029714.486040367, 3905011.3354516793], [14029763.341592517, 3904807.2805052707], [14029824.411032889, 3904574.0792145403], [14029861.052696653, 3904340.8825751734], [14029922.122136267, 3904107.690588767], [14029983.19157568, 3903874.5032539973], [14030044.261016268, 3903641.3205705], [14030105.33045631, 3903393.5690651014], [14030178.613783477, 3903174.96915455], [14030239.683223227, 3902941.800421957], [14030312.96655101, 3902723.2089581615], [14030386.24987901, 3902475.4769059164], [14030447.319319358, 3902256.8941590027], [14030520.602646638, 3902023.7437318605], [14030606.099863403, 3901790.597953613], [14030691.597078484, 3901572.028007055], [14030764.880406654, 3901338.8912325953], [14030862.591510149, 3901120.329726406], [14030935.874838341, 3900887.201954522], [14031021.372054312, 3900668.648888896], [14031119.083157621, 3900450.0999057423], [14031204.580374023, 3900216.9854906956], [14031290.077589095, 3899998.444944336], [14031400.002581708, 3899779.908480802], [14031485.499797242, 3899561.37609934], [14031595.424789447, 3899342.847798803], [14031705.349781059, 3899138.891733757], [14031790.846996775, 3898905.803441251], [14031912.985876147, 3898701.8549923794], [14032010.696979966, 3898468.7754048174], [14032120.621971566, 3898264.8345725327], [14032230.54696337, 3898046.330481661], [14032352.685843932, 3897827.830470188], [14032450.396947037, 3897623.9008064135], [14032572.535826314, 3897405.4086795547], [14032694.674706502, 3897216.052135809], [14032816.81358599, 3896997.5676209624], [14032938.952466376, 3896793.652420099], [14033061.091346277, 3896589.740771255], [14033195.444113696, 3896385.8326731725], [14033329.796881828, 3896181.9281274145], [14033451.935761089, 3895978.0271314387], [14033586.288528644, 3895774.129686274], [14033720.641296018, 3895584.7995237107], [14033989.34683201, 3895191.5851198323], [14034258.05236752, 3894798.383918607], [14034563.399566252, 3894405.195917184], [14034844.31899014, 3894026.5829095095], [14035149.666189065, 3893647.982137594], [14035442.799501298, 3893283.954474], [14035760.360587686, 3892919.938120377], [14036077.92167527, 3892555.9330759617], [14036407.696651513, 3892191.9393388475], [14036737.471625743, 3891857.075086648], [14036870.141786523, 3891726.198837797], [14037091.674377358, 3891507.661720512], [14037421.449352924, 3891172.81701958], [14037787.86599152, 3890867.0976043935], [14038142.06874289, 3890532.271202124], [14038178.710406706, 3890503.1563148433], [14038215.352071756, 3890459.48412038], [14038569.554822957, 3890139.2263254467], [14038923.75757426, 3889818.9772791206], [14039302.388101518, 3889513.29316919], [14039681.018628426, 3889207.617028156], [14040059.649154926, 3888931.0597660383], [14040426.065794326, 3888639.953906682], [14040816.91020996, 3888348.855272826], [14041232.18240151, 3888072.318264321], [14041623.02681568, 3887810.3418493513], [14041818.449023297, 3887679.3558354746], [14042026.085119475, 3887548.3712851256], [14042221.507326983, 3887431.941802017], [14042429.14342227, 3887300.9600144974], [14042624.565629626, 3887184.532986891], [14042844.415613849, 3887053.553961726], [14043052.051709011, 3886951.682398294], [14043259.687804861, 3886820.705972922], [14043467.323900312, 3886704.283712141], [14043687.173883341, 3886602.415180504], [14043894.809980085, 3886471.4426559876], [14044102.446075153, 3886369.576147044], [14044310.082171045, 3886267.7105223597], [14044542.146041911, 3886151.2937484276], [14044749.78213759, 3886049.4300192064], [14044957.418233024, 3885947.567174553], [14045189.4821048, 3885860.256868118], [14045397.118200412, 3885743.844137209], [14045629.182072148, 3885656.5353473793], [14045849.032055777, 3885569.2272066567], [14046056.668151285, 3885481.9197152676], [14046288.73202292, 3885380.06179798], [14046508.58200612, 3885292.755714113], [14046740.6458772, 3885205.4502798985], [14046960.495860584, 3885132.696247827], [14047192.559732666, 3885045.392004632], [14047412.409715734, 3884958.0884115743], [14047644.473587925, 3884885.335912664], [14047864.323571343, 3884812.583865854], [14048096.387442762, 3884739.8322684863], [14048316.237426357, 3884667.081122923], [14048560.515186315, 3884579.7803441263], [14048780.365169752, 3884521.5801856825], [14049012.429040425, 3884448.8303926187], [14049232.279024879, 3884390.630883527], [14049476.556783637, 3884332.431662887], [14049708.620655935, 3884288.7824363117], [14049940.684527121, 3884216.034086747], [14050172.74839862, 3884157.835731066], [14050417.026158523, 3884099.6376657546], [14050636.876141772, 3884055.989305159], [14050881.153900763, 3884012.3411064013], [14051101.003885025, 3883968.693071144], [14051345.28164453, 3883925.045197191], [14051589.55940309, 3883881.397485363], [14051809.409386702, 3883837.7499363376], [14052053.687146327, 3883794.102549823], [14052297.96490621, 3883765.00438295], [14052530.028777948, 3883721.357266986], [14052774.306537522, 3883692.2592790583], [14052994.1565203, 3883677.710312659], [14053238.434280407, 3883648.6124334624], [14053482.712039571, 3883619.514626246], [14053726.989798827, 3883604.9657499343], [14053946.839782678, 3883575.8680508113], [14054203.331430309, 3883561.3192288275], [14054435.395301264, 3883546.770424295], [14054667.459173407, 3883532.221638027], [14054911.736932652, 3883517.6728706607], [14055156.014692299, 3883503.1241203477], [14055388.078563493, 3883503.1241198485], [14055632.356323073, 3883503.1241198387], [14055876.634082645, 3883503.1241195546], [14056120.911842786, 3883488.575388423], [14056352.97571373, 3883488.575387674], [14056585.039585229, 3883503.1241202564], [14056829.31734454, 3883503.1241205744], [14057061.381216811, 3883503.1241201926], [14057305.658975704, 3883517.672870415], [14057549.936734984, 3883532.2216384728], [14057782.000606766, 3883546.7704246663], [14058026.27836623, 3883561.3192288917], [14058270.556125652, 3883575.868050909], [14058514.833885796, 3883604.9657495967], [14058734.683868349, 3883619.5146270352], [14058978.96162855, 3883648.612433189], [14059211.02550005, 3883663.161363651], [14059455.303259443, 3883692.2592792334], [14059699.581018375, 3883721.35726594], [14059919.431002565, 3883765.0043828213], [14060163.708761357, 3883794.1025502756], [14060407.98652079, 3883837.7499373327], [14060652.26428107, 3883881.397485507], [14060872.114264324, 3883925.0451974412], [14061116.392023854, 3883968.6930701793], [14061348.455895012, 3884012.3411065093], [14061580.519767078, 3884055.9893042506], [14061824.797526775, 3884099.6376657034], [14062044.647510495, 3884157.8357315855], [14062288.925269853, 3884216.0340869497], [14062508.775252802, 3884288.782436949], [14062753.053012297, 3884332.4316628594], [14062985.116884248, 3884390.6308830315], [14063204.966867786, 3884448.8303930266], [14063449.244626828, 3884521.5801856043], [14063669.09461016, 3884579.780344494], [14063901.15848242, 3884667.0811240533], [14064121.008465912, 3884739.832268733], [14064353.072337683, 3884812.5838650106], [14064585.136208225, 3884885.3359133895], [14064817.200079866, 3884958.088411405], [14065037.050063629, 3885045.3920052126], [14065269.113934992, 3885132.6962482887], [14065488.963918757, 3885205.450280084], [14065708.813902, 3885292.755713535], [14065928.663886081, 3885380.0617970554], [14066160.72775689, 3885481.9197158483], [14066380.577740904, 3885569.2272062856], [14066588.21383698, 3885656.5353471697], [14066820.277708739, 3885743.8441374716], [14067040.127691144, 3885860.2568676393], [14067259.977674901, 3885947.5671746274], [14067467.613770252, 3886049.4300197763], [14067687.463753887, 3886151.2937480435], [14067907.313738173, 3886267.7105219206], [14068114.949834043, 3886369.5761465007], [14068322.585929519, 3886471.442655181], [14068554.649800802, 3886602.4151809313], [14068750.072007738, 3886704.283711534], [14068957.708103167, 3886820.705972922], [14069165.344199639, 3886951.6823977036], [14069372.980294656, 3887053.5539613245], [14069592.83027846, 3887184.53298712], [14069788.25248586, 3887300.9600143926], [14069995.888581414, 3887431.9418018376], [14070203.524677217, 3887548.3712856653], [14070398.946884345, 3887679.3558351737], [14070606.58297968, 3887810.341848666], [14070997.427395098, 3888072.318263754], [14071400.485698925, 3888348.855272708], [14071791.330113634, 3888639.95390655], [14072157.746753618, 3888931.059766593], [14072548.59116818, 3889207.617027234], [14072915.007807814, 3889513.2931684074], [14073293.638334833, 3889818.9772791504], [14073647.841086097, 3890139.226324991], [14074014.257724732, 3890459.4841204123], [14074356.24658839, 3890779.750664216], [14074710.449339252, 3891114.584135423], [14075052.438203165, 3891463.985782468], [14075369.999290047, 3891813.397846801], [14075699.774265824, 3892162.8203282068], [14076029.549241148, 3892526.813160914], [14076334.896440182, 3892890.817300508], [14076652.457527356, 3893254.8327489593], [14076933.376951266, 3893633.4208147195], [14077238.72415068, 3894012.0211142646], [14077519.643573463, 3894405.1959178024], [14077812.776884863, 3894798.383917827], [14078081.482420994, 3895191.5851198635], [14078337.974068029, 3895584.7995231976], [14078472.326835675, 3895788.693671682], [14078594.465715563, 3895992.591370282], [14078728.818483, 3896196.4926201487], [14078850.957363801, 3896400.3974202448], [14078960.88235518, 3896604.30577156], [14079083.021235045, 3896808.2176739727], [14079205.16011431, 3897012.1331286556], [14079327.298994398, 3897230.6179148587], [14079534.935089272, 3897623.9008055353], [14079766.998961411, 3897594.7682869313], [14079999.062833289, 3897551.069644458], [14080243.34059258, 3897521.9373066192], [14080475.404464027, 3897492.8050412447], [14080707.468334954, 3897449.106778766], [14080951.746094488, 3897419.9746949435], [14081196.023853954, 3897405.408680112], [14081415.87383798, 3897376.2767037847], [14081672.365485134, 3897361.710744355], [14081904.429357013, 3897332.5788773843], [14082136.49322842, 3897318.012970849], [14082380.770987837, 3897303.44708331], [14082625.048747523, 3897303.447082881], [14082844.89873114, 3897288.8812134634], [14083101.39037848, 3897288.8812134196], [14083333.454250371, 3897274.3153610313], [14083577.73200986, 3897274.315360886], [14083809.795881303, 3897274.3153613866], [14084054.07364039, 3897274.315361214], [14084298.35139951, 3897274.3153610793], [14084530.415272055, 3897288.881213491], [14084774.693031047, 3897288.881213693], [14085018.970789962, 3897303.447082888], [14085263.248550324, 3897303.4470835133], [14085483.09853329, 3897318.012971403], [14085727.376292782, 3897332.5788774174], [14085971.654052334, 3897361.71074422], [14086203.7179239, 3897376.276704187], [14086447.995682908, 3897405.4086796427], [14086692.27344248, 3897419.9746941905], [14086912.123426246, 3897449.1067788443], [14087156.401186522, 3897492.8050409877], [14087400.678945886, 3897521.9373064945], [14087620.528929327, 3897551.0696441643], [14087864.806688221, 3897594.7682873094], [14088109.084447999, 3897623.900805951], [14088341.148319451, 3897667.5997203756], [14088573.212191245, 3897696.7324211453], [14088805.276063038, 3897740.4316074788], [14089037.339933824, 3897784.1309570693], [14089281.617693743, 3897827.830470751], [14089501.4676769, 3897900.66335366], [14089745.745436855, 3897944.3633015817], [14089965.595419774, 3898002.630152866], [14090209.873179223, 3898060.897294051], [14090441.937050942, 3898119.164725707], [14090674.000922583, 3898162.8654892095], [14090906.064793834, 3898235.7004582426], [14091125.914777994, 3898308.535880703], [14091370.19253783, 3898381.3717555343], [14091590.04252131, 3898439.6407829127], [14091822.106392259, 3898512.4774739025], [14092041.956375973, 3898585.314618505], [14092274.020247314, 3898672.7197900466], [14092493.870230613, 3898745.557932266], [14092591.581335085, 3898774.693316213], [14092616.00911053, 3898760.125616015], [14092860.286870733, 3898701.8549935655], [14093080.136854356, 3898629.0171225015], [14093312.200724699, 3898570.7471534302], [14093556.478484493, 3898527.044866997], [14093776.328468569, 3898468.775404192], [14094020.606227417, 3898425.073498839], [14094264.883987851, 3898381.3717556526], [14094484.733970387, 3898337.670176473], [14094729.011730317, 3898293.9687599814], [14094961.075602157, 3898235.7004585327], [14095193.13947348, 3898206.566416765], [14095437.417232322, 3898162.865489851], [14095669.481104298, 3898133.7316290126], [14095901.544975886, 3898090.0309733814], [14096145.8227352, 3898060.897294544], [14096377.886607243, 3898031.7636878835], [14096622.164366543, 3898017.196910956], [14096866.442125635, 3897988.063412855], [14097086.292109383, 3897973.4966916144], [14097330.569869047, 3897944.3633026695], [14097574.847628593, 3897929.7966353055], [14097806.91150033, 3897915.2299849386], [14098051.189258894, 3897900.6633545416], [14098295.467019573, 3897886.0967414593], [14098515.317002017, 3897871.5301457075], [14098759.594761733, 3897871.5301461737], [14099003.872522173, 3897871.5301457415], [14099248.15028098, 3897842.3970105853], [14099480.214152526, 3897842.3970112065], [14099724.491911555, 3897842.3970108926], [14099968.769672029, 3897871.5301465685], [14100200.833543025, 3897871.530146231], [14100445.111302666, 3897871.530146376], [14100689.389062308, 3897886.0967411445], [14100909.23904563, 3897900.6633543223], [14101153.516805617, 3897915.229985139], [14101397.794564402, 3897929.796634782], [14101629.858436095, 3897944.363301649], [14101874.136195809, 3897973.4966918174], [14102118.413954498, 3897988.0634127976], [14102338.263938468, 3898017.1969116074], [14102582.541697871, 3898031.763687157], [14102826.819457443, 3898060.897294939], [14103046.669441475, 3898090.0309742764], [14103290.947201025, 3898133.731628701], [14103535.224959875, 3898162.865489382], [14103767.288831646, 3898206.566415829], [14104011.566590969, 3898235.7004580023], [14104243.63046214, 3898293.9687599214], [14104475.694334047, 3898337.6701762597], [14104707.758205285, 3898381.3717553043], [14104952.035964744, 3898425.0734980954], [14105171.885948928, 3898468.7754048845], [14105416.163708236, 3898527.044866273], [14105636.013691971, 3898570.7471529637], [14105880.291451601, 3898629.017122684], [14106100.141434586, 3898701.8549932986], [14106344.419193909, 3898760.1256155283], [14106576.483066218, 3898818.3965284377], [14106796.33304983, 3898891.2355768005], [14107040.61080865, 3898949.5071421904], [14107260.460792817, 3899022.347006401], [14107492.524664072, 3899109.7554428596], [14107712.374646941, 3899168.0280965483], [14107944.43851916, 3899240.8693216643], [14108164.288502041, 3899313.7110006236], [14108396.35237358, 3899401.1216131775], [14108616.202357315, 3899488.5328786755], [14108848.26622925, 3899575.9447972635], [14109068.116212262, 3899648.788562357], [14109300.180084735, 3899736.201678742], [14109507.81617953, 3899823.6154470327], [14109739.8800512, 3899925.5990036307], [14109959.730035394, 3900013.0141874147], [14110167.366130177, 3900100.430024312], [14110399.430001773, 3900202.4159939], [14110607.066097446, 3900318.9724758286], [14110826.91608077, 3900406.3905996806], [14111046.766064817, 3900508.3792349175], [14111254.402160756, 3900610.368760547], [14111462.038255833, 3900726.9293071674], [14111694.102127243, 3900828.9207385355], [14111901.738223149, 3900945.483462624], [14112109.374317858, 3901062.047348597], [14112317.010413814, 3901178.6123951804], [14112536.86039789, 3901295.1786045753], [14112732.282604866, 3901411.745975727], [14112939.91870098, 3901542.8856554995], [14113147.554796439, 3901659.455495108], [14113342.977004122, 3901776.026497272], [14113562.826987848, 3901907.170262538], [14113758.249194663, 3902023.7437324016], [14113965.885290999, 3902154.8902756744], [14114161.307498729, 3902300.6103812656], [14114552.151913268, 3902562.911149271], [14114942.996328058, 3902839.790564282], [14115333.840744248, 3903131.2496635215], [14115712.471271036, 3903408.142537925], [14116091.101798624, 3903714.189658899], [14116457.518437536, 3904020.244793169], [14116823.935076432, 3904326.30794037], [14117190.351715742, 3904646.9541180977], [14117544.554466687, 3904967.6090920256], [14117874.329442928, 3905288.272862804], [14118228.532193437, 3905623.52166774], [14118558.307168506, 3905973.3567619547], [14118900.296031933, 3906323.2023288426], [14119217.857119897, 3906687.6359336833], [14119535.41820684, 3907022.924889553], [14119852.979293982, 3907387.380320937], [14120146.112605767, 3907766.426031002], [14120329.320924878, 3907999.69104288], [14120659.095900508, 3908145.484040798], [14120891.159772767, 3908247.5402225223], [14121098.79586814, 3908364.1769507537], [14121306.43196353, 3908466.2350431], [14121538.49583534, 3908568.294027048], [14121746.13193052, 3908699.514031146], [14121953.768026086, 3908801.575054541], [14122161.404121136, 3908918.217315232], [14122381.254104782, 3909034.860741095], [14122588.890200352, 3909166.0859882417], [14122784.312407838, 3909282.7318893564], [14122991.948504105, 3909399.378956402], [14123187.370711256, 3909530.6082997248], [14123407.22069531, 3909661.8391170804], [14123602.64290326, 3909778.4899717756], [14123675.926230324, 3909822.2343429914], [14123871.348437805, 3909851.3973478205], [14124103.412310153, 3909909.7235756326], [14124335.476181583, 3909953.4684379986], [14124579.753940387, 3909997.2134634694], [14124799.603924207, 3910040.9586544754], [14125043.88168351, 3910099.2858296824], [14125263.731667727, 3910143.0314019676], [14125508.009426983, 3910201.3590867375], [14125752.28718599, 3910259.6870635124], [14125972.137169205, 3910332.59744405], [14126204.201041877, 3910390.92607536], [14126424.051024832, 3910449.254999375], [14126668.328784036, 3910507.584213994], [14126888.17876787, 3910565.913720107], [14127120.242639447, 3910638.8260132936], [14127340.092622804, 3910726.3213651967], [14127584.370382065, 3910799.234659938], [14127816.434254477, 3910872.148409907], [14128036.284238072, 3910945.0626162733], [14128268.348108647, 3911017.9772767853], [14128488.198092327, 3911090.892392731], [14128720.261964472, 3911192.974320413], [14128940.111948168, 3911280.4738266575], [14129147.748043166, 3911367.973988523], [14129379.811914971, 3911440.891291154], [14129599.661898108, 3911557.559922042], [14129819.511881288, 3911645.0621605986], [14130039.361864883, 3911732.5650563217], [14130271.425736733, 3911820.068606416], [14130479.061832469, 3911936.741027793], [14130686.697928369, 3912038.8303543963], [14130918.761799408, 3912140.920572288], [14131126.397895552, 3912243.0116843027], [14131334.033990381, 3912359.6883341745], [14131566.09786253, 3912461.7813591575], [14131773.73395724, 3912578.460196206], [14131981.370053304, 3912680.5551354536], [14132189.006148996, 3912811.821370374], [14132408.856131978, 3912913.918351353], [14132604.278340138, 3913030.6017091586], [14132811.914436067, 3913161.8718802584], [14133019.55053132, 3913278.557718527], [14133214.972738754, 3913409.8306803126], [14133422.608834058, 3913526.518996874], [14133618.03104185, 3913657.7947483873], [14133837.881025733, 3913789.0719763637], [14134033.303232925, 3913920.3506824095], [14134424.147647737, 3914197.4994676104], [14134814.99206299, 3914474.654837102], [14135205.836479066, 3914737.2291565286], [14135584.467006274, 3915043.5733327474], [14135950.883645028, 3915335.3371710163], [14136329.514172366, 3915641.697056373], [14136695.93081197, 3915948.0649892436], [14137050.133562554, 3916269.0305042304], [14137416.550201891, 3916590.0048536095], [14137758.539065152, 3916925.578393816], [14138112.741816988, 3917261.161591096], [14138124.955704955, 3917290.3431960098], [14138222.66680882, 3917363.297525027], [14138576.869559862, 3917669.710695894], [14138931.072311323, 3918005.315314095], [14139297.488950564, 3918326.3374690292], [14139627.263925616, 3918676.5535388705], [14139969.25278898, 3919012.187147011], [14140286.813875556, 3919362.423825106], [14140616.588851899, 3919712.671030513], [14140934.149938418, 3920077.5230633905], [14141251.711026246, 3920442.3865206414], [14141557.058225727, 3920821.8566365796], [14141850.191537393, 3921186.743403776], [14142143.324847814, 3921580.833950086], [14142412.030383276, 3921960.3411501986], [14142705.163695073, 3922354.4578704755], [14142973.869230365, 3922748.5879268395], [14143108.221998872, 3922952.9569106484], [14143230.360877866, 3923157.3294808976], [14143364.71364529, 3923347.1072222115], [14143486.852526005, 3923566.085382383], [14143621.205292745, 3923770.468714929], [14143743.344173217, 3923974.8556338386], [14143853.269164307, 3924179.246141352], [14143975.40804453, 3924398.239951181], [14144085.333036179, 3924602.6378918802], [14144207.471916584, 3924807.039420669], [14144329.61079611, 3925011.4445379367], [14144427.321899917, 3925245.054782049], [14144537.246891692, 3925449.467591055], [14144659.38577103, 3925668.485299123], [14144757.096875027, 3925872.905545292], [14144867.021866404, 3926091.931221066], [14144976.946858248, 3926310.9610188995], [14145062.444074264, 3926529.9949393696], [14145172.369066445, 3926749.032982101], [14145257.866282122, 3926968.07514812], [14145343.36349802, 3927187.1214373973], [14145453.288489206, 3927420.7753580655], [14145538.785705116, 3927639.830169832], [14145636.496809188, 3927858.8891063603], [14145709.78013733, 3928077.9521672083], [14145831.91901659, 3928194.7874860945], [14146173.907880068, 3928516.090663645], [14146528.11063154, 3928837.402716595], [14146857.885606105, 3929187.935079745], [14147199.87446994, 3929538.4780067294], [14147529.649444804, 3929874.424892384], [14147847.210531974, 3930224.988511853], [14148164.771620288, 3930590.1701873746], [14148470.118818687, 3930955.363331506], [14148763.252130508, 3931335.176369344], [14149068.599329932, 3931715.0018159356], [14149349.518753031, 3932094.839672609], [14149642.652064433, 3932489.2998140086], [14149911.357600631, 3932869.1629744656], [14150167.84924795, 3933263.649396584], [14150302.202015493, 3933482.814306461], [14150436.554783072, 3933672.760570551], [14150558.69366279, 3933891.9331968473], [14150693.04643046, 3934081.88614829], [14150815.185310263, 3934301.0664916416], [14150937.324189857, 3934505.638541194], [14151059.463069875, 3934710.2141920165], [14151169.388061073, 3934914.793446248], [14151279.313053045, 3935133.9895019485], [14151401.45193336, 3935338.576218246], [14151499.163036935, 3935557.7802698156], [14151621.301916642, 3935762.3744509714], [14151731.226908179, 3935981.586499216], [14151841.151899958, 3936186.188145459], [14151938.863003807, 3936405.408193718], [14152195.354651982, 3936726.938415809], [14152488.487962838, 3937121.555854616], [14152757.193498401, 3937516.186704223], [14153013.685146198, 3937910.830965573], [14153148.037913712, 3938130.0835739793], [14153282.39068159, 3938320.1058491296], [14153404.52956081, 3938539.3661864866], [14153526.668440903, 3938729.3951621894], [14153661.021208057, 3938934.045228578], [14153783.16008862, 3939153.3171624425], [14153893.085079862, 3939343.356187306], [14154003.010072157, 3939562.635853141], [14154125.148951644, 3939767.300611019], [14154247.287830727, 3939986.588286231], [14154344.998935373, 3940191.260520088], [14154454.923926366, 3940410.5562055036], [14154577.062806187, 3940615.235917028], [14154686.987798307, 3940834.5396143007], [14154784.698902179, 3941053.847455504], [14154894.623893438, 3941273.159443157], [14154980.121109651, 3941477.8543701675], [14155090.046101721, 3941711.795852182], [14155175.543317659, 3941916.4985195934], [14155285.468309484, 3942150.4488461153], [14155370.965524651, 3942369.7815635717], [14155468.676628448, 3942589.1184271937], [14155554.173845042, 3942808.4594375123], [14155639.671059819, 3943042.427754295], [14155725.168275682, 3943261.7773370524], [14155810.665492112, 3943481.131068717], [14155883.948820263, 3943700.4889500365], [14155957.232147578, 3943934.475261892], [14156030.51547582, 3944153.84171713], [14156116.012691723, 3944387.8371760393], [14156189.296019575, 3944621.8373581613], [14156262.579347137, 3944855.842261796], [14156335.86267503, 3945075.226148344], [14156396.932115378, 3945309.240202267], [14156458.001554107, 3945543.2589806733], [14156531.284882791, 3945777.2824817533], [14156580.140434783, 3945996.683805485], [14156641.209874306, 3946245.343659381], [14156702.279314281, 3946479.3813347146], [14156763.348753486, 3946713.4237364368], [14156836.632082347, 3947093.7527188975], [14156922.129297458, 3947210.779532316], [14157044.268176915, 3947415.5792986215], [14157166.407056939, 3947620.3826843738], [14157300.759824937, 3947825.1896898304], [14157422.89870435, 3948030.000315225], [14157545.037583863, 3948234.8145606047], [14157667.17646415, 3948454.2624128303], [14157764.887568416, 3948659.084157569], [14157887.026448287, 3948878.5400449373], [14157996.951438919, 3949083.369290671], [14158119.090319661, 3949288.202157963], [14158216.801423091, 3949507.6699629608], [14158326.726414407, 3949727.1419266723], [14158424.437519114, 3949931.986177989], [14158534.362510465, 3950166.098329143], [14158644.287502103, 3950370.950342997], [14158729.78471748, 3950590.4386638855], [14158815.281933218, 3950809.931145622], [14158925.206925157, 3951029.427785851], [14159010.704141628, 3951248.9285875857], [14159108.415245011, 3951483.0673614168], [14159193.912461305, 3951702.576762513], [14159291.6235645, 3951936.724709892], [14159364.906892387, 3952141.6080482737], [14159450.404108545, 3952375.764874187], [14159523.687435796, 3952595.291199293], [14159621.398540307, 3952829.4572015903], [14159694.681868514, 3953048.992130602], [14159767.965195222, 3953283.1673107734], [14159841.248523328, 3953502.71084457], [14159914.531851163, 3953751.5318834265], [14159975.601291291, 3953971.084300843], [14160036.670731131, 3954205.278137325], [14160097.740171447, 3954424.8391631977], [14160171.023498941, 3954673.6800276935], [14160232.092939438, 3954893.249940999], [14160293.162378157, 3955127.4624392823], [14160342.017930873, 3955361.679679122], [14160403.087371092, 3955610.540691105], [14160451.942922773, 3955830.128382198], [14160500.798474764, 3956064.3598468173], [14160549.65402654, 3956298.596053535], [14160598.509577785, 3956547.477220151], [14160622.937354516, 3956693.880406732], [14160636.617809776, 3956751.2753614364], [14160696.220681982, 3957001.3331325892], [14160757.290121438, 3957220.947224554], [14160818.359561661, 3957469.8482368095], [14160867.215113139, 3957704.112907182], [14160916.070665386, 3957938.3823223934], [14160952.712329246, 3958172.656482196], [14161001.567881363, 3958406.9353898573], [14161050.423433455, 3958641.2190423324], [14161099.278985005, 3958890.1506262408], [14161135.920648871, 3959109.800590544], [14161172.562312365, 3959358.7422623085], [14161209.203976428, 3959593.0452026036], [14161245.845640494, 3959827.352890803], [14161270.273416875, 3960061.6653290996], [14161306.915080808, 3960310.627497995], [14161319.128968159, 3960544.9497317653], [14161343.55674493, 3960779.276716447], [14161380.198409086, 3961013.6084507345], [14161392.412296837, 3961262.5911258464], [14161404.626184527, 3961511.5791632985], [14161429.053960387, 3961745.92574739], [14161441.267848562, 3961994.924199761], [14161453.481735952, 3962214.63317148], [14161453.481736058, 3962463.6417239243], [14161465.695623817, 3962698.007616471], [14161465.695623918, 3962947.0265865847], [14161490.123400616, 3963181.402284813], [14161490.123400327, 3963430.4316741815], [14161490.12340009, 3963664.817180731], [14161490.123400327, 3963913.856991753], [14161465.695623929, 3964148.2523065866], [14161453.481736246, 3964382.6523781596], [14161453.481736366, 3964631.7076659855], [14161441.267847814, 3964866.11754977], [14161429.053960953, 3965100.5321910167], [14161429.053960415, 3965349.602961888], [14161392.412296638, 3965584.027417495], [14161380.198408043, 3965833.1086162445], [14161343.556744935, 3966067.542888706], [14161343.556744233, 3966155.4569668346], [14161331.342857195, 3966316.6345174764], [14160989.35399289, 3968675.9452985157], [14160366.445706693, 3971035.738514718], [14159609.18465247, 3973249.3995169974], [14158717.57082961, 3975478.1498437845], [14157471.754256403, 3977560.6613335563], [14155041.190549271, 3980831.8443643325], [14152329.70741956, 3983619.6844641836], [14151987.71855616, 3983825.1309771356], [14148885.391009744, 3986188.029616028], [14146809.030054526, 3987362.3212974994], [14144659.385770556, 3988331.2022260944], [14142448.672047747, 3989109.3020907817], [14140018.108340502, 3989667.217435894], [14137673.041849403, 3990078.3302842528], [14136695.930811903, 3990151.744841114], [14135095.911487218, 3991458.6024279883], [14131847.017286118, 3993896.5093908194], [14128537.053643916, 3995644.4841260226], [14126998.103759484, 3996261.479934003], [14128940.111947352, 3996599.372644142], [14130882.120135639, 3997025.425458683], [14132824.128323132, 3997436.8018056713], [14133581.38937842, 3997642.4955095113], [14135169.19481456, 3997995.1218662434], [14137111.20300309, 3998215.51884181], [14138979.92786301, 3998421.2265061126], [14140848.652722916, 3998685.713208066], [14142790.66091092, 3998906.123450371], [14144732.669099433, 3999170.621330323], [14146601.39395857, 3999317.5672330167], [14148543.402146455, 3999596.769632924], [14150412.12700758, 3999875.9788301843], [14152329.70741913, 4000081.7162713646], [14154271.715607245, 4000302.153339294], [14155041.190549305, 4000360.9372721342], [14156983.19873771, 4000640.165071943], [14158912.993037248, 4000919.399671307], [14160781.717897676, 4001125.1558314012], [14162797.009413889, 4001345.612956999], [14164580.237058092, 4001610.1671023793], [14166522.245245533, 4001815.935658166], [14167120.725757152, 4001933.519348061], [14167987.911802663, 4002036.4060658], [14173362.02251159, 4002697.8427284476], [14178760.46447004, 4003210.7735754605], [14183572.832858339, 4003668.0188718894], [14206901.358889744, 4005932.0826477716], [14210370.103074845, 4006505.5204501776], [14216550.330390768, 4008623.077985625], [14220983.971725512, 4010961.6694398993], [14225038.982532345, 4014271.8137577237], [14230608.515449705, 4021896.1459267535], [14237533.789931284, 4030556.9660328445], [14248477.43355596, 4044607.966866741], [14248819.422419427, 4044725.9920548676], [14248894.821504684, 4044751.2460003477], [14332384.43799789, 4072715.0099619655], [14339458.685080042, 4075084.437250298], [14339432.408530401, 4075075.6362608722], [14347742.667809354, 4070231.176358625], [14347779.925183792, 4070274.6906909533], [14337204.573632397, 4057923.327535437], [14332384.437499993, 4053484.2600356997], [14325516.027051724, 4047158.849973145], [14297318.800024424, 4007338.115534628], [14296205.605126167, 4000638.8920895318], [14289147.949369663, 3995496.544127517], [14248894.821533248, 3948046.126943193], [14240260.255669821, 3937867.6942487117], [14234601.398877025, 3931197.0296311476], [14233310.092817476, 3925646.324512699], [14227187.520809716, 3914566.141529868], [14215309.731123524, 3889770.2929695556], [14213829.181949753, 3888669.1786231115], [14154273.254398886, 3837917.649432496], [14085910.608311396, 3787395.72363925], [14026312.49388151, 3787395.72363925]]]]}}]} \ No newline at end of file diff --git a/frontend/src/data/zones/특정어업수역Ⅲ.json b/frontend/src/data/zones/특정어업수역Ⅲ.json new file mode 100644 index 0000000..186078b --- /dev/null +++ b/frontend/src/data/zones/특정어업수역Ⅲ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed3", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2162", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[13817590.293393573, 4163976.6556012244], [13935718.55954324, 4163976.6556012244], [13935619.01320107, 4163881.1438622964], [13923844.505073382, 4152583.8553656973], [13918348.255484505, 4147059.076131751], [13915673.414018149, 4144230.73373971], [13914293.244677586, 4142310.8465966308], [13912412.305929314, 4139320.052336007], [13910836.71437977, 4135854.113539339], [13910055.02554902, 4132998.86846704], [13908345.081232697, 4127825.5937968497], [13907722.172945773, 4125120.982583305], [13907392.39797139, 4121971.379578588], [13905548.100886794, 4109218.5915996092], [13903996.937114129, 4098184.7930658716], [13902433.559452614, 4086362.162986858], [13899270.162467, 4063581.503750727], [13896473.182120506, 4043914.5936157014], [13897865.565350175, 4034226.54128085], [13899148.023587234, 4029334.0367157785], [13901432.020639004, 4024841.2655827063], [13904314.498200562, 4020453.314503754], [13908809.208975887, 4016346.6591829173], [13913084.069767442, 4012962.3532931497], [13916381.819520816, 4010770.4459119253], [13921743.716342429, 4008961.335245877], [13958947.219114931, 4002242.1822701376], [13979370.589551304, 3998601.864321506], [14011454.723518057, 3992883.0996783567], [14012407.40678011, 3992706.863639312], [14013372.303930415, 3992530.6303036474], [14014324.987193013, 3992369.0854542954], [14015277.670454111, 3992192.8573017977], [14016230.35371629, 3992031.3172021704], [14017195.250866242, 3991855.094230278], [14018147.934129067, 3991678.873962591], [14019100.617390765, 3991502.656396452], [14020053.300652908, 3991341.126002646], [14021018.197802687, 3991179.5978787914], [14021909.811624419, 3991018.0720274393], [14022813.639334908, 3990856.5484449966], [14023766.32259687, 3990650.976261227], [14024731.219746705, 3990445.407755316], [14025525.122466035, 3990298.575359531], [14026477.805727897, 3990063.6474289736], [14027442.702878024, 3989828.724300673], [14027845.761180786, 3989740.629365874], [14028798.444442954, 3989505.7128408654], [14029763.341592584, 3989270.80111587], [14030716.024854451, 3989050.57573502], [14031668.70811654, 3988815.6733126394], [14033073.30523366, 3988478.0094901477], [14033024.449681701, 3988213.757764475], [14033000.021905882, 3987949.512112862], [14032963.380242558, 3987582.5143390666], [14032938.952466676, 3987332.9625437283], [14032914.524689825, 3987098.09521391], [14032902.310802005, 3986863.232680407], [14032877.883026825, 3986613.69649413], [14032877.883025693, 3986378.843854627], [14032841.241362942, 3986143.996010515], [14032841.241361871, 3985894.47543004], [14032829.027473792, 3985659.6374763353], [14032829.027474709, 3985410.127403529], [14032829.027473787, 3985175.2993370066], [14032829.02747455, 3984940.4760656278], [14032816.813586919, 3984690.9815914035], [14032829.027474267, 3984456.1682059844], [14032829.027474323, 3984221.3596118884], [14032829.02747492, 3983971.8807323813], [14032841.24136266, 3983737.082022118], [14032841.24136211, 3983487.613641918], [14032865.66913836, 3983252.8248136323], [14032877.88302635, 3983003.3669318845], [14032902.310802022, 3982783.2615266712], [14032914.52469013, 3982533.813822452], [14032938.952465724, 3982299.044452602], [14032963.38024159, 3982064.2798706265], [14032975.59412962, 3981814.847749137], [14033012.23579437, 3981594.76507107], [14033036.663569707, 3981345.3431236055], [14033073.305233562, 3981110.597991455], [14033097.733010307, 3980861.18653368], [14033146.588561533, 3980641.122086566], [14033171.016337542, 3980391.7207993036], [14033219.871889306, 3980171.665326035], [14033268.727441864, 3979922.2742063804], [14033305.369105555, 3979687.5580867655], [14033354.224656736, 3979452.846750307], [14033403.080208756, 3979218.140198897], [14033464.149649367, 3978968.769728522], [14033500.79131351, 3978748.741443538], [14033561.860753022, 3978514.0492399754], [14033622.930192148, 3978279.3618192296], [14033683.999632282, 3978044.6791785043], [14033745.06907279, 3977824.6685465015], [14033806.138512583, 3977575.3282415215], [14033867.207951993, 3977355.3265721146], [14033940.49128036, 3977120.6627559136], [14034013.774608184, 3976886.0037188698], [14034074.84404724, 3976651.349460947], [14034148.127375823, 3976431.365434353], [14034233.624591732, 3976211.385607544], [14034306.907918751, 3975976.745086343], [14034380.191247718, 3975756.7739374605], [14034465.688463435, 3975522.1426732005], [14034563.399566704, 3975302.1802014867], [14034636.682894846, 3975067.5581912696], [14034722.18011089, 3974862.2678504367], [14034819.89121484, 3974627.654795317], [14034905.388430584, 3974407.7093927874], [14035015.313421464, 3974187.7681863704], [14035100.810637798, 3973967.831176343], [14035198.52174194, 3973747.8983624075], [14035296.232845142, 3973527.9697442516], [14035406.157837268, 3973322.706818716], [14035503.868941093, 3973102.7863105624], [14035564.938380616, 3972970.836018713], [14035430.585612448, 3972882.8699968406], [14035051.955086311, 3972589.6547698523], [14034685.538446855, 3972281.7868043673], [14034306.907918967, 3971988.5868595946], [14033928.277392257, 3971680.7349405857], [14033561.860752953, 3971358.232219437], [14033207.658001786, 3971050.3971231086], [14032865.66913857, 3970727.9120247746], [14032511.46638746, 3970376.1203815714], [14032169.477523897, 3970053.654131018], [14031851.916436315, 3969701.8830453446], [14031522.141461194, 3969350.1226848057], [14031192.36648596, 3969013.029068952], [14030874.80539861, 3968646.634131671], [14030569.4581991, 3968265.5957370186], [14030276.324888123, 3967899.224527907], [14029970.977688076, 3967518.2108068704], [14029677.844376301, 3967137.2096621543], [14029409.138841135, 3966756.2210914562], [14029140.433306115, 3966360.592421976], [14028871.727769978, 3965964.977308897], [14028737.375003058, 3965759.848882349], [14028615.236123221, 3965554.724099639], [14028480.883355275, 3965349.6029609945], [14028358.744475357, 3965144.4854657836], [14028224.391707785, 3964939.371614421], [14028102.252828015, 3964734.2614048333], [14027980.11394778, 3964529.1548381275], [14027857.975069236, 3964324.051914157], [14027760.26396438, 3964104.302822176], [14027638.125085097, 3963899.207441546], [14027528.200093549, 3963679.4664326143], [14027406.061214069, 3963474.378595102], [14027308.350109257, 3963254.6456658943], [14027198.425118413, 3963049.5653696535], [14027088.500126269, 3962815.1923459424], [14027003.00291053, 3962610.11984976], [14026893.077918677, 3962390.4033569284], [14026795.366814636, 3962170.6910431627], [14026697.655711418, 3961965.629985329], [14026599.944607599, 3961731.278946997], [14026502.233502936, 3961511.5791640463], [14026416.736287547, 3961291.883556766], [14026331.239071513, 3961072.192127874], [14026245.741855916, 3960837.859204179], [14026148.030752573, 3960603.531032333], [14026050.319648793, 3960515.6591924117], [14025696.116896924, 3960208.113014822], [14025354.12803417, 3959885.930555696], [14024987.711394375, 3959549.113039522], [14024645.722530954, 3959226.948944662], [14024291.51977964, 3958890.150626061], [14023961.74480434, 3958538.7193601266], [14023644.183716903, 3958172.6564824986], [14023314.408742214, 3957835.8888684427], [14023009.061542125, 3957469.8482367387], [14022703.714343427, 3957103.8191892453], [14022398.367143923, 3956723.1612672796], [14022117.447720738, 3956342.515870507], [14021812.100520544, 3955947.2435213765], [14021543.394986114, 3955566.6236525774], [14021250.261673959, 3955171.377811075], [14020993.770026248, 3954776.145468736], [14020859.417258942, 3954556.577778357], [14020737.27837941, 3954366.2891494785], [14020602.925611844, 3954176.0036486983], [14020480.786732143, 3953956.4473441243], [14020370.861740284, 3953751.53188359], [14020248.722860433, 3953546.620051208], [14020126.583980937, 3953341.7118456624], [14020004.445100708, 3953122.171365647], [14019882.306221237, 3952917.270673172], [14019784.595117368, 3952697.7382422695], [14019662.456236959, 3952492.8450626153], [14019552.531245224, 3952273.3206794215], [14019442.606254242, 3952068.435010681], [14019344.89515069, 3951834.2844000966], [14019234.97015812, 3951629.406499703], [14019137.259054784, 3951424.532224624], [14019027.33406315, 3951190.394634004], [14018941.836846726, 3950985.528125728], [14018831.911855552, 3950751.3994105365], [14018734.200752072, 3950546.540667035], [14018648.703535682, 3950312.4208263634], [14018563.206319205, 3950092.937773037], [14018465.495216299, 3949858.827100896], [14018379.99800022, 3949639.352642628], [14018306.71467212, 3949405.251136331], [14018209.003568063, 3949185.785271415], [14018135.72024025, 3948951.692931064], [14018062.43691314, 3948732.2356575206], [14017989.153585482, 3948498.1524819196], [14017915.87025767, 3948278.7037986964], [14017842.586929433, 3948044.6297840844], [14017757.089713955, 3947825.1896894635], [14017696.02027415, 3947591.124836192], [14017622.736946309, 3947371.6933298754], [14017561.667506203, 3947123.0093109384], [14017500.598066194, 3946888.958639055], [14017451.742514167, 3946654.9126930023], [14017390.673074305, 3946420.8714734227], [14017329.603633871, 3946157.5807487303], [14017195.25086627, 3945791.9091077414], [14017109.753650622, 3945557.885310957], [14017036.470322493, 3945338.4922908037], [14016963.186994748, 3945104.477646201], [14016889.903667673, 3944885.0932069574], [14016816.620339664, 3944651.0877120267], [14016743.337011732, 3944431.7118500136], [14016682.267571904, 3944197.715505999], [14016621.198132234, 3943963.7238824223], [14016547.914804032, 3943729.736980628], [14016486.845363976, 3943510.378546276], [14016425.775924595, 3943261.7773378296], [14016364.70648495, 3943042.427753915], [14016315.850932516, 3942793.8365748394], [14016254.781492097, 3942574.4958398864], [14016193.712052723, 3942325.9146883073], [14016157.070389032, 3942106.582801053], [14016108.214837069, 3941858.0116748144], [14016059.359284986, 3941624.0672444003], [14016034.931508765, 3941390.1275309804], [14015986.075957134, 3941141.571752944], [14015937.22040512, 3940922.2622533315], [14015912.792629696, 3940673.7164975833], [14015876.150965236, 3940439.7959427596], [14015839.509301143, 3940191.260519465], [14015802.867637865, 3939971.9689781107], [14015790.653749412, 3939723.443572689], [14015754.012085313, 3939489.542170439], [14015729.584309753, 3939241.0270946748], [14015717.37042194, 3939007.1354134823], [14015680.728758091, 3938758.630664511], [14015668.514870305, 3938510.131235958], [14015656.300982298, 3938290.871450771], [14015644.087093962, 3938042.3820334156], [14015644.087093726, 3937808.5144985737], [14015619.659317939, 3937560.035403366], [14015619.659317758, 3937326.1775837634], [14015607.445430323, 3937077.708810477], [14015607.445430165, 3936843.86070301], [14015607.44543046, 3936610.017304439], [14015607.445429819, 3936361.563852228], [14015619.659318132, 3936127.730163849], [14015619.65931785, 3935879.2870282456], [14015619.659318443, 3935864.672891335], [14015644.087093579, 3935645.4630488665], [14015644.087094065, 3935397.030227534], [14015656.300982174, 3935163.215955291], [14015668.51486993, 3934914.7934458014], [14015680.728758322, 3934680.988878479], [14015717.370422252, 3934432.576680492], [14015729.584309453, 3934213.393858009], [14015754.012085263, 3933964.991656822], [14015790.653749326, 3933716.5947647644], [14015802.867637549, 3933482.8143059933], [14015839.509301828, 3933234.4277204718], [14015863.937076895, 3933000.6569608934], [14015912.79262938, 3932766.8909023306], [14015937.220405562, 3932533.1295448784], [14015986.075957565, 3932299.3728892817], [14016010.503732808, 3932065.6209347122], [14016059.359284472, 3931817.2646324276], [14016108.214836905, 3931583.522371577], [14016169.284276439, 3931349.784810239], [14016205.925940333, 3931101.4438007176], [14016254.781492744, 3930882.3237842843], [14016315.85093268, 3930633.992758967], [14016364.706484258, 3930414.8815508736], [14016425.77592429, 3930166.5605083024], [14016486.845364062, 3929947.45810794], [14016547.91480444, 3929699.147045077], [14016621.19813177, 3929480.053450812], [14016682.267571429, 3929246.358166313], [14016743.337011438, 3929012.667577336], [14016816.620339306, 3928793.5869140886], [14016889.903667254, 3928545.3004834815], [14016963.186994506, 3928326.228621952], [14017036.470323365, 3928092.5565168317], [14017109.753650554, 3927873.4931812496], [14017195.250866888, 3927639.8301689653], [14017280.748082507, 3927420.7753582653], [14017354.031410536, 3927187.121436862], [14017451.742513658, 3926982.678106103], [14017537.239729747, 3926749.032981505], [14017622.736945918, 3926529.994938449], [14017720.448049057, 3926296.358903417], [14017818.159152932, 3926091.931220958], [14017915.870256566, 3925858.3039796036], [14018001.367473086, 3925653.883990288], [14018111.292464526, 3925420.265541152], [14018209.00356852, 3925201.2525036694], [14018318.928560698, 3924996.844052992], [14018428.853552194, 3924763.2387903365], [14018526.564656094, 3924558.8380302167], [14018636.489648066, 3924339.8411988374], [14018758.628527954, 3924135.447872815], [14018856.339631462, 3923916.459004483], [14018978.478511153, 3923712.073110865], [14019100.617391173, 3923493.0922050807], [14019222.756270064, 3923288.713741847], [14019344.895150287, 3923084.338865018], [14019467.034029689, 3922879.9675755682], [14019589.172909968, 3922661.0023167264], [14019723.525677131, 3922471.235755037], [14019833.45066894, 3922252.2781808744], [14019980.017325126, 3922062.518278752], [14020114.370092534, 3921872.7614680813], [14020383.075627644, 3921464.06499056], [14020651.781162936, 3921084.5739564244], [14020932.700586699, 3920690.500196857], [14021225.833898693, 3920311.0343588293], [14021518.967209466, 3919931.5808786163], [14021824.31440906, 3919552.1397545147], [14021873.169961166, 3919508.3588811457], [14022007.522728465, 3919158.1178169283], [14022117.447720494, 3918939.222496516], [14022215.158823974, 3918720.331287067], [14022312.869927935, 3918486.851860621], [14022410.581031999, 3918282.5611990155], [14022520.506023144, 3918063.6823207946], [14022618.217127495, 3917844.807552783], [14022728.14211945, 3917625.9368926086], [14022838.06711147, 3917421.6613179413], [14022960.205990292, 3917202.798602683], [14023057.917095127, 3916998.53044049], [14023180.055974487, 3916779.675667872], [14023302.194854327, 3916575.4149192595], [14023424.333733585, 3916356.568086974], [14023546.472613554, 3916166.904154751], [14023656.397605527, 3915948.0649889517], [14023778.536485165, 3915758.4077001447], [14023912.889252687, 3915539.5761998114], [14024035.028132746, 3915335.337170445], [14024303.73366759, 3914941.4577129018], [14024560.225315124, 3914547.5915551204], [14024853.358626463, 3914153.7386953356], [14025134.278049812, 3913759.899131608], [14025427.411361214, 3913380.6587829227], [14025720.544672925, 3913001.430759424], [14026025.891872894, 3912636.800052067], [14026272.928939708, 3912341.809856742], [14026312.49388151, 3787395.72363925], [13961312.04903013, 3787395.72363925], [13947389.033479117, 3802232.5362782693], [13947389.033479117, 3802232.5362782683], [13822584.485054709, 3935228.2723445967], [13818455.7465445, 3939627.988734125], [13804540.810181938, 4028802.026185181], [13817531.794596978, 4163881.1427735523], [13817531.794716273, 4163881.144013962], [13817590.293393573, 4163976.6556012244]]]]}}]} \ No newline at end of file diff --git a/frontend/src/data/zones/특정어업수역Ⅳ.json b/frontend/src/data/zones/특정어업수역Ⅳ.json new file mode 100644 index 0000000..1ce6f88 --- /dev/null +++ b/frontend/src/data/zones/특정어업수역Ⅳ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed4", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2163", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[13859276.603817873, 4232038.462456921], [13859276.603762543, 4321218.244482412], [13859276.603710985, 4404317.064005076], [13840719.645028654, 4439106.786523586], [13884632.712472571, 4439106.787250583], [13884632.712472571, 4439504.084564682], [13940418.269436067, 4439504.375880923], [13969123.924724836, 4439504.525783945], [13968718.329494288, 4438626.439593866], [13962623.599395147, 4425543.915710401], [13960437.31344761, 4420657.3891166765], [13958238.813611617, 4416093.569832627], [13958143.094601436, 4415900.994484875], [13958143.094601437, 4415900.994484875], [13957298.344237303, 4414201.456484755], [13953878.455604602, 4406316.186534493], [13949652.450365951, 4397019.979821594], [13948553.200448176, 4393395.13065616], [13947612.731073817, 4389132.176741289], [13947612.731072996, 4387549.226905922], [13947466.164417507, 4385829.556682826], [13947783.725505754, 4381721.729468383], [13948260.06713652, 4379835.70012994], [13949359.317054221, 4375897.403884492], [13951093.689146286, 4371808.582233328], [13954867.780530114, 4365670.678186072], [13964809.885341855, 4351190.629491161], [13978342.873219142, 4331838.456925102], [13980382.592510404, 4329007.496874151], [13981728.043604897, 4327079.749205159], [13985775.34591557, 4321280.81855131], [13997066.763484716, 4305102.598482491], [13999424.043863578, 4300225.286038025], [14003039.354703771, 4290447.064438686], [14005091.287883686, 4284626.561498255], [14006520.312777169, 4279426.932176922], [14007631.77658257, 4275178.643476352], [14008242.470981453, 4271549.325573796], [14009378.362562515, 4262248.123573576], [14009427.990871342, 4261704.85208626], [14009708.137538105, 4258638.140769343], [14009854.704193696, 4257224.555715567], [14009378.362562606, 4254698.603440943], [14005347.779531531, 4240996.452433007], [14002367.590864772, 4231511.1380338315], [14001280.554835469, 4227266.412716273], [14000486.652116666, 4225212.134400094], [13998047.81589918, 4222926.459154359], [13991387.305576058, 4216684.234498038], [13970721.407121927, 4197120.494488488], [13958654.085803084, 4185745.4565721145], [13956602.15262321, 4184012.5742896623], [13944065.033685392, 4171984.566055202], [13940467.606607554, 4168533.224265296], [13935619.01320107, 4163881.1438622964], [13935718.55954324, 4163976.6556012244], [13817590.293393573, 4163976.6556012244], [13859276.603817873, 4232038.462456921]]]]}}]} \ No newline at end of file diff --git a/frontend/src/hooks/useAnalysisDeckLayers.ts b/frontend/src/hooks/useAnalysisDeckLayers.ts new file mode 100644 index 0000000..07d16c8 --- /dev/null +++ b/frontend/src/hooks/useAnalysisDeckLayers.ts @@ -0,0 +1,187 @@ +import { useMemo } from 'react'; +import { ScatterplotLayer, TextLayer } from '@deck.gl/layers'; +import type { Layer } from '@deck.gl/core'; +import type { Ship, VesselAnalysisDto } from '../types'; + +interface AnalyzedShip { + ship: Ship; + dto: VesselAnalysisDto; +} + +// RISK_RGBA: [r, g, b, a] 충전색 +const RISK_RGBA: Record = { + CRITICAL: [239, 68, 68, 60], + HIGH: [249, 115, 22, 50], + MEDIUM: [234, 179, 8, 40], +}; + +// 테두리색 +const RISK_RGBA_BORDER: Record = { + CRITICAL: [239, 68, 68, 230], + HIGH: [249, 115, 22, 210], + MEDIUM: [234, 179, 8, 190], +}; + +// 픽셀 반경 +const RISK_SIZE: Record = { + CRITICAL: 18, + HIGH: 14, + MEDIUM: 12, +}; + +const RISK_LABEL: Record = { + CRITICAL: '긴급', + HIGH: '경고', + MEDIUM: '주의', +}; + +const RISK_PRIORITY: Record = { + CRITICAL: 0, + HIGH: 1, + MEDIUM: 2, +}; + +/** + * 분석 결과 기반 deck.gl 레이어를 반환하는 훅. + * AnalysisOverlay DOM Marker 대체 — GPU 렌더링으로 성능 향상. + */ +export function useAnalysisDeckLayers( + analysisMap: Map, + ships: Ship[], + activeFilter: string | null, + sizeScale: number = 1.0, +): Layer[] { + return useMemo(() => { + if (analysisMap.size === 0) return []; + + const analyzedShips: AnalyzedShip[] = ships + .filter(s => analysisMap.has(s.mmsi)) + .map(s => ({ ship: s, dto: analysisMap.get(s.mmsi)! })); + + const riskData = analyzedShips + .filter(({ dto }) => { + const level = dto.algorithms.riskScore.level; + return level === 'CRITICAL' || level === 'HIGH' || level === 'MEDIUM'; + }) + .sort((a, b) => { + const pa = RISK_PRIORITY[a.dto.algorithms.riskScore.level] ?? 99; + const pb = RISK_PRIORITY[b.dto.algorithms.riskScore.level] ?? 99; + return pa - pb; + }) + .slice(0, 100); + + const layers: Layer[] = []; + + // 위험도 원형 마커 + layers.push( + new ScatterplotLayer({ + id: 'risk-markers', + data: riskData, + getPosition: (d) => [d.ship.lng, d.ship.lat], + getRadius: (d) => (RISK_SIZE[d.dto.algorithms.riskScore.level] ?? 12) * sizeScale, + getFillColor: (d) => RISK_RGBA[d.dto.algorithms.riskScore.level] ?? [100, 100, 100, 40], + getLineColor: (d) => RISK_RGBA_BORDER[d.dto.algorithms.riskScore.level] ?? [100, 100, 100, 200], + stroked: true, + filled: true, + radiusUnits: 'pixels', + lineWidthUnits: 'pixels', + getLineWidth: 2, + }), + ); + + // 위험도 라벨 (선박명 + 위험도 등급) + layers.push( + new TextLayer({ + id: 'risk-labels', + data: riskData, + getPosition: (d) => [d.ship.lng, d.ship.lat], + getText: (d) => { + const label = RISK_LABEL[d.dto.algorithms.riskScore.level] ?? d.dto.algorithms.riskScore.level; + const name = d.ship.name || d.ship.mmsi; + return `${name}\n${label} ${d.dto.algorithms.riskScore.score}`; + }, + getSize: 10 * sizeScale, + getColor: (d) => RISK_RGBA_BORDER[d.dto.algorithms.riskScore.level] ?? [200, 200, 200, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 16], + fontFamily: 'monospace', + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + + // 다크베셀 (activeFilter === 'darkVessel' 일 때만) + if (activeFilter === 'darkVessel') { + const darkData = analyzedShips.filter(({ dto }) => dto.algorithms.darkVessel.isDark); + + if (darkData.length > 0) { + layers.push( + new ScatterplotLayer({ + id: 'dark-vessel-markers', + data: darkData, + getPosition: (d) => [d.ship.lng, d.ship.lat], + getRadius: 12 * sizeScale, + getFillColor: [168, 85, 247, 40], + getLineColor: [168, 85, 247, 200], + stroked: true, + filled: true, + radiusUnits: 'pixels', + lineWidthUnits: 'pixels', + getLineWidth: 2, + }), + ); + + // 다크베셀 gap 라벨 + layers.push( + new TextLayer({ + id: 'dark-vessel-labels', + data: darkData, + getPosition: (d) => [d.ship.lng, d.ship.lat], + getText: (d) => { + const gap = d.dto.algorithms.darkVessel.gapDurationMin; + return gap > 0 ? `AIS 소실 ${Math.round(gap)}분` : 'DARK'; + }, + getSize: 10 * sizeScale, + getColor: [168, 85, 247, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 14], + fontFamily: 'monospace', + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + } + + // GPS 스푸핑 라벨 + const spoofData = analyzedShips.filter(({ dto }) => dto.algorithms.gpsSpoofing.spoofingScore > 0.5); + if (spoofData.length > 0) { + layers.push( + new TextLayer({ + id: 'spoof-labels', + data: spoofData, + getPosition: (d) => [d.ship.lng, d.ship.lat], + getText: (d) => `GPS ${Math.round(d.dto.algorithms.gpsSpoofing.spoofingScore * 100)}%`, + getSize: 10 * sizeScale, + getColor: [239, 68, 68, 255], + getTextAnchor: 'start', + getPixelOffset: [12, -8], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + return layers; + }, [analysisMap, ships, activeFilter, sizeScale]); +} diff --git a/frontend/src/hooks/useKoreaFilters.ts b/frontend/src/hooks/useKoreaFilters.ts index 135da1d..253599d 100644 --- a/frontend/src/hooks/useKoreaFilters.ts +++ b/frontend/src/hooks/useKoreaFilters.ts @@ -1,7 +1,9 @@ import { useState, useMemo, useRef } from 'react'; +import { useLocalStorage } from './useLocalStorage'; import { KOREA_SUBMARINE_CABLES } from '../services/submarineCable'; import { getMarineTrafficCategory } from '../utils/marineTraffic'; -import type { Ship } from '../types'; +import { classifyFishingZone } from '../utils/fishingAnalysis'; +import type { Ship, VesselAnalysisDto } from '../types'; interface KoreaFilters { illegalFishing: boolean; @@ -41,8 +43,10 @@ export function useKoreaFilters( koreaShips: Ship[], visibleShips: Ship[], currentTime: number, + analysisMap?: Map, + cnFishingOn = false, ): UseKoreaFiltersResult { - const [filters, setFilters] = useState({ + const [filters, setFilters] = useLocalStorage('koreaFilters', { illegalFishing: false, illegalTransship: false, darkVessel: false, @@ -67,7 +71,8 @@ export function useKoreaFilters( filters.darkVessel || filters.cableWatch || filters.dokdoWatch || - filters.ferryWatch; + filters.ferryWatch || + cnFishingOn; // 불법환적 의심 선박 탐지 const transshipSuspects = useMemo(() => { @@ -190,8 +195,17 @@ export function useKoreaFilters( } } + // Python 분류 결과 합집합: is_dark=true인 mmsi 추가 + if (analysisMap) { + for (const [mmsi, dto] of analysisMap.entries()) { + if (dto.algorithms.darkVessel.isDark) { + result.add(mmsi); + } + } + } + return result; - }, [koreaShips, filters.darkVessel, currentTime]); + }, [koreaShips, filters.darkVessel, currentTime, analysisMap]); // 해저케이블 감시 const cableWatchSet = useMemo(() => { @@ -297,15 +311,32 @@ export function useKoreaFilters( if (!anyFilterOn) return visibleShips; return visibleShips.filter(s => { const mtCat = getMarineTrafficCategory(s.typecode, s.category); - if (filters.illegalFishing && mtCat === 'fishing' && s.flag !== 'KR') return true; + if (filters.illegalFishing) { + // 특정어업수역 Ⅰ~Ⅳ 내 비한국 어선만 불법어선으로 판별 + if (mtCat === 'fishing' && s.flag !== 'KR') { + const zoneInfo = classifyFishingZone(s.lat, s.lng); + if (zoneInfo.zone !== 'OUTSIDE') return true; + } + // Python 분석: 영해/접속수역 침범 + const analysis = analysisMap?.get(s.mmsi); + if (analysis) { + const zone = analysis.algorithms.location.zone; + if (zone === 'TERRITORIAL_SEA' || zone === 'CONTIGUOUS_ZONE') return true; + } + } if (filters.illegalTransship && transshipSuspects.has(s.mmsi)) return true; if (filters.darkVessel && darkVesselSet.has(s.mmsi)) return true; if (filters.cableWatch && cableWatchSet.has(s.mmsi)) return true; if (filters.dokdoWatch && dokdoWatchSet.has(s.mmsi)) return true; if (filters.ferryWatch && getMarineTrafficCategory(s.typecode, s.category) === 'passenger') return true; + if (cnFishingOn) { + const isCnFishing = s.flag === 'CN' && getMarineTrafficCategory(s.typecode, s.category) === 'fishing'; + const isGearPattern = /^.+?_\d+_\d+_?$/.test(s.name || ''); + if (isCnFishing || isGearPattern) return true; + } return false; }); - }, [visibleShips, filters, anyFilterOn, transshipSuspects, darkVesselSet, cableWatchSet, dokdoWatchSet]); + }, [visibleShips, filters, anyFilterOn, transshipSuspects, darkVesselSet, cableWatchSet, dokdoWatchSet, analysisMap, cnFishingOn]); return { filters, diff --git a/frontend/src/hooks/useLocalStorage.ts b/frontend/src/hooks/useLocalStorage.ts new file mode 100644 index 0000000..a2bdbed --- /dev/null +++ b/frontend/src/hooks/useLocalStorage.ts @@ -0,0 +1,68 @@ +import { useState, useCallback } from 'react'; + +const PREFIX = 'kcg.'; + +/** + * localStorage 연동 useState — JSON 직렬화/역직렬화 자동 처리. + * 새 키가 추가된 Record 타입은 defaults와 자동 머지. + */ +export function useLocalStorage(key: string, defaults: T): [T, (v: T | ((prev: T) => T)) => void] { + const storageKey = PREFIX + key; + + const [value, setValueRaw] = useState(() => { + try { + const raw = localStorage.getItem(storageKey); + if (raw === null) return defaults; + const parsed = JSON.parse(raw) as T; + // Record 타입이면 defaults에 있는 키가 저장값에 없을 때 머지 + if (defaults !== null && typeof defaults === 'object' && !Array.isArray(defaults)) { + return { ...defaults, ...parsed }; + } + return parsed; + } catch { + return defaults; + } + }); + + const setValue = useCallback((updater: T | ((prev: T) => T)) => { + setValueRaw(prev => { + const next = typeof updater === 'function' ? (updater as (prev: T) => T)(prev) : updater; + try { + localStorage.setItem(storageKey, JSON.stringify(next)); + } catch { /* quota exceeded — 무시 */ } + return next; + }); + }, [storageKey]); + + return [value, setValue]; +} + +/** + * Set용 localStorage 연동 — 내부적으로 Array로 직렬화. + */ +export function useLocalStorageSet(key: string, defaults: Set): [Set, (v: Set | ((prev: Set) => Set)) => void] { + const storageKey = PREFIX + key; + + const [value, setValueRaw] = useState>(() => { + try { + const raw = localStorage.getItem(storageKey); + if (raw === null) return defaults; + const arr = JSON.parse(raw); + return Array.isArray(arr) ? new Set(arr) : defaults; + } catch { + return defaults; + } + }); + + const setValue = useCallback((updater: Set | ((prev: Set) => Set)) => { + setValueRaw(prev => { + const next = typeof updater === 'function' ? updater(prev) : updater; + try { + localStorage.setItem(storageKey, JSON.stringify(Array.from(next))); + } catch { /* quota exceeded */ } + return next; + }); + }, [storageKey]); + + return [value, setValue]; +} diff --git a/frontend/src/hooks/useStaticDeckLayers.ts b/frontend/src/hooks/useStaticDeckLayers.ts new file mode 100644 index 0000000..0d4c796 --- /dev/null +++ b/frontend/src/hooks/useStaticDeckLayers.ts @@ -0,0 +1,1086 @@ +import { useMemo } from 'react'; +import { IconLayer, TextLayer, PathLayer } from '@deck.gl/layers'; +import type { Layer, PickingInfo } from '@deck.gl/core'; +import { svgToDataUri } from '../utils/svgToDataUri'; + +// Data imports +import { EAST_ASIA_PORTS } from '../data/ports'; +import type { Port } from '../data/ports'; +import { KOREA_WIND_FARMS } from '../data/windFarms'; +import type { WindFarm } from '../data/windFarms'; +import { MILITARY_BASES } from '../data/militaryBases'; +import type { MilitaryBase } from '../data/militaryBases'; +import { GOV_BUILDINGS } from '../data/govBuildings'; +import type { GovBuilding } from '../data/govBuildings'; +import { NK_LAUNCH_SITES, NK_LAUNCH_TYPE_META } from '../data/nkLaunchSites'; +import type { NKLaunchSite } from '../data/nkLaunchSites'; +import { NK_MISSILE_EVENTS } from '../data/nkMissileEvents'; +import type { NKMissileEvent } from '../data/nkMissileEvents'; +import { COAST_GUARD_FACILITIES } from '../services/coastGuard'; +import type { CoastGuardFacility, CoastGuardType } from '../services/coastGuard'; +import { KOREAN_AIRPORTS } from '../services/airports'; +import type { KoreanAirport } from '../services/airports'; +import { NAV_WARNINGS } from '../services/navWarning'; +import type { NavWarning, NavWarningLevel, TrainingOrg } from '../services/navWarning'; +import { PIRACY_ZONES, PIRACY_LEVEL_COLOR } from '../services/piracy'; +import type { PiracyZone } from '../services/piracy'; +import type { PowerFacility } from '../services/infra'; +import { HAZARD_FACILITIES } from '../data/hazardFacilities'; +import type { HazardFacility, HazardType } from '../data/hazardFacilities'; +import { CN_POWER_PLANTS, CN_MILITARY_FACILITIES } from '../data/cnFacilities'; +import type { CnFacility } from '../data/cnFacilities'; +import { JP_POWER_PLANTS, JP_MILITARY_FACILITIES } from '../data/jpFacilities'; +import type { JpFacility } from '../data/jpFacilities'; + +// ─── Type alias to avoid 'any' in PickingInfo ─────────────────────────────── + +export type StaticPickedObject = + | Port + | WindFarm + | MilitaryBase + | GovBuilding + | NKLaunchSite + | NKMissileEvent + | CoastGuardFacility + | KoreanAirport + | NavWarning + | PiracyZone + | PowerFacility + | HazardFacility + | CnFacility + | JpFacility; + +export type StaticLayerKind = + | 'port' + | 'windFarm' + | 'militaryBase' + | 'govBuilding' + | 'nkLaunch' + | 'nkMissile' + | 'coastGuard' + | 'airport' + | 'navWarning' + | 'piracy' + | 'infra' + | 'hazard' + | 'cnFacility' + | 'jpFacility'; + +export interface StaticPickInfo { + kind: StaticLayerKind; + object: StaticPickedObject; +} + +interface StaticLayerConfig { + ports: boolean; + coastGuard: boolean; + windFarm: boolean; + militaryBases: boolean; + govBuildings: boolean; + airports: boolean; + navWarning: boolean; + nkLaunch: boolean; + nkMissile: boolean; + piracy: boolean; + infra: boolean; + infraFacilities: PowerFacility[]; + hazardTypes: HazardType[]; + cnPower: boolean; + cnMilitary: boolean; + jpPower: boolean; + jpMilitary: boolean; + onPick: (info: StaticPickInfo) => void; + sizeScale?: number; // 줌 레벨별 스케일 배율 (기본 1.0) +} + +// ─── Color helpers ──────────────────────────────────────────────────────────── + +function hexToRgb(hex: string): [number, number, number] { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return [r, g, b]; +} + +// ─── Port SVG ──────────────────────────────────────────────────────────────── + +const PORT_COUNTRY_COLOR: Record = { + KR: '#3b82f6', + CN: '#ef4444', + JP: '#f472b6', + KP: '#f97316', + TW: '#10b981', +}; + +function portSvg(color: string, size: number): string { + return ` + + + + + `; +} + +// ─── Wind Turbine SVG ───────────────────────────────────────────────────────── + +const WIND_COLOR = '#00bcd4'; + +function windTurbineSvg(size: number): string { + return ` + + + + + + + + `; +} + +// ─── CoastGuard SVG ─────────────────────────────────────────────────────────── + +const CG_TYPE_COLOR: Record = { + hq: '#ff6b6b', + regional: '#ffa94d', + station: '#4dabf7', + substation: '#69db7c', + vts: '#da77f2', + navy: '#3b82f6', +}; + +function coastGuardSvg(type: CoastGuardType, size: number): string { + const color = CG_TYPE_COLOR[type]; + if (type === 'navy') { + return ` + + + + + `; + } + if (type === 'vts') { + return ` + + + + + `; + } + return ` + + + + + + `; +} + +const CG_TYPE_SIZE: Record = { + hq: 24, + regional: 20, + station: 16, + substation: 13, + vts: 14, + navy: 18, +}; + +// ─── Airport SVG ───────────────────────────────────────────────────────────── + +const AP_COUNTRY_COLOR: Record = { + KR: { intl: '#a78bfa', domestic: '#7c8aaa' }, + CN: { intl: '#ef4444', domestic: '#b91c1c' }, + JP: { intl: '#f472b6', domestic: '#9d174d' }, + KP: { intl: '#f97316', domestic: '#c2410c' }, + TW: { intl: '#10b981', domestic: '#059669' }, +}; + +function apColor(ap: KoreanAirport): string { + const cc = AP_COUNTRY_COLOR[ap.country ?? 'KR'] ?? AP_COUNTRY_COLOR.KR; + return ap.intl ? cc.intl : cc.domestic; +} + +function airportSvg(color: string, size: number): string { + return ` + + + `; +} + +// ─── NavWarning SVG ─────────────────────────────────────────────────────────── + +const NW_ORG_COLOR: Record = { + '해군': '#8b5cf6', + '해병대': '#22c55e', + '공군': '#f97316', + '육군': '#ef4444', + '해경': '#3b82f6', + '국과연': '#eab308', +}; + +function navWarningSvg(level: NavWarningLevel, org: TrainingOrg, size: number): string { + const color = NW_ORG_COLOR[org]; + if (level === 'danger') { + return ` + + + + `; + } + return ` + + + + `; +} + +// ─── Piracy SVG ─────────────────────────────────────────────────────────────── + +function piracySvg(color: string, size: number): string { + return ` + + + + + + + + `; +} + +// ─── NKMissile SVG ──────────────────────────────────────────────────────────── + +function getMissileColor(type: string): string { + if (type.includes('ICBM')) return '#dc2626'; + if (type.includes('IRBM')) return '#ef4444'; + if (type.includes('SLBM')) return '#3b82f6'; + return '#f97316'; +} + +function missileLaunchSvg(color: string): string { + return ` + + `; +} + +function missileImpactSvg(color: string): string { + return ` + + + + `; +} + +// ─── Infra SVG ──────────────────────────────────────────────────────────────── + +const INFRA_SOURCE_COLOR: Record = { + nuclear: '#e040fb', + coal: '#795548', + gas: '#ff9800', + oil: '#5d4037', + hydro: '#2196f3', + solar: '#ffc107', + wind: '#00bcd4', + biomass: '#4caf50', +}; +const INFRA_SUBSTATION_COLOR = '#ffeb3b'; + +function infraColor(f: PowerFacility): string { + if (f.type === 'substation') return INFRA_SUBSTATION_COLOR; + return INFRA_SOURCE_COLOR[f.source ?? ''] ?? '#9e9e9e'; +} + +function infraSvg(f: PowerFacility): string { + const color = infraColor(f); + if (f.source === 'wind') { + return windTurbineSvg(14).replace(`stroke="${WIND_COLOR}"`, `stroke="${color}"`).replace(new RegExp(`fill="${WIND_COLOR}"`, 'g'), `fill="${color}"`); + } + const size = f.type === 'substation' ? 7 : 12; + return ` + + `; +} + +// ─── Memoized icon atlases ──────────────────────────────────────────────────── + +// We use individual Data URI per item via getIcon accessor instead of atlas +// ─── Main hook ─────────────────────────────────────────────────────────────── + +export function useStaticDeckLayers(config: StaticLayerConfig): Layer[] { + return useMemo(() => { + const layers: Layer[] = []; + const sc = config.sizeScale ?? 1.0; // 줌 레벨별 스케일 배율 + + // ── Ports ─────────────────────────────────────────────────────────────── + if (config.ports) { + // Build per-item data-uri icons: reuse by (country, type) key + const portIconCache = new Map(); + function getPortIconUrl(p: Port): string { + const key = `${p.country}-${p.type}`; + if (!portIconCache.has(key)) { + const color = PORT_COUNTRY_COLOR[p.country] ?? PORT_COUNTRY_COLOR.KR; + const size = p.type === 'major' ? 32 : 24; + portIconCache.set(key, svgToDataUri(portSvg(color, size))); + } + return portIconCache.get(key)!; + } + + layers.push( + new IconLayer({ + id: 'static-ports-icon', + data: EAST_ASIA_PORTS, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => ({ + url: getPortIconUrl(d), + width: d.type === 'major' ? 32 : 24, + height: d.type === 'major' ? 32 : 24, + anchorX: d.type === 'major' ? 16 : 12, + anchorY: d.type === 'major' ? 16 : 12, + }), + getSize: (d) => (d.type === 'major' ? 16 : 12) * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'port', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-ports-label', + data: EAST_ASIA_PORTS, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.nameKo.replace('항', ''), + getSize: 9 * sc, + getColor: (d) => [...hexToRgb(PORT_COUNTRY_COLOR[d.country] ?? PORT_COUNTRY_COLOR.KR), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 8], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Wind Farms ───────────────────────────────────────────────────────── + if (config.windFarm) { + const windUrl = svgToDataUri(windTurbineSvg(36)); + layers.push( + new IconLayer({ + id: 'static-windfarm-icon', + data: KOREA_WIND_FARMS, + getPosition: (d) => [d.lng, d.lat], + getIcon: () => ({ url: windUrl, width: 36, height: 36, anchorX: 18, anchorY: 18 }), + getSize: 18 * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'windFarm', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-windfarm-label', + data: KOREA_WIND_FARMS, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => (d.name.length > 10 ? d.name.slice(0, 10) + '..' : d.name), + getSize: 9 * sc, + getColor: [...hexToRgb(WIND_COLOR), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 10], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Coast Guard ──────────────────────────────────────────────────────── + if (config.coastGuard) { + const cgIconCache = new Map(); + function getCgIconUrl(type: CoastGuardType): string { + if (!cgIconCache.has(type)) { + const size = CG_TYPE_SIZE[type]; + cgIconCache.set(type, svgToDataUri(coastGuardSvg(type, size * 2))); + } + return cgIconCache.get(type)!; + } + + layers.push( + new IconLayer({ + id: 'static-coastguard-icon', + data: COAST_GUARD_FACILITIES, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => { + const sz = CG_TYPE_SIZE[d.type] * 2; + return { url: getCgIconUrl(d.type), width: sz, height: sz, anchorX: sz / 2, anchorY: sz / 2 }; + }, + getSize: (d) => CG_TYPE_SIZE[d.type] * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'coastGuard', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-coastguard-label', + data: COAST_GUARD_FACILITIES.filter(f => f.type === 'hq' || f.type === 'regional' || f.type === 'navy' || f.type === 'vts'), + getPosition: (d) => [d.lng, d.lat], + getText: (d) => { + if (d.type === 'vts') return 'VTS'; + if (d.type === 'navy') return d.name.replace('해군', '').replace('사령부', '사').replace('전대', '전').replace('전단', '전').trim().slice(0, 8); + return d.name.replace('해양경찰청', '').replace('지방', '').trim() || '본청'; + }, + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(CG_TYPE_COLOR[d.type]), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 8], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Airports ─────────────────────────────────────────────────────────── + if (config.airports) { + const apIconCache = new Map(); + function getApIconUrl(ap: KoreanAirport): string { + const color = apColor(ap); + const size = ap.intl ? 40 : 32; + const key = `${color}-${size}`; + if (!apIconCache.has(key)) { + apIconCache.set(key, svgToDataUri(airportSvg(color, size))); + } + return apIconCache.get(key)!; + } + + layers.push( + new IconLayer({ + id: 'static-airports-icon', + data: KOREAN_AIRPORTS, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => { + const sz = d.intl ? 40 : 32; + return { url: getApIconUrl(d), width: sz, height: sz, anchorX: sz / 2, anchorY: sz / 2 }; + }, + getSize: (d) => (d.intl ? 20 : 16) * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'airport', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-airports-label', + data: KOREAN_AIRPORTS, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.nameKo.replace('국제공항', '').replace('공항', ''), + getSize: 9 * sc, + getColor: (d) => [...hexToRgb(apColor(d)), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 10], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── NavWarning ───────────────────────────────────────────────────────── + if (config.navWarning) { + const nwIconCache = new Map(); + function getNwIconUrl(w: NavWarning): string { + const key = `${w.level}-${w.org}`; + if (!nwIconCache.has(key)) { + const size = w.level === 'danger' ? 32 : 28; + nwIconCache.set(key, svgToDataUri(navWarningSvg(w.level, w.org, size))); + } + return nwIconCache.get(key)!; + } + + layers.push( + new IconLayer({ + id: 'static-navwarning-icon', + data: NAV_WARNINGS, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => { + const sz = d.level === 'danger' ? 32 : 28; + return { url: getNwIconUrl(d), width: sz, height: sz, anchorX: sz / 2, anchorY: sz / 2 }; + }, + getSize: (d) => (d.level === 'danger' ? 16 : 14) * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'navWarning', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-navwarning-label', + data: NAV_WARNINGS, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.id, + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(NW_ORG_COLOR[d.org]), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 9], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Piracy ───────────────────────────────────────────────────────────── + if (config.piracy) { + const piracyIconCache = new Map(); + function getPiracyIconUrl(zone: PiracyZone): string { + const key = zone.level; + if (!piracyIconCache.has(key)) { + const color = PIRACY_LEVEL_COLOR[zone.level]; + const size = zone.level === 'critical' ? 56 : zone.level === 'high' ? 48 : 40; + piracyIconCache.set(key, svgToDataUri(piracySvg(color, size))); + } + return piracyIconCache.get(key)!; + } + + layers.push( + new IconLayer({ + id: 'static-piracy-icon', + data: PIRACY_ZONES, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => { + const sz = d.level === 'critical' ? 56 : d.level === 'high' ? 48 : 40; + return { url: getPiracyIconUrl(d), width: sz, height: sz, anchorX: sz / 2, anchorY: sz / 2 }; + }, + getSize: (d) => (d.level === 'critical' ? 28 : d.level === 'high' ? 24 : 20) * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'piracy', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-piracy-label', + data: PIRACY_ZONES, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.nameKo, + getSize: 9 * sc, + getColor: (d) => [...hexToRgb(PIRACY_LEVEL_COLOR[d.level] ?? '#ef4444'), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 14], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Military Bases — TextLayer (이모지) ─────────────────────────────── + if (config.militaryBases) { + const TYPE_COLOR: Record = { + naval: '#3b82f6', airforce: '#f59e0b', army: '#22c55e', + missile: '#ef4444', joint: '#a78bfa', + }; + const TYPE_ICON: Record = { + naval: '⚓', airforce: '✈', army: '🪖', missile: '🚀', joint: '⭐', + }; + layers.push( + new TextLayer({ + id: 'static-militarybase-emoji', + data: MILITARY_BASES, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => TYPE_ICON[d.type] ?? '⭐', + getSize: 14 * sc, + getColor: [255, 255, 255, 220], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + billboard: false, + characterSet: 'auto', + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'militaryBase', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-militarybase-label', + data: MILITARY_BASES, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => (d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo), + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(TYPE_COLOR[d.type] ?? '#a78bfa'), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 9], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Gov Buildings — TextLayer (이모지) ───────────────────────────────── + if (config.govBuildings) { + const GOV_TYPE_COLOR: Record = { + executive: '#f59e0b', legislature: '#a78bfa', military_hq: '#ef4444', + intelligence: '#6366f1', foreign: '#3b82f6', maritime: '#06b6d4', defense: '#dc2626', + }; + const GOV_TYPE_ICON: Record = { + executive: '🏛', legislature: '🏛', military_hq: '⭐', + intelligence: '🔍', foreign: '🌐', maritime: '⚓', defense: '🛡', + }; + layers.push( + new TextLayer({ + id: 'static-govbuilding-emoji', + data: GOV_BUILDINGS, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => GOV_TYPE_ICON[d.type] ?? '🏛', + getSize: 12 * sc, + getColor: [255, 255, 255, 220], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + billboard: false, + characterSet: 'auto', + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'govBuilding', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-govbuilding-label', + data: GOV_BUILDINGS, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => (d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo), + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(GOV_TYPE_COLOR[d.type] ?? '#f59e0b'), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 8], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── NK Launch Sites — TextLayer (이모지) ────────────────────────────── + if (config.nkLaunch) { + layers.push( + new TextLayer({ + id: 'static-nklaunch-emoji', + data: NK_LAUNCH_SITES, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => NK_LAUNCH_TYPE_META[d.type]?.icon ?? '🚀', + getSize: (d) => (d.type === 'artillery' || d.type === 'mlrs' ? 10 : 13) * sc, + getColor: [255, 255, 255, 220], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + billboard: false, + characterSet: 'auto', + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'nkLaunch', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-nklaunch-label', + data: NK_LAUNCH_SITES, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => (d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo), + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(NK_LAUNCH_TYPE_META[d.type]?.color ?? '#f97316'), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 8], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── NK Missile Events — IconLayer ───────────────────────────────────── + if (config.nkMissile) { + // Launch points (triangle) + const launchIconCache = new Map(); + function getLaunchIconUrl(type: string): string { + if (!launchIconCache.has(type)) { + launchIconCache.set(type, svgToDataUri(missileLaunchSvg(getMissileColor(type)))); + } + return launchIconCache.get(type)!; + } + // Impact points (X) + const impactIconCache = new Map(); + function getImpactIconUrl(type: string): string { + if (!impactIconCache.has(type)) { + impactIconCache.set(type, svgToDataUri(missileImpactSvg(getMissileColor(type)))); + } + return impactIconCache.get(type)!; + } + + interface LaunchPoint { ev: NKMissileEvent; lat: number; lng: number } + interface ImpactPoint { ev: NKMissileEvent; lat: number; lng: number } + + const launchData: LaunchPoint[] = NK_MISSILE_EVENTS.map(ev => ({ ev, lat: ev.launchLat, lng: ev.launchLng })); + const impactData: ImpactPoint[] = NK_MISSILE_EVENTS.map(ev => ({ ev, lat: ev.impactLat, lng: ev.impactLng })); + + // 발사→착탄 궤적선 + const trajectoryData = NK_MISSILE_EVENTS.map(ev => ({ + path: [[ev.launchLng, ev.launchLat], [ev.impactLng, ev.impactLat]] as [number, number][], + color: hexToRgb(getMissileColor(ev.type)), + })); + layers.push( + new PathLayer<{ path: [number, number][]; color: [number, number, number] }>({ + id: 'static-nkmissile-trajectory', + data: trajectoryData, + getPath: (d) => d.path, + getColor: (d) => [...d.color, 150] as [number, number, number, number], + getWidth: 2, + widthUnits: 'pixels', + getDashArray: [6, 3], + dashJustified: true, + extensions: [], + }), + ); + + layers.push( + new IconLayer({ + id: 'static-nkmissile-launch', + data: launchData, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => ({ url: getLaunchIconUrl(d.ev.type), width: 24, height: 24, anchorX: 12, anchorY: 12 }), + getSize: 12 * sc, + getColor: (d) => { + const today = new Date().toISOString().slice(0, 10) === d.ev.date; + return [255, 255, 255, today ? 255 : 90] as [number, number, number, number]; + }, + }), + new IconLayer({ + id: 'static-nkmissile-impact', + data: impactData, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => ({ url: getImpactIconUrl(d.ev.type), width: 32, height: 32, anchorX: 16, anchorY: 16 }), + getSize: 16 * sc, + getColor: (d) => { + const today = new Date().toISOString().slice(0, 10) === d.ev.date; + return [255, 255, 255, today ? 255 : 100] as [number, number, number, number]; + }, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'nkMissile', object: info.object.ev }); + return true; + }, + }), + new TextLayer({ + id: 'static-nkmissile-label', + data: impactData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => `${d.ev.date.slice(5)} ${d.ev.time} ← ${d.ev.launchNameKo}`, + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(getMissileColor(d.ev.type)), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 10], + fontFamily: 'monospace', + fontWeight: 700, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Infra ────────────────────────────────────────────────────────────── + if (config.infra && config.infraFacilities.length > 0) { + const infraIconCache = new Map(); + function getInfraIconUrl(f: PowerFacility): string { + const key = `${f.type}-${f.source ?? ''}`; + if (!infraIconCache.has(key)) { + infraIconCache.set(key, svgToDataUri(infraSvg(f))); + } + return infraIconCache.get(key)!; + } + + const plants = config.infraFacilities.filter(f => f.type === 'plant'); + const substations = config.infraFacilities.filter(f => f.type === 'substation'); + + layers.push( + new IconLayer({ + id: 'static-infra-substation', + data: substations, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => ({ url: getInfraIconUrl(d), width: 14, height: 14, anchorX: 7, anchorY: 7 }), + getSize: 7 * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'infra', object: info.object }); + return true; + }, + }), + new IconLayer({ + id: 'static-infra-plant', + data: plants, + getPosition: (d) => [d.lng, d.lat], + getIcon: (d) => ({ url: getInfraIconUrl(d), width: 24, height: 24, anchorX: 12, anchorY: 12 }), + getSize: 12 * sc, + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'infra', object: info.object }); + return true; + }, + }), + new TextLayer({ + id: 'static-infra-plant-label', + data: plants, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => (d.name.length > 10 ? d.name.slice(0, 10) + '..' : d.name), + getSize: 8 * sc, + getColor: (d) => [...hexToRgb(infraColor(d)), 255] as [number, number, number, number], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 8], + fontFamily: 'monospace', + fontWeight: 600, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + + // ── Hazard Facilities ────────────────────────────────────────────────── + if (config.hazardTypes.length > 0) { + const hazardTypeSet = new Set(config.hazardTypes); + const hazardData = HAZARD_FACILITIES.filter(f => hazardTypeSet.has(f.type)); + + const HAZARD_META: Record = { + petrochemical: { icon: '🏭', color: [249, 115, 22, 255] }, + lng: { icon: '🔵', color: [6, 182, 212, 255] }, + oilTank: { icon: '🛢️', color: [234, 179, 8, 255] }, + hazardPort: { icon: '⚠️', color: [239, 68, 68, 255] }, + nuclear: { icon: '☢️', color: [168, 85, 247, 255] }, + thermal: { icon: '🔥', color: [100, 116, 139, 255] }, + shipyard: { icon: '🚢', color: [14, 165, 233, 255] }, + wastewater: { icon: '💧', color: [16, 185, 129, 255] }, + heavyIndustry: { icon: '⚙️', color: [148, 163, 184, 255] }, + }; + + if (hazardData.length > 0) { + layers.push( + new TextLayer({ + id: 'static-hazard-emoji', + data: hazardData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => HAZARD_META[d.type]?.icon ?? '⚠️', + getSize: 16 * sc, + getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'hazard', object: info.object }); + return true; + }, + billboard: false, + characterSet: 'auto', + }), + ); + layers.push( + new TextLayer({ + id: 'static-hazard-label', + data: hazardData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo, + getSize: 9 * sc, + getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 200], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 12], + fontFamily: 'monospace', + fontWeight: 600, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + } + + // ── CN Facilities ────────────────────────────────────────────────────── + { + const CN_META: Record = { + nuclear: { icon: '☢️', color: [239, 68, 68, 255] }, + thermal: { icon: '🔥', color: [249, 115, 22, 255] }, + naval: { icon: '⚓', color: [59, 130, 246, 255] }, + airbase: { icon: '✈️', color: [34, 211, 238, 255] }, + army: { icon: '🪖', color: [34, 197, 94, 255] }, + shipyard: { icon: '🚢', color: [148, 163, 184, 255] }, + }; + const cnData: CnFacility[] = [ + ...(config.cnPower ? CN_POWER_PLANTS : []), + ...(config.cnMilitary ? CN_MILITARY_FACILITIES : []), + ]; + if (cnData.length > 0) { + layers.push( + new TextLayer({ + id: 'static-cn-emoji', + data: cnData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => CN_META[d.subType]?.icon ?? '📍', + getSize: 16 * sc, + getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'cnFacility', object: info.object }); + return true; + }, + billboard: false, + characterSet: 'auto', + }), + ); + layers.push( + new TextLayer({ + id: 'static-cn-label', + data: cnData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.name, + getSize: 9 * sc, + getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 200], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 12], + fontFamily: 'monospace', + fontWeight: 600, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + } + + // ── JP Facilities ────────────────────────────────────────────────────── + { + const JP_META: Record = { + nuclear: { icon: '☢️', color: [239, 68, 68, 255] }, + thermal: { icon: '🔥', color: [249, 115, 22, 255] }, + naval: { icon: '⚓', color: [59, 130, 246, 255] }, + airbase: { icon: '✈️', color: [34, 211, 238, 255] }, + army: { icon: '🪖', color: [34, 197, 94, 255] }, + }; + const jpData: JpFacility[] = [ + ...(config.jpPower ? JP_POWER_PLANTS : []), + ...(config.jpMilitary ? JP_MILITARY_FACILITIES : []), + ]; + if (jpData.length > 0) { + layers.push( + new TextLayer({ + id: 'static-jp-emoji', + data: jpData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => JP_META[d.subType]?.icon ?? '📍', + getSize: 16 * sc, + getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 255], + getTextAnchor: 'middle', + getAlignmentBaseline: 'center', + pickable: true, + onClick: (info: PickingInfo) => { + if (info.object) config.onPick({ kind: 'jpFacility', object: info.object }); + return true; + }, + billboard: false, + characterSet: 'auto', + }), + ); + layers.push( + new TextLayer({ + id: 'static-jp-label', + data: jpData, + getPosition: (d) => [d.lng, d.lat], + getText: (d) => d.name, + getSize: 9 * sc, + getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 200], + getTextAnchor: 'middle', + getAlignmentBaseline: 'top', + getPixelOffset: [0, 12], + fontFamily: 'monospace', + fontWeight: 600, + outlineWidth: 2, + outlineColor: [0, 0, 0, 200], + billboard: false, + characterSet: 'auto', + }), + ); + } + } + + return layers; + // infraFacilities는 배열 참조가 바뀌어야 갱신 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + config.ports, + config.windFarm, + config.coastGuard, + config.airports, + config.navWarning, + config.piracy, + config.militaryBases, + config.govBuildings, + config.nkLaunch, + config.nkMissile, + config.infra, + config.infraFacilities, + config.hazardTypes, + config.cnPower, + config.cnMilitary, + config.jpPower, + config.jpMilitary, + config.onPick, + config.sizeScale, + ]); +} + +// Re-export types that KoreaMap will need for Popup rendering +export type { Port, WindFarm, MilitaryBase, GovBuilding, NKLaunchSite, NKMissileEvent, CoastGuardFacility, KoreanAirport, NavWarning, PiracyZone, PowerFacility }; +export type { HazardFacility, HazardType, CnFacility, JpFacility }; +// Re-export label/color helpers used in Popup rendering +export { CG_TYPE_COLOR, PORT_COUNTRY_COLOR, WIND_COLOR, NW_ORG_COLOR, PIRACY_LEVEL_COLOR, getMissileColor }; diff --git a/frontend/src/hooks/useVesselAnalysis.ts b/frontend/src/hooks/useVesselAnalysis.ts new file mode 100644 index 0000000..ea53b32 --- /dev/null +++ b/frontend/src/hooks/useVesselAnalysis.ts @@ -0,0 +1,114 @@ +import { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import type { VesselAnalysisDto, RiskLevel } from '../types'; +import { fetchVesselAnalysis } from '../services/vesselAnalysis'; + +const POLL_INTERVAL_MS = 5 * 60_000; // 5분 +const STALE_MS = 30 * 60_000; // 30분 + +export interface AnalysisStats { + total: number; + critical: number; + high: number; + medium: number; + low: number; + dark: number; + spoofing: number; + clusterCount: number; +} + +export interface UseVesselAnalysisResult { + analysisMap: Map; + stats: AnalysisStats; + clusters: Map; + isLoading: boolean; + lastUpdated: number; +} + +const EMPTY_STATS: AnalysisStats = { + total: 0, critical: 0, high: 0, medium: 0, low: 0, + dark: 0, spoofing: 0, clusterCount: 0, +}; + +export function useVesselAnalysis(enabled: boolean): UseVesselAnalysisResult { + const mapRef = useRef>(new Map()); + const [version, setVersion] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [lastUpdated, setLastUpdated] = useState(0); + + const doFetch = useCallback(async () => { + if (!enabled) return; + setIsLoading(true); + try { + const items = await fetchVesselAnalysis(); + const now = Date.now(); + const map = mapRef.current; + + // stale 제거 + for (const [mmsi, dto] of map) { + const ts = new Date(dto.timestamp).getTime(); + if (now - ts > STALE_MS) map.delete(mmsi); + } + + // 새 결과 merge + for (const item of items) { + map.set(item.mmsi, item); + } + + setLastUpdated(now); + setVersion(v => v + 1); + } catch { + // 에러 시 기존 데이터 유지 (graceful degradation) + } finally { + setIsLoading(false); + } + }, [enabled]); + + useEffect(() => { + doFetch(); + const t = setInterval(doFetch, POLL_INTERVAL_MS); + return () => clearInterval(t); + }, [doFetch]); + + const analysisMap = mapRef.current; + + const stats = useMemo((): AnalysisStats => { + if (analysisMap.size === 0) return EMPTY_STATS; + let critical = 0, high = 0, medium = 0, low = 0, dark = 0, spoofing = 0; + const clusterIds = new Set(); + + for (const dto of analysisMap.values()) { + const level: RiskLevel = dto.algorithms.riskScore.level; + if (level === 'CRITICAL') critical++; + else if (level === 'HIGH') high++; + else if (level === 'MEDIUM') medium++; + else low++; + + if (dto.algorithms.darkVessel.isDark) dark++; + if (dto.algorithms.gpsSpoofing.spoofingScore > 0.5) spoofing++; + if (dto.algorithms.cluster.clusterId >= 0) { + clusterIds.add(dto.algorithms.cluster.clusterId); + } + } + + return { + total: analysisMap.size, critical, high, medium, low, + dark, spoofing, clusterCount: clusterIds.size, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [version]); + + const clusters = useMemo((): Map => { + const result = new Map(); + for (const [mmsi, dto] of analysisMap) { + const cid = dto.algorithms.cluster.clusterId; + if (cid < 0) continue; + const arr = result.get(cid); + if (arr) arr.push(mmsi); + else result.set(cid, [mmsi]); + } + return result; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [version]); + + return { analysisMap, stats, clusters, isLoading, lastUpdated }; +} diff --git a/frontend/src/services/vesselAnalysis.ts b/frontend/src/services/vesselAnalysis.ts new file mode 100644 index 0000000..c871c11 --- /dev/null +++ b/frontend/src/services/vesselAnalysis.ts @@ -0,0 +1,30 @@ +import type { VesselAnalysisDto } from '../types'; + +const API_BASE = '/api/kcg'; + +export async function fetchVesselAnalysis(): Promise { + const res = await fetch(`${API_BASE}/vessel-analysis`, { + headers: { accept: 'application/json' }, + }); + if (!res.ok) return []; + const data: { count: number; items: VesselAnalysisDto[] } = await res.json(); + return data.items ?? []; +} + +export interface FleetCompany { + id: number; + nameCn: string; + nameEn: string; +} + +// 캐시 (세션 중 1회 로드) +let companyCache: Map | null = null; + +export async function fetchFleetCompanies(): Promise> { + if (companyCache) return companyCache; + const res = await fetch(`${API_BASE}/fleet-companies`); + if (!res.ok) return new Map(); + const items: FleetCompany[] = await res.json(); + companyCache = new Map(items.map(c => [c.id, c])); + return companyCache; +} diff --git a/frontend/src/services/vesselTrack.ts b/frontend/src/services/vesselTrack.ts new file mode 100644 index 0000000..8181e5d --- /dev/null +++ b/frontend/src/services/vesselTrack.ts @@ -0,0 +1,39 @@ +const SIGNAL_BATCH_BASE = '/signal-batch'; + +interface TrackResponse { + vesselId: string; + geometry: [number, number][]; + speeds: number[]; + timestamps: string[]; + pointCount: number; + totalDistance: number; + shipName: string; +} + +// mmsi별 캐시 (TTL 5분) +const trackCache = new Map(); +const CACHE_TTL = 5 * 60_000; + +export async function fetchVesselTrack(mmsi: string, hours: number = 6): Promise<[number, number][]> { + const cached = trackCache.get(mmsi); + if (cached && Date.now() - cached.time < CACHE_TTL) return cached.coords; + + const endTime = new Date().toISOString(); + const startTime = new Date(Date.now() - hours * 3600_000).toISOString(); + + try { + const res = await fetch(`${SIGNAL_BATCH_BASE}/api/v2/tracks/vessels`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', accept: 'application/json' }, + body: JSON.stringify({ startTime, endTime, vessels: [mmsi] }), + }); + if (!res.ok) return []; + const data: TrackResponse[] = await res.json(); + if (!data.length || !data[0].geometry?.length) return []; + const coords = data[0].geometry; + trackCache.set(mmsi, { time: Date.now(), coords }); + return coords; + } catch { + return []; + } +} diff --git a/frontend/src/types.ts b/frontend/src/types.ts index fe906ed..c4f2dfd 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -146,6 +146,7 @@ export interface LayerVisibility { militaryOnly: boolean; overseasUS: boolean; overseasIsrael: boolean; + [key: string]: boolean; overseasIran: boolean; overseasUAE: boolean; overseasSaudi: boolean; @@ -154,65 +155,34 @@ export interface LayerVisibility { overseasKuwait: boolean; overseasIraq: boolean; overseasBahrain: boolean; - // Dynamic keys for energy/hazard sub-layers - [key: string]: boolean; } export type AppMode = 'replay' | 'live'; -// ── 중국어선 분석 결과 (Python 분류기 → REST API → Frontend) ── +// Vessel analysis (Python prediction 결과) +export type VesselType = 'TRAWL' | 'PURSE' | 'LONGLINE' | 'TRAP' | 'UNKNOWN'; +export type RiskLevel = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; +export type ActivityState = 'STATIONARY' | 'FISHING' | 'SAILING' | 'UNKNOWN'; +export type FleetRole = 'LEADER' | 'MEMBER' | 'NOISE'; -export type VesselType = 'TRAWL' | 'PURSE' | 'LONGLINE' | 'TRAP'; -export type RiskLevel = 'CRITICAL' | 'WATCH' | 'MONITOR' | 'NORMAL'; -export type ActivityState = 'FISHING' | 'SAILING' | 'STATIONARY' | 'AIS_LOSS'; -export type ZoneType = 'TERRITORIAL' | 'CONTIGUOUS' | 'EEZ' | 'BEYOND'; -export type FleetRole = 'MOTHER' | 'SUB' | 'TRANSPORT' | 'INDEPENDENT'; - -export interface VesselClassification { - vesselType: VesselType; - confidence: number; // 0~1 - fishingPct: number; // 조업 비율 % - clusterId: number; // BIRCH 군집 ID (-1=노이즈) - season: string; // SPRING/SUMMER/FALL/WINTER -} - -export interface VesselAlgorithms { - location: { zone: ZoneType; distToBaselineNm: number }; - activity: { state: ActivityState; ucafScore: number; ucftScore: number }; - darkVessel: { isDark: boolean; gapDurationMin: number }; - gpsSpoofing: { spoofingScore: number; bd09OffsetM: number; speedJumpCount: number }; - cluster: { clusterId: number; clusterSize: number; centroid?: [number, number] }; - fleetRole: { isLeader: boolean; role: FleetRole }; - riskScore: { score: number; level: RiskLevel }; -} - -export interface VesselAnalysisResult { +export interface VesselAnalysisDto { mmsi: string; - timestamp: string; // ISO 분석 시점 - classification: VesselClassification; - algorithms: VesselAlgorithms; + timestamp: string; + classification: { + vesselType: VesselType; + confidence: number; + fishingPct: number; + clusterId: number; + season: string; + }; + algorithms: { + location: { zone: string; distToBaselineNm: number }; + activity: { state: ActivityState; ucafScore: number; ucftScore: number }; + darkVessel: { isDark: boolean; gapDurationMin: number }; + gpsSpoofing: { spoofingScore: number; bd09OffsetM: number; speedJumpCount: number }; + cluster: { clusterId: number; clusterSize: number }; + fleetRole: { isLeader: boolean; role: FleetRole }; + riskScore: { score: number; level: RiskLevel }; + }; features: Record; } - -// 허가어선 정보 (signal-batch /api/v2/vessels/chnprmship) -export interface ChnPrmShipInfo { - mmsi: string; - imo: number; - name: string; - callsign: string; - vesselType: string; - lat: number; - lon: number; - sog: number; - cog: number; - heading: number; - length: number; - width: number; - draught: number; - destination: string; - status: string; - signalKindCode: string; - messageTimestamp: string; - shipImagePath?: string | null; - shipImageCount?: number; -} diff --git a/frontend/src/utils/fishingAnalysis.ts b/frontend/src/utils/fishingAnalysis.ts index ad93e24..6efec44 100644 --- a/frontend/src/utils/fishingAnalysis.ts +++ b/frontend/src/utils/fishingAnalysis.ts @@ -2,6 +2,11 @@ // 한중어업협정 허가현황 (2026.01.06, 906척) + GB/T 5147-2003 어구 분류 import type { Ship } from '../types'; +import booleanPointInPolygon from '@turf/boolean-point-in-polygon'; +import { point } from '@turf/helpers'; +import type { Feature, MultiPolygon } from 'geojson'; + +import fishingZonesWgs84 from '../data/zones/fishing-zones-wgs84.json'; /** * 중국 허가 업종 코드 (허가번호 접두사) @@ -42,16 +47,45 @@ const GEAR_META: Record = { + ZONE_I: ['PS', 'FC'], + ZONE_II: ['PT', 'OT', 'GN', 'PS', 'FC'], + ZONE_III: ['PT', 'OT', 'GN', 'PS', 'FC'], + ZONE_IV: ['GN', 'PS', 'FC'], }; +/** + * 특정어업수역 Ⅰ~Ⅳ 폴리곤 (사전 변환된 WGS84 GeoJSON) + */ +export const ZONE_POLYGONS = fishingZonesWgs84.features.map(f => ({ + id: f.properties.id as FishingZoneId, + name: f.properties.name, + allowed: ZONE_ALLOWED[f.properties.id] ?? [], + geojson: f as unknown as Feature, +})); + +/** + * 특정어업수역 폴리곤 기반 수역 분류 + */ +export function classifyFishingZone(lat: number, lng: number): FishingZoneInfo { + const pt = point([lng, lat]); + for (const z of ZONE_POLYGONS) { + if (booleanPointInPolygon(pt, z.geojson)) { + return { zone: z.id, name: z.name, allowed: z.allowed }; + } + } + return { zone: 'OUTSIDE', name: '수역 외', allowed: [] }; +} + /** * 업종별 허가 기간 (월/일) */ diff --git a/frontend/src/utils/fleetDetection.ts b/frontend/src/utils/fleetDetection.ts index fc2dc5a..ffb87dd 100644 --- a/frontend/src/utils/fleetDetection.ts +++ b/frontend/src/utils/fleetDetection.ts @@ -20,6 +20,22 @@ export interface FleetConnection { fleetTypeKo: string; } +// ── 사전 그룹핑 인터페이스 ── + +export interface FleetGroupMember { + ship: Ship; + role: FleetRole; + roleKo: string; +} + +export interface FleetGroup { + groupId: number; + fleetType: 'trawl_pair' | 'purse_seine_fleet' | 'transship' | 'cluster'; + fleetTypeKo: string; + members: FleetGroupMember[]; + center: { lat: number; lng: number }; +} + /** 두 지점 사이 거리(NM) */ function distNm(lat1: number, lng1: number, lat2: number, lng2: number): number { const R = 3440.065; // 지구 반경 (해리) @@ -29,8 +45,167 @@ function distNm(lat1: number, lng1: number, lat2: number, lng2: number): number return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } +/** 선박이 중국 어선 후보인지 판별 */ +function isCnFishingCandidate(ship: Ship): boolean { + if (ship.flag !== 'CN') return false; + const cat = getMarineTrafficCategory(ship.typecode, ship.category); + return cat === 'fishing' || cat === 'unspecified'; +} + +/** 그룹 내 역할 결정 */ +function assignRoles(members: Ship[]): FleetGroupMember[] { + if (members.length === 0) return []; + + // 운반선 후보: 이름에 냉동운반선 관련 한자 포함 또는 cargo 카테고리 + const isCarrierCandidate = (s: Ship): boolean => { + const cat = getMarineTrafficCategory(s.typecode, s.category); + return cat === 'cargo' || s.name.includes('运') || s.name.includes('冷'); + }; + + // 최대 속도 선박 → mothership + const maxSpeed = Math.max(...members.map(s => s.speed)); + const motherIdx = members.findIndex(s => s.speed === maxSpeed); + + return members.map((ship, idx) => { + const cat = getMarineTrafficCategory(ship.typecode, ship.category); + + if (isCarrierCandidate(ship) && ship.speed <= 2) { + return { ship, role: 'carrier' as FleetRole, roleKo: '운반선 (FC)' }; + } + if (ship.speed < 1 && (cat === 'fishing' || cat === 'unspecified')) { + return { ship, role: 'lighting' as FleetRole, roleKo: '조명선' }; + } + if (idx === motherIdx && members.length > 1) { + return { ship, role: 'mothership' as FleetRole, roleKo: '모선' }; + } + return { ship, role: 'subsidiary' as FleetRole, roleKo: '선단 멤버' }; + }); +} + +/** 그룹 유형 판별 */ +function classifyGroup(members: Ship[]): { fleetType: FleetGroup['fleetType']; fleetTypeKo: string } { + if (members.length === 2) { + const [a, b] = members; + const speedDiff = Math.abs(a.speed - b.speed); + let headingDiff = Math.abs(a.heading - b.heading); + if (headingDiff > 180) headingDiff = 360 - headingDiff; + if ( + speedDiff < 1 && headingDiff < 20 && + a.speed >= 2 && a.speed <= 5 && + b.speed >= 2 && b.speed <= 5 + ) { + return { fleetType: 'trawl_pair', fleetTypeKo: '2척식 저인망 (본선·부속선)' }; + } + } + + const hasCarrier = members.some(s => { + const cat = getMarineTrafficCategory(s.typecode, s.category); + return (cat === 'cargo' || s.name.includes('运') || s.name.includes('冷')) && s.speed <= 2; + }); + if (hasCarrier) { + return { fleetType: 'transship', fleetTypeKo: '환적 의심 (운반선 접근)' }; + } + + const hasLighting = members.some(s => s.speed < 1); + if (members.length >= 3 || hasLighting) { + return { fleetType: 'purse_seine_fleet', fleetTypeKo: '위망 선단 (모선·운반·조명)' }; + } + + return { fleetType: 'cluster', fleetTypeKo: '인근 어선 클러스터' }; +} + /** - * 선택한 중국어선 주변의 선단 구성을 탐지 + * 전체 중국어선을 사전 클러스터링하여 선단 그룹 생성. + * 어느 멤버를 눌러도 같은 그룹을 반환할 수 있도록 mmsi→groupId 맵도 함께 반환. + * + * 알고리즘: 3NM 이내 BFS flood fill (O(N²), N>1000 시 0.3도 박스 사전필터) + */ +export function buildFleetGroups(ships: Ship[]): { + groups: FleetGroup[]; + memberMap: Map; +} { + // 중국 어선 후보만 추출 + const candidates = ships.filter(isCnFishingCandidate); + + // N > 1000이면 0.3도 박스로 사전필터링하여 인접 목록 구성 + const USE_BBOX_FILTER = candidates.length > 1000; + const CLUSTER_NM = 3; + const BBOX_DEG = 0.3; // ~18NM + + // 미할당 인덱스 집합 + const unassigned = new Set(candidates.map((_, i) => i)); + + const groups: FleetGroup[] = []; + const memberMap = new Map(); + + let groupId = 0; + + for (let i = 0; i < candidates.length; i++) { + if (!unassigned.has(i)) continue; + + // BFS + const cluster: number[] = []; + const queue: number[] = [i]; + unassigned.delete(i); + + while (queue.length > 0) { + const cur = queue.shift()!; + cluster.push(cur); + const curShip = candidates[cur]; + + // 탐색 후보: bbox 필터 또는 전체 + const searchPool = USE_BBOX_FILTER + ? candidates.reduce((acc, s, idx) => { + if ( + unassigned.has(idx) && + Math.abs(s.lat - curShip.lat) < BBOX_DEG && + Math.abs(s.lng - curShip.lng) < BBOX_DEG + ) { + acc.push(idx); + } + return acc; + }, []) + : Array.from(unassigned); + + for (const j of searchPool) { + if (!unassigned.has(j)) continue; + const neighbor = candidates[j]; + if (distNm(curShip.lat, curShip.lng, neighbor.lat, neighbor.lng) <= CLUSTER_NM) { + unassigned.delete(j); + queue.push(j); + } + } + } + + // 2척 미만은 그룹 해제 + if (cluster.length < 2) continue; + + const memberShips = cluster.map(idx => candidates[idx]); + const { fleetType, fleetTypeKo } = classifyGroup(memberShips); + const memberRoles = assignRoles(memberShips); + + // 중심점 계산 + const centerLat = memberShips.reduce((sum, s) => sum + s.lat, 0) / memberShips.length; + const centerLng = memberShips.reduce((sum, s) => sum + s.lng, 0) / memberShips.length; + + const group: FleetGroup = { + groupId, + fleetType, + fleetTypeKo, + members: memberRoles, + center: { lat: centerLat, lng: centerLng }, + }; + + groups.push(group); + memberShips.forEach(s => memberMap.set(s.mmsi, groupId)); + groupId++; + } + + return { groups, memberMap }; +} + +/** + * 선택한 중국어선 주변의 선단 구성을 탐지 (기존 1척 기준 탐지 — fallback용 유지) * * 보고서 기준: * - PT 2척식 저인망: 본선+부속선 3NM 이내, 유사 속도(2~5kn), 유사 방향 diff --git a/frontend/src/utils/svgToDataUri.ts b/frontend/src/utils/svgToDataUri.ts new file mode 100644 index 0000000..9d6f96c --- /dev/null +++ b/frontend/src/utils/svgToDataUri.ts @@ -0,0 +1,3 @@ +export function svgToDataUri(svg: string): string { + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; +} diff --git a/prediction/algorithms/fleet.py b/prediction/algorithms/fleet.py index 22ccd0c..ee56787 100644 --- a/prediction/algorithms/fleet.py +++ b/prediction/algorithms/fleet.py @@ -1,152 +1,177 @@ -import math +"""선단(Fleet) 패턴 탐지 — 공간+행동 기반. + +단순 공간 근접이 아닌, 협조 운항 패턴(유사 속도/방향/역할)으로 선단을 판별. +- PT 저인망: 2척, 3NM 이내, 유사 속도(2~5kn) + 유사 방향(20° 이내) +- PS 선망: 3~5척, 2NM 이내, 모선(고속)+조명선(정지)+운반선(저속 대형) +- FC 환적: 2척, 0.5NM 이내, 양쪽 저속(2kn 이하) +""" + import logging +from typing import Optional import numpy as np import pandas as pd -from algorithms.location import haversine_nm, dist_to_baseline, EARTH_RADIUS_NM +from algorithms.location import haversine_nm, dist_to_baseline logger = logging.getLogger(__name__) -def detect_group_clusters( - vessel_snapshots: list[dict], - spatial_eps_nm: float = 10.0, - time_eps_hours: float = 2.0, - min_vessels: int = 3, +def _heading_diff(h1: float, h2: float) -> float: + """두 방향 사이 최소 각도차 (0~180).""" + d = abs(h1 - h2) % 360 + return d if d <= 180 else 360 - d + + +def detect_fleet_patterns( + vessel_dfs: dict[str, pd.DataFrame], ) -> dict[int, list[dict]]: - """DBSCAN 시공간 클러스터링으로 집단 탐지.""" - if len(vessel_snapshots) < min_vessels: - return {} + """행동 패턴 기반 선단 탐지. - try: - from sklearn.cluster import DBSCAN - except ImportError: - logger.warning('sklearn not available for DBSCAN clustering') - return {} - - lat_rad = [math.radians(v['lat']) * EARTH_RADIUS_NM for v in vessel_snapshots] - lon_rad = [math.radians(v['lon']) * EARTH_RADIUS_NM for v in vessel_snapshots] - - # 시간을 NM 단위로 정규화 - timestamps = [pd.Timestamp(v['timestamp']).timestamp() for v in vessel_snapshots] - t_min = min(timestamps) - time_nm = [(t - t_min) / 3600 * 10 / time_eps_hours for t in timestamps] - - X = np.array(list(zip(lat_rad, lon_rad, time_nm))) - - db = DBSCAN(eps=spatial_eps_nm, min_samples=min_vessels, metric='euclidean').fit(X) - - clusters: dict[int, list[dict]] = {} - for idx, label in enumerate(db.labels_): - if label == -1: + Returns: {fleet_id: [{mmsi, lat, lon, sog, cog, role, pattern}, ...]} + """ + # 각 선박의 최신 스냅샷 추출 + snapshots: list[dict] = [] + for mmsi, df in vessel_dfs.items(): + if df is None or len(df) == 0: continue - clusters.setdefault(int(label), []).append(vessel_snapshots[idx]) + last = df.iloc[-1] + snapshots.append({ + 'mmsi': mmsi, + 'lat': float(last['lat']), + 'lon': float(last['lon']), + 'sog': float(last.get('sog', 0)), + 'cog': float(last.get('cog', 0)), + }) - return clusters - - -def identify_lead_vessel(cluster_vessels: list[dict]) -> dict: - """5기준 스코어링으로 대표선 특정.""" - if not cluster_vessels: + if len(snapshots) < 2: return {} - scores: dict[str, float] = {} + matched: set[str] = set() + fleets: dict[int, list[dict]] = {} + fleet_id = 0 - timestamps = [pd.Timestamp(v.get('timestamp', 0)).timestamp() for v in cluster_vessels] - min_ts = min(timestamps) if timestamps else 0 + # 1차: PT 저인망 쌍 탐지 (2척, 3NM, 유사 속도/방향) + for i in range(len(snapshots)): + if snapshots[i]['mmsi'] in matched: + continue + a = snapshots[i] + for j in range(i + 1, len(snapshots)): + if snapshots[j]['mmsi'] in matched: + continue + b = snapshots[j] + dist = haversine_nm(a['lat'], a['lon'], b['lat'], b['lon']) + if dist > 3.0: + continue + # 둘 다 조업 속도 (2~5kn) + if not (2.0 <= a['sog'] <= 5.0 and 2.0 <= b['sog'] <= 5.0): + continue + # 유사 속도 (차이 1kn 미만) + if abs(a['sog'] - b['sog']) >= 1.0: + continue + # 유사 방향 (20° 미만) + if _heading_diff(a['cog'], b['cog']) >= 20.0: + continue - lats = [v['lat'] for v in cluster_vessels] - lons = [v['lon'] for v in cluster_vessels] - centroid_lat = float(np.mean(lats)) - centroid_lon = float(np.mean(lons)) + fleets[fleet_id] = [ + {**a, 'role': 'LEADER', 'pattern': 'TRAWL_PAIR'}, + {**b, 'role': 'MEMBER', 'pattern': 'TRAWL_PAIR'}, + ] + matched.add(a['mmsi']) + matched.add(b['mmsi']) + fleet_id += 1 + break - for i, v in enumerate(cluster_vessels): - mmsi = v['mmsi'] - s = 0.0 + # 2차: FC 환적 쌍 탐지 (2척, 0.5NM, 양쪽 저속) + for i in range(len(snapshots)): + if snapshots[i]['mmsi'] in matched: + continue + a = snapshots[i] + for j in range(i + 1, len(snapshots)): + if snapshots[j]['mmsi'] in matched: + continue + b = snapshots[j] + dist = haversine_nm(a['lat'], a['lon'], b['lat'], b['lon']) + if dist > 0.5: + continue + if a['sog'] > 2.0 or b['sog'] > 2.0: + continue - # 기준 1: 최초 시각 (30점) - ts_rank = timestamps[i] - min_ts - s += 30.0 * (1.0 - min(ts_rank, 7200) / 7200) + fleets[fleet_id] = [ + {**a, 'role': 'LEADER', 'pattern': 'TRANSSHIP'}, + {**b, 'role': 'MEMBER', 'pattern': 'TRANSSHIP'}, + ] + matched.add(a['mmsi']) + matched.add(b['mmsi']) + fleet_id += 1 + break - # 기준 2: 총톤수 (25점) — 외부 DB 연동 전까지 균등 배점 - s += 12.5 + # 3차: PS 선망 선단 탐지 (3~10척, 2NM 이내 클러스터) + unmatched = [s for s in snapshots if s['mmsi'] not in matched] + for anchor in unmatched: + if anchor['mmsi'] in matched: + continue + nearby = [] + for other in unmatched: + if other['mmsi'] == anchor['mmsi'] or other['mmsi'] in matched: + continue + dist = haversine_nm(anchor['lat'], anchor['lon'], other['lat'], other['lon']) + if dist <= 2.0: + nearby.append(other) - # 기준 3: 클러스터 중심 근접성 (20점) - dist_center = haversine_nm(v['lat'], v['lon'], centroid_lat, centroid_lon) - s += 20.0 * (1.0 - min(dist_center, 10) / 10) + if len(nearby) < 2: # 본인 포함 3척 이상 + continue - # 기준 4: 기선 최근접 (15점) - dist_base = dist_to_baseline(v['lat'], v['lon']) - s += 15.0 * (1.0 - min(dist_base, 12) / 12) + # 역할 분류: 고속(모선), 정지(조명선), 나머지(멤버) + members = [{**anchor, 'role': 'LEADER', 'pattern': 'PURSE_SEINE'}] + matched.add(anchor['mmsi']) + for n in nearby[:9]: # 최대 10척 + if n['sog'] < 0.5: + role = 'LIGHTING' + else: + role = 'MEMBER' + members.append({**n, 'role': role, 'pattern': 'PURSE_SEINE'}) + matched.add(n['mmsi']) - # 기준 5: AIS 소실 이력 (10점) — 이력 없으면 만점 - s += 10.0 + fleets[fleet_id] = members + fleet_id += 1 - scores[mmsi] = round(s, 2) - - lead_mmsi = max(scores, key=lambda k: scores[k]) - score_vals = sorted(scores.values(), reverse=True) - - if len(score_vals) > 1 and score_vals[0] - score_vals[1] > 15: - confidence = 'HIGH' - elif len(score_vals) > 1 and score_vals[0] - score_vals[1] > 8: - confidence = 'MED' - else: - confidence = 'LOW' - - return { - 'lead_mmsi': lead_mmsi, - 'lead_score': scores[lead_mmsi], - 'all_scores': scores, - 'confidence': confidence, - } + logger.info('fleet detection: %d fleets found (%d vessels matched)', + len(fleets), len(matched)) + return fleets def assign_fleet_roles( vessel_dfs: dict[str, pd.DataFrame], cluster_map: dict[str, int], ) -> dict[str, dict]: - """선단 역할 할당: LEADER/MEMBER/NOISE.""" + """선단 역할 할당 — 패턴 매칭 기반. + + cluster_map은 파이프라인에서 전달되지만, 여기서는 vessel_dfs로 직접 패턴 탐지. + """ + fleets = detect_fleet_patterns(vessel_dfs) + results: dict[str, dict] = {} - # 클러스터별 그룹핑 - clusters: dict[int, list[str]] = {} - for mmsi, cid in cluster_map.items(): - clusters.setdefault(cid, []).append(mmsi) + # 매칭된 선박 (fleet_id를 cluster_id로 사용) + fleet_mmsis: set[str] = set() + for fid, members in fleets.items(): + for m in members: + fleet_mmsis.add(m['mmsi']) + results[m['mmsi']] = { + 'cluster_id': fid, + 'cluster_size': len(members), + 'is_leader': m['role'] == 'LEADER', + 'fleet_role': m['role'], + } - for cid, mmsi_list in clusters.items(): - if cid == -1: - for mmsi in mmsi_list: - results[mmsi] = { - 'cluster_size': 0, - 'is_leader': False, - 'fleet_role': 'NOISE', - } - continue - - cluster_size = len(mmsi_list) - - # 스냅샷 생성 (각 선박의 마지막 포인트) - snapshots: list[dict] = [] - for mmsi in mmsi_list: - df = vessel_dfs.get(mmsi) - if df is not None and len(df) > 0: - last = df.iloc[-1] - snapshots.append({ - 'mmsi': mmsi, - 'lat': last['lat'], - 'lon': last['lon'], - 'timestamp': last.get('timestamp', pd.Timestamp.now()), - }) - - lead_info = identify_lead_vessel(snapshots) if len(snapshots) >= 2 else {} - lead_mmsi = lead_info.get('lead_mmsi') - - for mmsi in mmsi_list: + # 매칭 안 된 선박 → NOISE (cluster_id = -1) + for mmsi in vessel_dfs: + if mmsi not in fleet_mmsis: results[mmsi] = { - 'cluster_size': cluster_size, - 'is_leader': mmsi == lead_mmsi, - 'fleet_role': 'LEADER' if mmsi == lead_mmsi else 'MEMBER', + 'cluster_id': -1, + 'cluster_size': 0, + 'is_leader': False, + 'fleet_role': 'NOISE', } return results diff --git a/prediction/algorithms/location.py b/prediction/algorithms/location.py index 44ccf86..e2dfddd 100644 --- a/prediction/algorithms/location.py +++ b/prediction/algorithms/location.py @@ -10,6 +10,7 @@ TERRITORIAL_SEA_NM = 12.0 CONTIGUOUS_ZONE_NM = 24.0 _baseline_points: Optional[List[Tuple[float, float]]] = None +_zone_polygons: Optional[list] = None def _load_baseline() -> List[Tuple[float, float]]: @@ -46,10 +47,91 @@ def dist_to_baseline(vessel_lat: float, vessel_lon: float, return min_dist -def classify_zone(vessel_lat: float, vessel_lon: float) -> dict: - """선박 위치 수역 분류.""" - dist = dist_to_baseline(vessel_lat, vessel_lon) +def _epsg3857_to_wgs84(x: float, y: float) -> Tuple[float, float]: + """EPSG:3857 (Web Mercator) → WGS84 변환.""" + lon = x / (math.pi * 6378137) * 180 + lat = math.atan(math.exp(y / 6378137)) * 360 / math.pi - 90 + return lat, lon + +def _load_zone_polygons() -> list: + """특정어업수역 Ⅰ~Ⅳ GeoJSON 로드 + EPSG:3857→WGS84 변환.""" + global _zone_polygons + if _zone_polygons is not None: + return _zone_polygons + + zone_dir = Path(__file__).parent.parent / 'data' / 'zones' + zones_meta = [ + ('ZONE_I', '수역Ⅰ(동해)', ['PS', 'FC'], '특정어업수역Ⅰ.json'), + ('ZONE_II', '수역Ⅱ(남해)', ['PT', 'OT', 'GN', 'PS', 'FC'], '특정어업수역Ⅱ.json'), + ('ZONE_III', '수역Ⅲ(서남해)', ['PT', 'OT', 'GN', 'PS', 'FC'], '특정어업수역Ⅲ.json'), + ('ZONE_IV', '수역Ⅳ(서해)', ['GN', 'PS', 'FC'], '특정어업수역Ⅳ.json'), + ] + result = [] + for zone_id, name, allowed, filename in zones_meta: + filepath = zone_dir / filename + if not filepath.exists(): + continue + with open(filepath, 'r') as f: + data = json.load(f) + multi_coords = data['features'][0]['geometry']['coordinates'] + wgs84_polys = [] + for poly in multi_coords: + wgs84_rings = [] + for ring in poly: + wgs84_rings.append([_epsg3857_to_wgs84(x, y) for x, y in ring]) + wgs84_polys.append(wgs84_rings) + result.append({ + 'id': zone_id, 'name': name, 'allowed': allowed, + 'polygons': wgs84_polys, + }) + _zone_polygons = result + return result + + +def _point_in_polygon(lat: float, lon: float, ring: list) -> bool: + """Ray-casting point-in-polygon.""" + n = len(ring) + inside = False + j = n - 1 + for i in range(n): + yi, xi = ring[i] + yj, xj = ring[j] + if ((yi > lat) != (yj > lat)) and (lon < (xj - xi) * (lat - yi) / (yj - yi) + xi): + inside = not inside + j = i + return inside + + +def _point_in_multipolygon(lat: float, lon: float, polygons: list) -> bool: + """MultiPolygon 내 포함 여부 (외곽 링 in + 내곽 링 hole 제외).""" + for poly in polygons: + outer = poly[0] + if _point_in_polygon(lat, lon, outer): + for hole in poly[1:]: + if _point_in_polygon(lat, lon, hole): + return False + return True + return False + + +def classify_zone(vessel_lat: float, vessel_lon: float) -> dict: + """선박 위치 수역 분류 — 특정어업수역 Ⅰ~Ⅳ 폴리곤 기반.""" + zones = _load_zone_polygons() + + for z in zones: + if _point_in_multipolygon(vessel_lat, vessel_lon, z['polygons']): + dist = dist_to_baseline(vessel_lat, vessel_lon) + return { + 'zone': z['id'], + 'zone_name': z['name'], + 'allowed_gears': z['allowed'], + 'dist_from_baseline_nm': round(dist, 2), + 'violation': False, + 'alert_level': 'WATCH', + } + + dist = dist_to_baseline(vessel_lat, vessel_lon) if dist <= TERRITORIAL_SEA_NM: return { 'zone': 'TERRITORIAL_SEA', diff --git a/prediction/algorithms/risk.py b/prediction/algorithms/risk.py index b11b3c0..d0c58b2 100644 --- a/prediction/algorithms/risk.py +++ b/prediction/algorithms/risk.py @@ -32,6 +32,10 @@ def compute_vessel_risk_score( score += 40 elif zone == 'CONTIGUOUS_ZONE': score += 10 + elif zone.startswith('ZONE_'): + # 특정어업수역 내 — 무허가면 가산 + if is_permitted is not None and not is_permitted: + score += 25 # 2. 조업 행위 (최대 30점) segs = detect_fishing_segments(df_vessel) diff --git a/prediction/algorithms/track_similarity.py b/prediction/algorithms/track_similarity.py new file mode 100644 index 0000000..0212f98 --- /dev/null +++ b/prediction/algorithms/track_similarity.py @@ -0,0 +1,160 @@ +"""궤적 유사도 — DTW(Dynamic Time Warping) 기반.""" +import math + +_MAX_RESAMPLE_POINTS = 50 + + +def haversine_m(lat1: float, lon1: float, lat2: float, lon2: float) -> float: + """두 좌표 간 거리 (미터).""" + R = 6371000 + phi1, phi2 = math.radians(lat1), math.radians(lat2) + dphi = math.radians(lat2 - lat1) + dlam = math.radians(lon2 - lon1) + a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlam / 2) ** 2 + return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) + + +def _resample(track: list[tuple[float, float]], n: int) -> list[tuple[float, float]]: + """궤적을 n 포인트로 균등 리샘플링 (선형 보간).""" + if len(track) == 0: + return [] + if len(track) == 1: + return [track[0]] * n + if len(track) <= n: + return list(track) + + # 누적 거리 계산 + cumulative = [0.0] + for i in range(1, len(track)): + d = haversine_m(track[i - 1][0], track[i - 1][1], track[i][0], track[i][1]) + cumulative.append(cumulative[-1] + d) + + total_dist = cumulative[-1] + if total_dist == 0.0: + return [track[0]] * n + + step = total_dist / (n - 1) + result: list[tuple[float, float]] = [] + + seg = 0 + for k in range(n): + target = step * k + # 해당 target 거리에 해당하는 선분 찾기 + while seg < len(cumulative) - 2 and cumulative[seg + 1] < target: + seg += 1 + seg_len = cumulative[seg + 1] - cumulative[seg] + if seg_len == 0.0: + result.append(track[seg]) + else: + t = (target - cumulative[seg]) / seg_len + lat = track[seg][0] + t * (track[seg + 1][0] - track[seg][0]) + lon = track[seg][1] + t * (track[seg + 1][1] - track[seg][1]) + result.append((lat, lon)) + + return result + + +def _dtw_distance( + track_a: list[tuple[float, float]], + track_b: list[tuple[float, float]], +) -> float: + """두 궤적 간 DTW 거리 (미터 단위 평균 거리).""" + n, m = len(track_a), len(track_b) + if n == 0 or m == 0: + return float('inf') + + INF = float('inf') + # 1D 롤링 DP (공간 최적화) + prev = [INF] * (m + 1) + prev[0] = 0.0 + # 첫 행 초기화 + row = [INF] * (m + 1) + row[0] = INF + + dp_prev = [INF] * (m + 1) + dp_curr = [INF] * (m + 1) + dp_prev[0] = 0.0 + for j in range(1, m + 1): + dp_prev[j] = INF + + for i in range(1, n + 1): + dp_curr[0] = INF + for j in range(1, m + 1): + cost = haversine_m(track_a[i - 1][0], track_a[i - 1][1], + track_b[j - 1][0], track_b[j - 1][1]) + min_prev = min(dp_curr[j - 1], dp_prev[j], dp_prev[j - 1]) + dp_curr[j] = cost + min_prev + dp_prev, dp_curr = dp_curr, [INF] * (m + 1) + + # dp_prev는 마지막으로 계산된 행 + total = dp_prev[m] + if total == INF: + return INF + return total / (n + m) + + +def compute_track_similarity( + track_a: list[tuple[float, float]], + track_b: list[tuple[float, float]], + max_dist_m: float = 10000.0, +) -> float: + """두 궤적의 DTW 거리 기반 유사도 (0~1). + + track이 비어있으면 0.0 반환. + 유사할수록 1.0에 가까움. + """ + if not track_a or not track_b: + return 0.0 + + a = _resample(track_a, _MAX_RESAMPLE_POINTS) + b = _resample(track_b, _MAX_RESAMPLE_POINTS) + + avg_dist = _dtw_distance(a, b) + if avg_dist == float('inf') or max_dist_m <= 0.0: + return 0.0 + + similarity = 1.0 - (avg_dist / max_dist_m) + return max(0.0, min(1.0, similarity)) + + +def match_gear_by_track( + gear_tracks: dict[str, list[tuple[float, float]]], + vessel_tracks: dict[str, list[tuple[float, float]]], + threshold: float = 0.6, +) -> list[dict]: + """어구 궤적을 선단 선박 궤적과 비교하여 매칭. + + Args: + gear_tracks: mmsi → [(lat, lon), ...] — 어구 궤적 + vessel_tracks: mmsi → [(lat, lon), ...] — 선박 궤적 + threshold: 유사도 하한 (이상이면 매칭) + + Returns: + [{gear_mmsi, vessel_mmsi, similarity, match_method: 'TRACK_SIMILAR'}] + """ + results: list[dict] = [] + + for gear_mmsi, g_track in gear_tracks.items(): + if not g_track: + continue + + best_mmsi: str | None = None + best_sim = -1.0 + + for vessel_mmsi, v_track in vessel_tracks.items(): + if not v_track: + continue + sim = compute_track_similarity(g_track, v_track) + if sim > best_sim: + best_sim = sim + best_mmsi = vessel_mmsi + + if best_mmsi is not None and best_sim >= threshold: + results.append({ + 'gear_mmsi': gear_mmsi, + 'vessel_mmsi': best_mmsi, + 'similarity': best_sim, + 'match_method': 'TRACK_SIMILAR', + }) + + return results diff --git a/prediction/cache/vessel_store.py b/prediction/cache/vessel_store.py index 8a59536..550ba89 100644 --- a/prediction/cache/vessel_store.py +++ b/prediction/cache/vessel_store.py @@ -113,12 +113,29 @@ class VesselStore: for mmsi, group in df_all.groupby('mmsi'): self._tracks[str(mmsi)] = group.reset_index(drop=True) + # last_bucket 설정 — incremental fetch 시작점 + if 'time_bucket' in df_all.columns and not df_all['time_bucket'].dropna().empty: + max_bucket = pd.to_datetime(df_all['time_bucket'].dropna()).max() + if hasattr(max_bucket, 'to_pydatetime'): + max_bucket = max_bucket.to_pydatetime() + if isinstance(max_bucket, datetime) and max_bucket.tzinfo is None: + max_bucket = max_bucket.replace(tzinfo=timezone.utc) + self._last_bucket = max_bucket + elif 'timestamp' in df_all.columns and not df_all['timestamp'].dropna().empty: + max_ts = pd.to_datetime(df_all['timestamp'].dropna()).max() + if hasattr(max_ts, 'to_pydatetime'): + max_ts = max_ts.to_pydatetime() + if isinstance(max_ts, datetime) and max_ts.tzinfo is None: + max_ts = max_ts.replace(tzinfo=timezone.utc) + self._last_bucket = max_ts + vessel_count = len(self._tracks) point_count = sum(len(v) for v in self._tracks.values()) logger.info( - 'initial load complete: %d vessels, %d total points', + 'initial load complete: %d vessels, %d total points, last_bucket=%s', vessel_count, point_count, + self._last_bucket, ) self.refresh_static_info() diff --git a/prediction/data/zones/특정어업수역Ⅰ.json b/prediction/data/zones/특정어업수역Ⅰ.json new file mode 100644 index 0000000..f0454ef --- /dev/null +++ b/prediction/data/zones/특정어업수역Ⅰ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed1", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2160", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[14612352.95900835, 4323569.555957972], [14550748.752774281, 4260105.381317261], [14544627.066163512, 4252568.169285575], [14439940.71936106, 4252568.1692174645], [14440259.902536998, 4254382.900417306], [14440565.249736432, 4256577.976660408], [14441200.37191117, 4258322.323996074], [14442128.627396706, 4261947.246114864], [14442446.188484557, 4263842.912458916], [14443081.310658677, 4265407.837348879], [14443838.571713375, 4268086.787104008], [14444461.480000762, 4270299.674084436], [14445414.16326165, 4272528.068283305], [14446488.98540456, 4275811.262361098], [14447111.893690424, 4279125.58051958], [14447441.668665636, 4283375.409280503], [14447441.668666717, 4285908.011073243], [14447747.015866045, 4287008.670616378], [14449298.17963799, 4289692.937620907], [14451325.68504222, 4294897.478106175], [14452583.715503562, 4299470.4800484385], [14452583.715504179, 4299666.724555172], [14452864.634927392, 4301297.200756734], [14452803.565487338, 4303864.187591692], [14452864.634926995, 4306733.892102277], [14452681.426607274, 4309982.105854211], [14452229.512752429, 4313034.803597244], [14451289.043378348, 4315906.938650241], [14450165.365684237, 4319883.836509038], [14448650.843575954, 4323816.808519151], [14447172.963130116, 4326268.076469068], [14445646.22713406, 4328477.720500556], [14443166.807874365, 4331384.242207033], [14440455.324744267, 4333928.090897115], [14438366.74990012, 4335578.885027033], [14435545.341778146, 4337381.416707463], [14435212.858055448, 4337568.367409996], [14433713.258582642, 4338411.570116835], [14431881.17538587, 4339153.947573591], [14430305.583837628, 4339729.704068462], [14430281.156061549, 4340669.162281877], [14432430.800344449, 4344124.648762426], [14433664.40302976, 4347050.554454509], [14434299.525204673, 4348582.044935347], [14435398.775122227, 4352055.241843149], [14436168.250064695, 4355377.820309184], [14436473.59726421, 4359156.794673912], [14436632.377808044, 4361358.014828757], [14437096.505551115, 4363255.98064219], [14438036.97492467, 4367341.541713113], [14438354.536012871, 4371595.823810485], [14438183.541581359, 4375213.2915699165], [14437218.644430902, 4379455.492532148], [14436754.516687542, 4381676.095802414], [14437218.644430652, 4383410.311934654], [14438940.802635401, 4387670.983955953], [14440333.18586435, 4392085.61379955], [14440821.741384165, 4395862.331199512], [14440968.308038985, 4399000.459396788], [14441114.874694768, 4403419.77764905], [14441273.655239271, 4409426.891672738], [14440772.885832038, 4413972.594613021], [14439991.197000964, 4416642.956092686], [14438891.94708353, 4419329.253028348], [14437621.702733777, 4422642.269553811], [14436046.111184767, 4426582.592213162], [14435117.855699727, 4428767.199310407], [14434946.861267168, 4430203.479647634], [14434946.86126783, 4432709.795922216], [14434470.519636003, 4435537.753788483], [14434617.086292291, 4437433.672085769], [14434617.08629216, 4439314.635756483], [14434922.433492135, 4440431.137925814], [14435545.341778962, 4443276.456208943], [14435862.902865293, 4448617.316293129], [14435374.347346198, 4453195.1551512005], [14434433.877972556, 4456978.343560426], [14433493.582733309, 4459337.341378763], [14433493.408598367, 4459337.778245751], [14432540.725337084, 4461222.640432275], [14430134.5894049, 4464840.066412149], [14429071.98115172, 4466066.593934474], [14429377.32835093, 4468244.031514878], [14429487.253342932, 4471664.436166196], [14429487.253342314, 4474871.114707357], [14428962.056159778, 4478201.569995016], [14428339.14787256, 4480581.111676515], [14427423.10627465, 4482961.1914115725], [14426482.636900885, 4485234.2862444], [14424332.992617503, 4488229.992534473], [14422684.117740594, 4490519.594851016], [14421218.451183844, 4492071.890874874], [14420192.484594172, 4493424.573415368], [14418641.32082132, 4495484.674666911], [14415856.554362778, 4498806.286043997], [14415123.721083565, 4501082.812019949], [14413291.637888283, 4505437.2749514375], [14411545.051908031, 4508454.147134834], [14409053.418760045, 4511333.299393252], [14407502.25498812, 4512996.485903551], [14405743.455119297, 4516308.245790257], [14404192.291346865, 4519020.04815391], [14402445.705366325, 4520776.931720719], [14401199.88879329, 4522117.909890488], [14401163.247128874, 4522441.620044785], [14400674.691610316, 4526804.977928812], [14399135.74172542, 4530630.196696397], [14397889.92515128, 4533530.893432845], [14396448.686370509, 4536216.299453886], [14394482.250406215, 4538609.053820163], [14393969.267111044, 4540878.818581772], [14393248.647720557, 4543272.6434066], [14391697.483947853, 4546578.5701886], [14389950.897967545, 4549576.439029636], [14387996.67589144, 4552080.475715166], [14386433.298231108, 4553935.706287525], [14384686.712251175, 4555296.4190314105], [14384063.803963741, 4555698.481870257], [14382817.987391062, 4558497.888716806], [14382023.990793852, 4559433.839678707], [14379825.584836785, 4562025.286767265], [14377993.501640612, 4563882.293207642], [14375709.504589545, 4565538.404034689], [14373230.085329972, 4569687.579878523], [14370848.377174843, 4572490.760700769], [14369101.79119466, 4574132.75433843], [14367978.113501683, 4575372.167021386], [14367025.430239363, 4578161.387517195], [14365584.191457871, 4580563.818041922], [14364155.16656508, 4582966.805917671], [14362591.788904771, 4585230.77563233], [14360136.797420906, 4586983.366182029], [14359501.67524686, 4589031.016082314], [14357755.089266032, 4591730.810433944], [14356924.544884088, 4593049.931812809], [14355397.808887746, 4596589.112554951], [14353321.447931753, 4599694.655522917], [14351355.011967713, 4602086.560165967], [14350548.895361273, 4602909.876326975], [14349058.80102805, 4604059.521970236], [14348362.609413402, 4605286.987737952], [14347006.867848393, 4607571.395078686], [14345260.281868543, 4610058.400036733], [14344344.240270587, 4614925.339107906], [14344178.729650684, 4615426.598601238], [14406264.563155375, 4615426.598601238], [14471145.302268442, 4615426.598601238], [14489820.50078106, 4579817.049246806], [14657058.866457367, 4579819.039140932], [14657058.866471501, 4498513.035634587], [14653280.330118885, 4484660.955595197], [14653257.89496764, 4484604.528273547], [14653111.328311926, 4484251.265963233], [14652952.547767457, 4483867.298646529], [14652805.981111629, 4483498.703204842], [14652793.767223712, 4483437.271887497], [14652732.697783664, 4483283.695161308], [14652573.91724023, 4482899.763155043], [14652463.992248593, 4482531.201607391], [14652317.425592588, 4482147.2970551355], [14652183.072824666, 4481778.761862163], [14652085.361720739, 4481394.884757528], [14651963.222840805, 4481011.021654676], [14651853.297849245, 4480627.172552061], [14651731.158969458, 4480243.337446124], [14651645.661754325, 4479828.811251124], [14651560.164537868, 4479460.357225606], [14651450.239546517, 4479061.213248133], [14651376.956218421, 4478677.435232205], [14651303.672890497, 4478278.320934065], [14651230.38956299, 4477894.571451181], [14651181.534010744, 4477495.486823346], [14651120.464571165, 4477096.417317753], [14651059.395131014, 4476712.710899764], [14650998.325691152, 4476313.671051835], [14650996.99554685, 4476290.271738564], [14645926.917379338, 4457703.411105691], [14630424.731020536, 4444761.216899179], [14601399.121065676, 4420528.823331998], [14513278.61218791, 4420528.823356817], [14513278.612126667, 4323569.5559741575], [14612352.95900835, 4323569.555957972]], [[14531705.281810218, 4513797.373626424], [14531693.067921922, 4513597.145949486], [14531680.854034334, 4513381.520424651], [14531680.854033662, 4513196.702081734], [14531656.42625728, 4512981.084804853], [14531631.99848197, 4512596.064997208], [14531631.998481335, 4512395.860290817], [14531631.998482231, 4512180.259503853], [14531619.784594133, 4512010.861999429], [14531619.784593917, 4511795.269138], [14531619.784594417, 4511579.680716071], [14531619.784594513, 4511394.89417161], [14531619.784594564, 4511194.712427556], [14531631.998481907, 4510979.136366602], [14531631.99848219, 4510794.36041791], [14531631.998481516, 4510578.792596463], [14531656.426257836, 4510193.861093197], [14531680.854033632, 4509993.7023007], [14531680.854033832, 4509793.547333112], [14531693.067922262, 4509593.396189629], [14531705.28180977, 4509393.248870632], [14531741.923473405, 4509193.105373775], [14531754.137361716, 4508992.965701181], [14531778.565137729, 4508608.0924608875], [14531827.420689235, 4508207.83928627], [14531888.490129804, 4507807.60139917], [14531937.345681723, 4507407.378797934], [14531986.201233227, 4507022.563787677], [14532071.698448502, 4506622.371163723], [14532144.981777469, 4506222.193819813], [14532218.26510484, 4505822.031752334], [14532291.54843238, 4505437.274939068], [14532389.259536454, 4505052.532246742], [14532486.970639894, 4504667.803672876], [14532584.681744624, 4504283.089215654], [14532682.3928476, 4503898.388874675], [14532804.5317276, 4503498.31548994], [14532816.74561534, 4503467.54124596], [14532841.173391888, 4503236.737298468], [14532890.028944094, 4502836.689154722], [14532951.098383617, 4502436.656270566], [14533036.595599566, 4502036.638641951], [14533085.451150997, 4501652.020693331], [14533170.94836721, 4501252.032985861], [14533256.445583222, 4500882.827099174], [14533329.728910444, 4500467.486007207], [14533427.440014655, 4500082.925582424], [14533512.937230268, 4499698.379251896], [14533537.365005817, 4499636.85314637], [14533635.07610988, 4499313.847011714], [14533720.573326208, 4498929.328863146], [14533842.71220557, 4498544.824803407], [14533964.851085538, 4498160.334828932], [14534086.989965722, 4497775.8589393], [14534221.342733555, 4497406.77533439], [14534343.481613029, 4497022.3270443855], [14534490.048267843, 4496653.269931789], [14534636.614923632, 4496284.225792352], [14534795.395467635, 4495899.818607236], [14534941.962123044, 4495530.800950158], [14535088.528779598, 4495161.796260659], [14535271.737098787, 4494808.178934617], [14535442.73153039, 4494454.573515013], [14535625.939850742, 4494085.606640488], [14535821.362058103, 4493732.025548209], [14535992.356489455, 4493363.084053765], [14536187.77869705, 4493024.899068225], [14536370.987016352, 4492671.353679356], [14536578.623112632, 4492333.190962355], [14536798.473096136, 4491979.668849295], [14537006.10919119, 4491656.8981793765], [14537225.959175108, 4491303.398821744], [14537421.381382758, 4490980.648924496], [14537653.44525381, 4490642.540617242], [14537909.936901638, 4490319.811016954], [14538129.786885347, 4489997.091326332], [14538374.0646448, 4489659.014660098], [14538618.342404164, 4489351.681672938], [14538850.406276468, 4489044.357672096], [14539131.325698882, 4488721.677141822], [14539363.389571583, 4488429.736623869], [14539644.308994643, 4488122.43957564], [14539900.800642235, 4487815.151508973], [14539986.297858382, 4487722.966840681], [14540010.725633759, 4487692.23879659], [14540267.217281476, 4487400.32686548], [14540511.495040976, 4487077.696790041], [14540780.200575706, 4486770.43925529], [14541073.333887419, 4486493.915150635], [14541329.825535271, 4486186.674672224], [14541598.531071173, 4485910.165917298], [14541867.23660634, 4485618.303450893], [14542148.156029876, 4485326.449084601], [14542453.503228514, 4485049.962943723], [14542734.422652928, 4484773.484071729], [14543039.76985148, 4484512.371808684], [14543332.903163565, 4484251.266027703], [14543638.250362527, 4483974.808145135], [14543943.59756212, 4483713.715705612], [14544248.944761822, 4483467.987564921], [14544566.505848715, 4483222.26516165], [14544871.85304831, 4482976.548497614], [14545213.841912381, 4482746.194336815], [14545519.189110842, 4482515.845218762], [14545836.750198007, 4482254.788980392], [14546190.952949973, 4482024.450618634], [14546508.514037313, 4481824.828116437], [14546838.28901309, 4481609.854272898], [14547180.27787654, 4481394.88481907], [14547510.052850928, 4481179.919756588], [14547876.469490254, 4480995.667482875], [14548218.458354343, 4480796.06449436], [14548560.447216801, 4480596.465289407], [14548926.863856547, 4480412.223228773], [14549293.280496065, 4480212.631302457], [14549647.483247736, 4480043.748782027], [14549989.472111017, 4479874.868972281], [14550355.88875037, 4479721.344221679], [14550624.594284926, 4479598.526033944], [14550856.658157144, 4479491.061295501], [14551198.647020763, 4479291.48683264], [14551565.063659478, 4479107.26761045], [14551919.266411318, 4478923.051610395], [14552285.683050882, 4478769.540739722], [14552627.671913499, 4478600.681367721], [14553006.302441431, 4478431.8247021455], [14553372.719079891, 4478278.320992562], [14553739.135719031, 4478124.8195214365], [14554117.766246844, 4477986.670110905], [14554496.39677429, 4477833.172889764], [14554875.02730134, 4477710.376725644], [14555253.657828972, 4477572.232751319], [14555620.074468583, 4477464.788689573], [14555998.704995206, 4477326.647937234], [14556377.335522415, 4477234.555108875], [14556511.688290423, 4477157.811699739], [14556878.10492996, 4476958.281453992], [14557220.093792727, 4476804.799222015], [14557598.724319693, 4476651.319227265], [14557952.92707147, 4476482.493814672], [14558331.557598298, 4476329.018516568], [14558697.974237733, 4476175.54545325], [14559076.604765655, 4476022.0746261515], [14559443.02140455, 4475883.952793535], [14559821.65193224, 4475761.179352402], [14560200.282459686, 4475638.407340859], [14560578.912986, 4475500.290538945], [14560957.54351379, 4475377.521568426], [14561348.38792862, 4475270.099892532], [14561727.01845645, 4475147.333604416], [14562117.862871993, 4475055.259828022], [14562484.27951158, 4474963.186855402], [14562899.551702326, 4474871.1146875555], [14563290.396117546, 4474809.733688274], [14563669.026644476, 4474733.007943433], [14564059.871060286, 4474656.282757424], [14564450.7154752, 4474579.558129849], [14564853.773778029, 4474518.178830546], [14565244.618193747, 4474456.79988746], [14565635.462609466, 4474395.4213030925], [14565843.09870468, 4474380.076712145], [14566026.307024052, 4474364.732145027], [14566429.365327647, 4474303.354096425], [14566624.787535438, 4474303.354096845], [14566820.209743189, 4474288.009639743], [14567040.059725929, 4474272.665205484], [14567235.481933901, 4474241.97640493], [14567430.904141523, 4474241.9764056895], [14567638.540236901, 4474226.632038639], [14567821.748556953, 4474226.632038577], [14568029.384652914, 4474211.287694249], [14568212.592971867, 4474211.287693342], [14568420.229068192, 4474195.943371697], [14568627.865163937, 4474195.943371153], [14568811.073482776, 4474195.943371623], [14569018.709579367, 4474195.94337142], [14569214.131786728, 4474195.943370581], [14569238.559562922, 4474195.943371392], [14569409.553994717, 4474195.943371547], [14569629.403978188, 4474195.943370894], [14569824.826185605, 4474211.287692922], [14570020.248392954, 4474211.287693488], [14570215.670600649, 4474211.287693272], [14570423.306696696, 4474226.632037414], [14570618.728903888, 4474226.632037795], [14570814.151111197, 4474241.97640376], [14571021.787206706, 4474272.665204512], [14571204.995527001, 4474288.009638165], [14571412.631621836, 4474288.009637828], [14571620.267718533, 4474303.354094652], [14572023.326021364, 4474364.732141971], [14572194.32045334, 4474380.076710127], [14572414.170436617, 4474395.421300662], [14572805.01485221, 4474456.799885332], [14573208.073155506, 4474518.178827347], [14573598.917569762, 4474579.558126053], [14573989.761985833, 4474656.282754001], [14574368.392513085, 4474733.007939714], [14574759.236928629, 4474809.733685015], [14575162.295230972, 4474871.114683236], [14575553.139646066, 4474963.186850651], [14575931.770173518, 4475055.259823188], [14576322.614588926, 4475147.3335995795], [14576701.245116178, 4475270.0998873925], [14577079.875643862, 4475377.521562786], [14577470.720059728, 4475500.290533416], [14577837.13669783, 4475638.407335261], [14578215.767225962, 4475761.179345143], [14578594.39775321, 4475883.95278683], [14578973.028280452, 4476022.074619254], [14579327.231032163, 4476175.54544557], [14579705.861558419, 4476329.018508264], [14580084.492086556, 4476482.493807283], [14580450.908725094, 4476651.3192181215], [14580805.111476777, 4476804.799213971], [14581183.742004093, 4476958.281445817], [14581525.730867168, 4477157.811690843], [14581879.933618782, 4477326.647927245], [14582234.136370221, 4477526.185151994], [14582588.3391218, 4477710.376715044], [14582930.327984763, 4477909.9212099165], [14583296.744624889, 4478094.119485319], [14583626.51959986, 4478309.021544471], [14583968.508463632, 4478539.278618473], [14584298.28343856, 4478754.189764326], [14584615.844526524, 4478969.105295983], [14584957.833389819, 4479199.376805741], [14585275.39447627, 4479429.653352519], [14585592.955563627, 4479659.934935787], [14585922.730539948, 4479890.221556971], [14586240.291627208, 4480135.866171305], [14586557.8527144, 4480396.869855059], [14586863.199913831, 4480642.526293688], [14587156.33322539, 4480888.188466244], [14587473.894312968, 4481164.565263089], [14587767.027623786, 4481425.59445718], [14588060.160935674, 4481701.985366741], [14588353.294247115, 4481978.383535463], [14588646.427559184, 4482254.788964908], [14588939.56086965, 4482546.558126468], [14589208.266406132, 4482822.978480075], [14589476.971941242, 4483114.763399049], [14589733.46358897, 4483421.914160338], [14590014.383012347, 4483698.35751204], [14590185.377443707, 4483898.0155623555], [14590331.944099901, 4484020.883937888], [14590661.719074445, 4484251.266009965], [14590979.280162634, 4484497.0124473], [14591296.841249231, 4484727.404948229], [14591602.18844864, 4484973.162509527], [14591919.749536166, 4485234.28621192], [14592212.882847624, 4485495.41639687], [14592518.23004711, 4485756.553064018], [14592823.577246739, 4486017.696217524], [14593116.710558899, 4486294.207799355], [14593409.843869546, 4486570.726653404], [14593690.763293965, 4486847.252777424], [14593983.89660544, 4487139.149354413], [14594252.602139814, 4487431.054034741], [14594533.52156419, 4487692.238776791], [14594802.227099039, 4487999.523250918], [14595058.718747094, 4488306.8167062495], [14595339.638170302, 4488598.753809731], [14595583.915929569, 4488906.064781649], [14595828.19368928, 4489213.384739871], [14596096.899225544, 4489536.080365369], [14596341.176984914, 4489858.785899267], [14596573.240855644, 4490181.501342655], [14596805.304727584, 4490488.858596873], [14597037.368599355, 4490826.961959009], [14597257.21858321, 4491149.707135566], [14597489.282454617, 4491472.462225323], [14597696.918549843, 4491825.967271368], [14597928.98242226, 4492148.743134879], [14598136.618516896, 4492502.270936908], [14598319.826836484, 4492840.439096991], [14598527.462932773, 4493193.990176503], [14598722.885140764, 4493547.553156926], [14598906.093460135, 4493901.128039849], [14599101.515668057, 4494254.714825081], [14599260.296211885, 4494623.687640952], [14599431.290642768, 4494977.2987543205], [14599590.07118727, 4495330.921775762], [14599773.279505912, 4495715.308132935], [14599919.846161587, 4496068.956009824], [14600078.626705699, 4496453.369387598], [14600225.193361096, 4496822.419472711], [14600347.33224078, 4497206.860440532], [14600481.685009632, 4497575.937016725], [14600616.037777228, 4497960.4055835055], [14600750.39054488, 4498344.888232693], [14600860.31553649, 4498714.004829062], [14600982.454416526, 4499098.51508797], [14601080.165519364, 4499483.0394360265], [14601202.304399468, 4499867.577874515], [14601300.01550408, 4500267.512801375], [14601385.512719708, 4500652.079992499], [14601458.796047695, 4501052.044824842], [14601471.009935707, 4501175.113995761], [14601654.218255237, 4501498.177436593], [14601800.784910476, 4501867.404980572], [14601971.779342758, 4502236.64552286], [14602118.34599798, 4502590.513240334], [14602277.126541303, 4502959.779238895], [14602423.693198, 4503344.4451490035], [14602558.045964886, 4503698.350247777], [14602680.184845533, 4504083.043249815], [14602814.537612794, 4504467.750366676], [14602936.676492875, 4504852.471599208], [14603058.815372452, 4505237.206950171], [14603180.954252187, 4505606.56617041], [14603266.451468613, 4506022.110849317], [14603388.590348229, 4506391.497726757], [14603474.087563867, 4506791.681535702], [14603547.37089101, 4507161.095535717], [14603645.08199488, 4507561.308730727], [14603718.365323769, 4507946.143524667], [14603791.648650708, 4508330.992452457], [14603864.931978848, 4508746.64517058], [14603926.001419289, 4509131.523501303], [14603987.070858913, 4509531.8119634325], [14604035.92641036, 4509916.719140585], [14604084.781962857, 4510332.43477583], [14604109.20973883, 4510717.37137208], [14604145.85140303, 4510932.942045082], [14604158.065290203, 4511117.720440137], [14604170.279178778, 4511317.900712452], [14604170.279177956, 4511533.483745455], [14604182.493066223, 4511718.272735513], [14604219.134730231, 4511933.864012004], [14604219.134729845, 4512318.859471839], [14604231.348618282, 4512519.062705031], [14604231.348618418, 4512719.269766486], [14604231.348617738, 4512904.079682778], [14604231.348618373, 4513119.695373501], [14604231.348618187, 4513319.913919597], [14604231.348618748, 4513520.136295258], [14604231.348618748, 4513720.362499544], [14604231.348618232, 4513920.592533981], [14604231.348618407, 4514120.826397525], [14604219.134730808, 4514336.467148453], [14604219.134730032, 4514721.550967597], [14604182.493066857, 4514906.396233077], [14604170.27917791, 4515122.053169531], [14604170.279178878, 4515322.310016387], [14604158.065290527, 4515507.165891377], [14604145.851402044, 4515722.835206429], [14604109.209738161, 4515923.103548852], [14604084.781962737, 4516308.245749079], [14604035.926410299, 4516708.808674816], [14603987.070858993, 4517093.9797944045], [14603926.00141845, 4517509.9805284925], [14603864.931978678, 4517895.181143359], [14603791.64865124, 4518295.804826711], [14603718.365322556, 4518681.034375972], [14603645.081995493, 4519097.098223365], [14603547.370891701, 4519466.946648656], [14603474.08756368, 4519867.630533778], [14603388.59034769, 4520237.506207204], [14603266.451468341, 4520638.219615408], [14603180.95425151, 4521008.12254481], [14603058.815372169, 4521408.865484192], [14602936.676491957, 4521778.79567707], [14602814.537612109, 4522164.153545156], [14602680.184844451, 4522534.110462632], [14602558.045964777, 4522919.496172441], [14602423.693197738, 4523289.47982265], [14602277.126541242, 4523674.893382433], [14602118.345997736, 4524044.903771376], [14601971.779341936, 4524414.927259076], [14601800.784909926, 4524784.963848068], [14601654.218254454, 4525155.013540586], [14601471.009935107, 4525525.076335961], [14601300.015503855, 4525864.3120796885], [14601129.021072082, 4526234.399998049], [14600933.598864602, 4526573.658772662], [14600750.390544403, 4526928.350179163], [14600542.754449246, 4527267.63148988], [14600347.332241392, 4527637.769124864], [14600151.910033902, 4527961.6503126025], [14599944.273938052, 4528316.388852858], [14599712.210066758, 4528640.29108562], [14599504.573970841, 4528964.203363799], [14599272.510098685, 4529318.975958536], [14599052.660115397, 4529627.483662931], [14598808.382356219, 4529951.426563321], [14598588.532372601, 4530275.379513526], [14598344.254612468, 4530599.342514882], [14598087.762964793, 4530907.88805286], [14597831.271317031, 4531216.442710067], [14597586.993558556, 4531525.006485532], [14597318.288022641, 4531833.57938278], [14597061.796375385, 4532126.732084111], [14596793.090839129, 4532419.893019322], [14596512.171415407, 4532713.062188578], [14596231.251992663, 4533021.670209015], [14595950.332568703, 4533299.42523091], [14595669.413144821, 4533577.187645228], [14595644.985369092, 4533623.48209964], [14595498.41871387, 4533777.7984313145], [14595376.279834235, 4533916.685081554], [14595119.788186248, 4534225.328697573], [14594838.868762594, 4534518.548590586], [14594582.377115823, 4534811.776722892], [14594289.243803782, 4535089.579398674], [14594008.324380705, 4535367.389470016], [14593727.404956257, 4535660.64146122], [14593446.48553373, 4535923.031808229], [14593153.352221377, 4536200.864075813], [14592848.005022287, 4536478.703743617], [14592567.08559909, 4536741.114669866], [14592249.524511898, 4537003.532198354], [14591944.177311765, 4537250.519432793], [14591614.402336147, 4537497.512515288], [14591309.055137279, 4537729.073843565], [14591003.707938092, 4537991.5162313925], [14590686.146850547, 4538238.5268653], [14590368.585762527, 4538454.665970274], [14590014.383011634, 4538686.248553525], [14589684.608036457, 4538917.836279545], [14589354.833060294, 4539118.549804093], [14589025.058085084, 4539319.267192334], [14588683.069222417, 4539535.42869948], [14588328.866470784, 4539736.154114145], [14587986.877606917, 4539952.324265979], [14587657.102632334, 4540137.616535028], [14587290.685992181, 4540338.353544246], [14586936.483240709, 4540508.210955269], [14586570.066601887, 4540678.071133686], [14586228.077738127, 4540863.376303522], [14585861.661099326, 4541033.242270953], [14585495.244460465, 4541187.668278762], [14585128.827820107, 4541342.096575501], [14584750.197293801, 4541496.527160034], [14584395.994541308, 4541650.960032226], [14584347.138989441, 4541666.403445655], [14584286.06955062, 4541697.29033999], [14583919.652910553, 4541851.726188135], [14583565.450158978, 4542021.60826263], [14583186.819632547, 4542176.048916555], [14582844.83076899, 4542330.491858846], [14582466.20024182, 4542484.937089604], [14582087.569714688, 4542608.49492355], [14581708.939186862, 4542762.94427407], [14581330.308658957, 4542901.950648047], [14580963.892020464, 4543010.067999098], [14580585.261492236, 4543149.07766965], [14580206.630964926, 4543257.1975837825], [14579803.57266225, 4543365.318620336], [14579424.942134961, 4543457.994687216], [14579192.878263632, 4543519.779190642], [14579034.09771989, 4543550.671579929], [14578655.467192937, 4543658.795661787], [14578264.622776985, 4543751.474338827], [14577873.77836218, 4543828.707200758], [14577495.147835061, 4543905.940633789], [14577092.089531014, 4543983.174640503], [14576701.245115522, 4544029.515319117], [14576310.400701316, 4544106.750241843], [14575919.556285223, 4544137.6443707235], [14575699.706302246, 4544183.98573635], [14575504.284093758, 4544199.432905103], [14575113.439679246, 4544230.327308818], [14574722.595264157, 4544276.669086714], [14574514.959168296, 4544292.1163917575], [14574331.750849022, 4544307.563720322], [14574124.11475319, 4544307.563719708], [14573916.478657782, 4544338.458445055], [14573733.270338044, 4544353.905841484], [14573525.634242143, 4544353.905841748], [14573330.212034652, 4544369.353261512], [14573134.789826233, 4544369.353261464], [14572939.367619064, 4544369.353262303], [14572719.517635329, 4544369.353261675], [14572524.095428342, 4544369.353261389], [14572328.673220538, 4544384.800704624], [14572121.037124906, 4544384.800704819], [14571937.828804424, 4544369.353261591], [14571730.192709187, 4544369.35326217], [14571522.556614075, 4544369.353262826], [14571339.34829391, 4544369.353262385], [14571131.712198837, 4544353.905842261], [14570948.503878202, 4544353.905843028], [14570740.867782762, 4544338.45844611], [14570545.44557518, 4544338.458445838], [14570350.023367973, 4544307.563721146], [14570130.173384072, 4544292.116393631], [14569934.751176836, 4544276.669089307], [14569543.906761209, 4544230.327311454], [14569153.062345682, 4544199.432906603], [14568945.42625058, 4544183.985738961], [14568762.21793041, 4544137.6443743445], [14568359.15962792, 4544106.750244441], [14567968.315212548, 4544029.515323136], [14567577.470797084, 4543983.174644471], [14567186.6263823, 4543905.940638149], [14566783.568079067, 4543828.707204437], [14566392.72366387, 4543751.474343973], [14566014.093135444, 4543658.795666206], [14565623.24872121, 4543550.671584686], [14565244.618193153, 4543457.994693514], [14564853.773778267, 4543365.3186267475], [14564475.14325144, 4543257.19758965], [14564096.512723364, 4543149.07767554], [14563693.454420516, 4543010.068005312], [14563314.823893422, 4542901.950654995], [14562936.193365432, 4542762.944282519], [14562606.418390313, 4542716.609237178], [14562398.782294482, 4542685.71932119], [14562191.146199709, 4542670.274397315], [14561812.51567194, 4542608.494931122], [14561421.671257151, 4542562.160572617], [14561030.826841386, 4542500.381747122], [14560639.982426064, 4542423.158731613], [14560236.924122458, 4542345.936288569], [14559833.86581993, 4542268.714416368], [14559467.449180512, 4542176.048925813], [14559076.604765026, 4542083.384260142], [14558697.97423732, 4541990.720416876], [14558307.129823012, 4541867.169908754], [14557928.499294864, 4541774.507987912], [14557525.440991675, 4541650.960043525], [14557146.81046419, 4541542.856792612], [14556768.179936875, 4541403.868545181], [14556389.549410133, 4541264.882152807], [14556010.918883575, 4541141.3402487645], [14555644.502244113, 4541017.7998076], [14555290.299492834, 4540863.376315873], [14554911.668964645, 4540708.95511347], [14554557.466213938, 4540523.652687981], [14554178.835686168, 4540384.678031769], [14553824.632935008, 4540214.822632615], [14553458.216296038, 4540044.970003144], [14553116.227432424, 4539844.238643887], [14552749.810793048, 4539689.83253557], [14552407.821930347, 4539504.548224409], [14552249.041385714, 4539427.347399758], [14551882.624746233, 4539303.827270621], [14551503.99422004, 4539195.748356934], [14551381.855340248, 4539149.4291654825], [14551125.36369234, 4539072.230970899], [14550746.73316546, 4538948.715047952], [14550368.102638047, 4538825.200587222], [14549989.472110914, 4538686.24856897], [14549623.055470902, 4538531.8596091075], [14549244.42494422, 4538377.472935194], [14548902.436080279, 4538238.526881961], [14548523.805553462, 4538084.144551083], [14548157.38891405, 4537914.3266261555], [14547803.186162714, 4537729.073861541], [14547436.769523405, 4537559.261718393], [14547094.7806594, 4537404.8894423675], [14546728.364020523, 4537204.208898579], [14546361.947381083, 4537018.9687477425], [14546032.172405548, 4536818.295628578], [14545677.969654717, 4536633.062331327], [14545323.76690356, 4536432.396637806], [14544993.991928555, 4536216.299437661], [14544652.003064753, 4536000.206715432], [14544322.228089612, 4535799.553195227], [14544004.667001592, 4535568.034697714], [14543662.678138765, 4535351.9554023], [14543345.117051568, 4535120.44683895], [14543027.555963451, 4534858.076675749], [14542697.780988807, 4534626.579069244], [14542380.219901314, 4534395.086599067], [14542074.87270228, 4534132.734675581], [14541781.73939042, 4533870.389347416], [14541464.178302774, 4533623.482121268], [14541158.831103068, 4533345.718475012], [14540877.911680002, 4533098.82366223], [14540572.564480469, 4532821.073979021], [14540291.645057205, 4532543.331687368], [14540010.725633612, 4532265.5967861395], [14539729.806210512, 4531972.440185714], [14539448.886786574, 4531679.291817728], [14539192.395139629, 4531417.008149412], [14538899.261827474, 4531108.447566406], [14538630.55629233, 4530815.323457989], [14538374.064645067, 4530506.780656056], [14538129.786885653, 4530198.2469714265], [14537873.295237642, 4529874.296414274], [14537616.803589916, 4529565.781417542], [14537396.953606762, 4529257.275535442], [14537152.67584749, 4528933.354167178], [14536932.825863078, 4528624.866966413], [14536676.334216729, 4528270.117950614], [14536444.27034455, 4527946.227197342], [14536248.848136436, 4527606.923846032], [14536028.998153169, 4527267.631515431], [14535821.362057582, 4526928.3502052585], [14535601.512074055, 4526589.079913595], [14535418.30375431, 4526249.820638423], [14535210.667658564, 4525910.572378158], [14535015.245451855, 4525555.915520818], [14534832.037131598, 4525185.851631445], [14534673.256587537, 4524831.219369989], [14534490.048268745, 4524476.599140016], [14534331.267724525, 4524106.573469703], [14534160.273292877, 4523751.977827433], [14533989.278861778, 4523366.561424789], [14533842.71220565, 4523027.406744612], [14533696.145549532, 4522642.01705591], [14533561.79278178, 4522287.47108967], [14533403.01223859, 4521902.108676457], [14533268.659471177, 4521532.174121515], [14533134.306703577, 4521146.839544288], [14533036.59559936, 4520776.931708183], [14532914.45671886, 4520376.21298474], [14532804.531727992, 4520006.332405602], [14532682.392847996, 4519621.054045723], [14532596.895632427, 4519220.379606621], [14532499.184527747, 4518835.130195337], [14532413.687312467, 4518449.894972147], [14532303.762320925, 4518049.265387172], [14532230.478993248, 4517664.059100537], [14532157.19566512, 4517278.866996087], [14532108.340113258, 4516878.282249036], [14532035.056785649, 4516477.712836037], [14531973.987345573, 4516092.564399366], [14531973.987345243, 4516030.941964629], [14531937.345681304, 4515784.455855288], [14531888.490129516, 4515383.928306678], [14531827.420689756, 4514983.416085525], [14531778.56513731, 4514582.919189623], [14531741.923473246, 4513982.2025730265], [14531705.281810218, 4513797.373626424]]], [[[14339432.408530401, 4075075.6362608722], [14339458.685080042, 4075084.437250298], [14339751.818391822, 4076223.538724107], [14338652.568473613, 4084644.61678336], [14338506.00181847, 4086228.895738871], [14336759.41583827, 4097428.8401910467], [14341315.196051706, 4104501.207588504], [14341938.104338527, 4105925.114876204], [14342707.57928172, 4107200.8516809675], [14343501.482000086, 4108610.2642060244], [14352735.181309098, 4124392.929620267], [14356472.63102952, 4130694.3846049826], [14357889.442034634, 4133221.908237402], [14359770.380782299, 4136374.679519459], [14361968.880617706, 4140004.4420440258], [14364619.294308105, 4144900.5395199214], [14371984.268757481, 4157187.447747604], [14376991.962826528, 4165401.170055259], [14381535.529153243, 4172024.2679253733], [14386396.65656731, 4178188.490732939], [14390109.678512042, 4182503.992319043], [14410071.323094087, 4181759.7407464916], [14411740.474114683, 4181697.507984717], [14411887.04077094, 4182668.28335924], [14412290.099073624, 4183937.8871818185], [14413145.071231768, 4185521.361705531], [14414659.593340822, 4189466.094009724], [14416235.184889486, 4190347.874119261], [14418116.123638438, 4191708.045624967], [14420009.276274431, 4193442.1331010573], [14421670.365039108, 4195430.688718817], [14423087.176044246, 4197314.91349072], [14424369.634281721, 4199438.789978044], [14425468.884199308, 4201682.765014417], [14426323.856356785, 4203642.876257398], [14426897.90909205, 4205708.102752736], [14427508.603490569, 4207998.262274081], [14427826.164578045, 4210229.005413773], [14428705.564512718, 4211007.626033115], [14430427.722717127, 4212669.865587721], [14432333.089241156, 4214736.783326548], [14433591.119701806, 4217118.712887043], [14434763.65294765, 4219576.085746894], [14435569.76955409, 4222498.676121303], [14436070.538960757, 4223802.851526747], [14436840.01390352, 4227416.375070025], [14437157.574990707, 4228856.1177854985], [14437963.691597, 4230581.057929868], [14439343.860938719, 4234526.939401433], [14440296.544200161, 4238774.472618673], [14440443.110856375, 4242242.7597973915], [14440113.33588104, 4245862.359133215], [14439478.213706143, 4248867.084183436], [14439331.647050746, 4249813.743335828], [14439649.208137836, 4250910.768227175], [14439940.719361056, 4252568.169217453], [14544627.066163512, 4252568.169285575], [14534796.669683114, 4240464.677659911], [14514759.161462417, 4205175.173351611], [14501957.419936124, 4179737.9296527705], [14485448.739516629, 4179288.840961339], [14439555.743282635, 4173635.0297790095], [14435722.322889304, 4173166.719291172], [14421250.789188549, 4166599.3964159037], [14402137.232618976, 4158446.631542469], [14394344.868232908, 4150978.506075684], [14389524.734276652, 4146676.4225404835], [14384326.1140242, 4142606.5776094557], [14365958.39800865, 4119341.9682434443], [14361694.86160705, 4101989.3419181844], [14361694.861568779, 4100867.6858379836], [14360581.666636346, 4094329.1704151263], [14359101.117467742, 4090737.3044364187], [14347779.925183792, 4070274.6906909533], [14347742.667809354, 4070231.176358625], [14339432.408530401, 4075075.6362608722]]]]}}]} \ No newline at end of file diff --git a/prediction/data/zones/특정어업수역Ⅱ.json b/prediction/data/zones/특정어업수역Ⅱ.json new file mode 100644 index 0000000..5f3cea7 --- /dev/null +++ b/prediction/data/zones/특정어업수역Ⅱ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed2", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2161", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[14026312.49388151, 3787395.72363925], [14026272.928939708, 3912341.809856742], [14026343.45295978, 3912257.596201178], [14026661.014047539, 3911892.988733094], [14026978.575133963, 3911557.559921697], [14027051.858461797, 3911470.0583396605], [14027137.355677672, 3911178.3911333913], [14027198.425118214, 3910930.4797375146], [14027271.708445255, 3910711.7387614], [14027344.991773328, 3910478.419569922], [14027406.061213229, 3910245.105041409], [14027503.77231727, 3910026.376906358], [14027577.055645473, 3909793.071410766], [14027650.338973064, 3909574.351742198], [14027723.622300655, 3909341.0552779627], [14027821.333404718, 3909122.34407479], [14027906.830620117, 3908903.6369685275], [14027992.327835856, 3908684.9339581975], [14028090.038940514, 3908451.65526086], [14028175.536155386, 3908218.3812227794], [14028273.247259233, 3908014.2702603256], [14028370.958363216, 3907781.0049572694], [14028468.669467552, 3907576.9016377437], [14028566.380571473, 3907343.6450677463], [14028664.091674816, 3907139.5493900776], [14028761.8027788, 3906920.8794053984], [14028883.941658128, 3906716.7911130013], [14028993.866650797, 3906483.5517138042], [14029091.577753998, 3906279.4710601526], [14029201.502746, 3906060.81717149], [14029323.641625034, 3905856.7438994534], [14029433.566617623, 3905638.097921009], [14029543.491609104, 3905419.4560323786], [14029665.630488753, 3905215.393960793], [14029714.486040367, 3905011.3354516793], [14029763.341592517, 3904807.2805052707], [14029824.411032889, 3904574.0792145403], [14029861.052696653, 3904340.8825751734], [14029922.122136267, 3904107.690588767], [14029983.19157568, 3903874.5032539973], [14030044.261016268, 3903641.3205705], [14030105.33045631, 3903393.5690651014], [14030178.613783477, 3903174.96915455], [14030239.683223227, 3902941.800421957], [14030312.96655101, 3902723.2089581615], [14030386.24987901, 3902475.4769059164], [14030447.319319358, 3902256.8941590027], [14030520.602646638, 3902023.7437318605], [14030606.099863403, 3901790.597953613], [14030691.597078484, 3901572.028007055], [14030764.880406654, 3901338.8912325953], [14030862.591510149, 3901120.329726406], [14030935.874838341, 3900887.201954522], [14031021.372054312, 3900668.648888896], [14031119.083157621, 3900450.0999057423], [14031204.580374023, 3900216.9854906956], [14031290.077589095, 3899998.444944336], [14031400.002581708, 3899779.908480802], [14031485.499797242, 3899561.37609934], [14031595.424789447, 3899342.847798803], [14031705.349781059, 3899138.891733757], [14031790.846996775, 3898905.803441251], [14031912.985876147, 3898701.8549923794], [14032010.696979966, 3898468.7754048174], [14032120.621971566, 3898264.8345725327], [14032230.54696337, 3898046.330481661], [14032352.685843932, 3897827.830470188], [14032450.396947037, 3897623.9008064135], [14032572.535826314, 3897405.4086795547], [14032694.674706502, 3897216.052135809], [14032816.81358599, 3896997.5676209624], [14032938.952466376, 3896793.652420099], [14033061.091346277, 3896589.740771255], [14033195.444113696, 3896385.8326731725], [14033329.796881828, 3896181.9281274145], [14033451.935761089, 3895978.0271314387], [14033586.288528644, 3895774.129686274], [14033720.641296018, 3895584.7995237107], [14033989.34683201, 3895191.5851198323], [14034258.05236752, 3894798.383918607], [14034563.399566252, 3894405.195917184], [14034844.31899014, 3894026.5829095095], [14035149.666189065, 3893647.982137594], [14035442.799501298, 3893283.954474], [14035760.360587686, 3892919.938120377], [14036077.92167527, 3892555.9330759617], [14036407.696651513, 3892191.9393388475], [14036737.471625743, 3891857.075086648], [14036870.141786523, 3891726.198837797], [14037091.674377358, 3891507.661720512], [14037421.449352924, 3891172.81701958], [14037787.86599152, 3890867.0976043935], [14038142.06874289, 3890532.271202124], [14038178.710406706, 3890503.1563148433], [14038215.352071756, 3890459.48412038], [14038569.554822957, 3890139.2263254467], [14038923.75757426, 3889818.9772791206], [14039302.388101518, 3889513.29316919], [14039681.018628426, 3889207.617028156], [14040059.649154926, 3888931.0597660383], [14040426.065794326, 3888639.953906682], [14040816.91020996, 3888348.855272826], [14041232.18240151, 3888072.318264321], [14041623.02681568, 3887810.3418493513], [14041818.449023297, 3887679.3558354746], [14042026.085119475, 3887548.3712851256], [14042221.507326983, 3887431.941802017], [14042429.14342227, 3887300.9600144974], [14042624.565629626, 3887184.532986891], [14042844.415613849, 3887053.553961726], [14043052.051709011, 3886951.682398294], [14043259.687804861, 3886820.705972922], [14043467.323900312, 3886704.283712141], [14043687.173883341, 3886602.415180504], [14043894.809980085, 3886471.4426559876], [14044102.446075153, 3886369.576147044], [14044310.082171045, 3886267.7105223597], [14044542.146041911, 3886151.2937484276], [14044749.78213759, 3886049.4300192064], [14044957.418233024, 3885947.567174553], [14045189.4821048, 3885860.256868118], [14045397.118200412, 3885743.844137209], [14045629.182072148, 3885656.5353473793], [14045849.032055777, 3885569.2272066567], [14046056.668151285, 3885481.9197152676], [14046288.73202292, 3885380.06179798], [14046508.58200612, 3885292.755714113], [14046740.6458772, 3885205.4502798985], [14046960.495860584, 3885132.696247827], [14047192.559732666, 3885045.392004632], [14047412.409715734, 3884958.0884115743], [14047644.473587925, 3884885.335912664], [14047864.323571343, 3884812.583865854], [14048096.387442762, 3884739.8322684863], [14048316.237426357, 3884667.081122923], [14048560.515186315, 3884579.7803441263], [14048780.365169752, 3884521.5801856825], [14049012.429040425, 3884448.8303926187], [14049232.279024879, 3884390.630883527], [14049476.556783637, 3884332.431662887], [14049708.620655935, 3884288.7824363117], [14049940.684527121, 3884216.034086747], [14050172.74839862, 3884157.835731066], [14050417.026158523, 3884099.6376657546], [14050636.876141772, 3884055.989305159], [14050881.153900763, 3884012.3411064013], [14051101.003885025, 3883968.693071144], [14051345.28164453, 3883925.045197191], [14051589.55940309, 3883881.397485363], [14051809.409386702, 3883837.7499363376], [14052053.687146327, 3883794.102549823], [14052297.96490621, 3883765.00438295], [14052530.028777948, 3883721.357266986], [14052774.306537522, 3883692.2592790583], [14052994.1565203, 3883677.710312659], [14053238.434280407, 3883648.6124334624], [14053482.712039571, 3883619.514626246], [14053726.989798827, 3883604.9657499343], [14053946.839782678, 3883575.8680508113], [14054203.331430309, 3883561.3192288275], [14054435.395301264, 3883546.770424295], [14054667.459173407, 3883532.221638027], [14054911.736932652, 3883517.6728706607], [14055156.014692299, 3883503.1241203477], [14055388.078563493, 3883503.1241198485], [14055632.356323073, 3883503.1241198387], [14055876.634082645, 3883503.1241195546], [14056120.911842786, 3883488.575388423], [14056352.97571373, 3883488.575387674], [14056585.039585229, 3883503.1241202564], [14056829.31734454, 3883503.1241205744], [14057061.381216811, 3883503.1241201926], [14057305.658975704, 3883517.672870415], [14057549.936734984, 3883532.2216384728], [14057782.000606766, 3883546.7704246663], [14058026.27836623, 3883561.3192288917], [14058270.556125652, 3883575.868050909], [14058514.833885796, 3883604.9657495967], [14058734.683868349, 3883619.5146270352], [14058978.96162855, 3883648.612433189], [14059211.02550005, 3883663.161363651], [14059455.303259443, 3883692.2592792334], [14059699.581018375, 3883721.35726594], [14059919.431002565, 3883765.0043828213], [14060163.708761357, 3883794.1025502756], [14060407.98652079, 3883837.7499373327], [14060652.26428107, 3883881.397485507], [14060872.114264324, 3883925.0451974412], [14061116.392023854, 3883968.6930701793], [14061348.455895012, 3884012.3411065093], [14061580.519767078, 3884055.9893042506], [14061824.797526775, 3884099.6376657034], [14062044.647510495, 3884157.8357315855], [14062288.925269853, 3884216.0340869497], [14062508.775252802, 3884288.782436949], [14062753.053012297, 3884332.4316628594], [14062985.116884248, 3884390.6308830315], [14063204.966867786, 3884448.8303930266], [14063449.244626828, 3884521.5801856043], [14063669.09461016, 3884579.780344494], [14063901.15848242, 3884667.0811240533], [14064121.008465912, 3884739.832268733], [14064353.072337683, 3884812.5838650106], [14064585.136208225, 3884885.3359133895], [14064817.200079866, 3884958.088411405], [14065037.050063629, 3885045.3920052126], [14065269.113934992, 3885132.6962482887], [14065488.963918757, 3885205.450280084], [14065708.813902, 3885292.755713535], [14065928.663886081, 3885380.0617970554], [14066160.72775689, 3885481.9197158483], [14066380.577740904, 3885569.2272062856], [14066588.21383698, 3885656.5353471697], [14066820.277708739, 3885743.8441374716], [14067040.127691144, 3885860.2568676393], [14067259.977674901, 3885947.5671746274], [14067467.613770252, 3886049.4300197763], [14067687.463753887, 3886151.2937480435], [14067907.313738173, 3886267.7105219206], [14068114.949834043, 3886369.5761465007], [14068322.585929519, 3886471.442655181], [14068554.649800802, 3886602.4151809313], [14068750.072007738, 3886704.283711534], [14068957.708103167, 3886820.705972922], [14069165.344199639, 3886951.6823977036], [14069372.980294656, 3887053.5539613245], [14069592.83027846, 3887184.53298712], [14069788.25248586, 3887300.9600143926], [14069995.888581414, 3887431.9418018376], [14070203.524677217, 3887548.3712856653], [14070398.946884345, 3887679.3558351737], [14070606.58297968, 3887810.341848666], [14070997.427395098, 3888072.318263754], [14071400.485698925, 3888348.855272708], [14071791.330113634, 3888639.95390655], [14072157.746753618, 3888931.059766593], [14072548.59116818, 3889207.617027234], [14072915.007807814, 3889513.2931684074], [14073293.638334833, 3889818.9772791504], [14073647.841086097, 3890139.226324991], [14074014.257724732, 3890459.4841204123], [14074356.24658839, 3890779.750664216], [14074710.449339252, 3891114.584135423], [14075052.438203165, 3891463.985782468], [14075369.999290047, 3891813.397846801], [14075699.774265824, 3892162.8203282068], [14076029.549241148, 3892526.813160914], [14076334.896440182, 3892890.817300508], [14076652.457527356, 3893254.8327489593], [14076933.376951266, 3893633.4208147195], [14077238.72415068, 3894012.0211142646], [14077519.643573463, 3894405.1959178024], [14077812.776884863, 3894798.383917827], [14078081.482420994, 3895191.5851198635], [14078337.974068029, 3895584.7995231976], [14078472.326835675, 3895788.693671682], [14078594.465715563, 3895992.591370282], [14078728.818483, 3896196.4926201487], [14078850.957363801, 3896400.3974202448], [14078960.88235518, 3896604.30577156], [14079083.021235045, 3896808.2176739727], [14079205.16011431, 3897012.1331286556], [14079327.298994398, 3897230.6179148587], [14079534.935089272, 3897623.9008055353], [14079766.998961411, 3897594.7682869313], [14079999.062833289, 3897551.069644458], [14080243.34059258, 3897521.9373066192], [14080475.404464027, 3897492.8050412447], [14080707.468334954, 3897449.106778766], [14080951.746094488, 3897419.9746949435], [14081196.023853954, 3897405.408680112], [14081415.87383798, 3897376.2767037847], [14081672.365485134, 3897361.710744355], [14081904.429357013, 3897332.5788773843], [14082136.49322842, 3897318.012970849], [14082380.770987837, 3897303.44708331], [14082625.048747523, 3897303.447082881], [14082844.89873114, 3897288.8812134634], [14083101.39037848, 3897288.8812134196], [14083333.454250371, 3897274.3153610313], [14083577.73200986, 3897274.315360886], [14083809.795881303, 3897274.3153613866], [14084054.07364039, 3897274.315361214], [14084298.35139951, 3897274.3153610793], [14084530.415272055, 3897288.881213491], [14084774.693031047, 3897288.881213693], [14085018.970789962, 3897303.447082888], [14085263.248550324, 3897303.4470835133], [14085483.09853329, 3897318.012971403], [14085727.376292782, 3897332.5788774174], [14085971.654052334, 3897361.71074422], [14086203.7179239, 3897376.276704187], [14086447.995682908, 3897405.4086796427], [14086692.27344248, 3897419.9746941905], [14086912.123426246, 3897449.1067788443], [14087156.401186522, 3897492.8050409877], [14087400.678945886, 3897521.9373064945], [14087620.528929327, 3897551.0696441643], [14087864.806688221, 3897594.7682873094], [14088109.084447999, 3897623.900805951], [14088341.148319451, 3897667.5997203756], [14088573.212191245, 3897696.7324211453], [14088805.276063038, 3897740.4316074788], [14089037.339933824, 3897784.1309570693], [14089281.617693743, 3897827.830470751], [14089501.4676769, 3897900.66335366], [14089745.745436855, 3897944.3633015817], [14089965.595419774, 3898002.630152866], [14090209.873179223, 3898060.897294051], [14090441.937050942, 3898119.164725707], [14090674.000922583, 3898162.8654892095], [14090906.064793834, 3898235.7004582426], [14091125.914777994, 3898308.535880703], [14091370.19253783, 3898381.3717555343], [14091590.04252131, 3898439.6407829127], [14091822.106392259, 3898512.4774739025], [14092041.956375973, 3898585.314618505], [14092274.020247314, 3898672.7197900466], [14092493.870230613, 3898745.557932266], [14092591.581335085, 3898774.693316213], [14092616.00911053, 3898760.125616015], [14092860.286870733, 3898701.8549935655], [14093080.136854356, 3898629.0171225015], [14093312.200724699, 3898570.7471534302], [14093556.478484493, 3898527.044866997], [14093776.328468569, 3898468.775404192], [14094020.606227417, 3898425.073498839], [14094264.883987851, 3898381.3717556526], [14094484.733970387, 3898337.670176473], [14094729.011730317, 3898293.9687599814], [14094961.075602157, 3898235.7004585327], [14095193.13947348, 3898206.566416765], [14095437.417232322, 3898162.865489851], [14095669.481104298, 3898133.7316290126], [14095901.544975886, 3898090.0309733814], [14096145.8227352, 3898060.897294544], [14096377.886607243, 3898031.7636878835], [14096622.164366543, 3898017.196910956], [14096866.442125635, 3897988.063412855], [14097086.292109383, 3897973.4966916144], [14097330.569869047, 3897944.3633026695], [14097574.847628593, 3897929.7966353055], [14097806.91150033, 3897915.2299849386], [14098051.189258894, 3897900.6633545416], [14098295.467019573, 3897886.0967414593], [14098515.317002017, 3897871.5301457075], [14098759.594761733, 3897871.5301461737], [14099003.872522173, 3897871.5301457415], [14099248.15028098, 3897842.3970105853], [14099480.214152526, 3897842.3970112065], [14099724.491911555, 3897842.3970108926], [14099968.769672029, 3897871.5301465685], [14100200.833543025, 3897871.530146231], [14100445.111302666, 3897871.530146376], [14100689.389062308, 3897886.0967411445], [14100909.23904563, 3897900.6633543223], [14101153.516805617, 3897915.229985139], [14101397.794564402, 3897929.796634782], [14101629.858436095, 3897944.363301649], [14101874.136195809, 3897973.4966918174], [14102118.413954498, 3897988.0634127976], [14102338.263938468, 3898017.1969116074], [14102582.541697871, 3898031.763687157], [14102826.819457443, 3898060.897294939], [14103046.669441475, 3898090.0309742764], [14103290.947201025, 3898133.731628701], [14103535.224959875, 3898162.865489382], [14103767.288831646, 3898206.566415829], [14104011.566590969, 3898235.7004580023], [14104243.63046214, 3898293.9687599214], [14104475.694334047, 3898337.6701762597], [14104707.758205285, 3898381.3717553043], [14104952.035964744, 3898425.0734980954], [14105171.885948928, 3898468.7754048845], [14105416.163708236, 3898527.044866273], [14105636.013691971, 3898570.7471529637], [14105880.291451601, 3898629.017122684], [14106100.141434586, 3898701.8549932986], [14106344.419193909, 3898760.1256155283], [14106576.483066218, 3898818.3965284377], [14106796.33304983, 3898891.2355768005], [14107040.61080865, 3898949.5071421904], [14107260.460792817, 3899022.347006401], [14107492.524664072, 3899109.7554428596], [14107712.374646941, 3899168.0280965483], [14107944.43851916, 3899240.8693216643], [14108164.288502041, 3899313.7110006236], [14108396.35237358, 3899401.1216131775], [14108616.202357315, 3899488.5328786755], [14108848.26622925, 3899575.9447972635], [14109068.116212262, 3899648.788562357], [14109300.180084735, 3899736.201678742], [14109507.81617953, 3899823.6154470327], [14109739.8800512, 3899925.5990036307], [14109959.730035394, 3900013.0141874147], [14110167.366130177, 3900100.430024312], [14110399.430001773, 3900202.4159939], [14110607.066097446, 3900318.9724758286], [14110826.91608077, 3900406.3905996806], [14111046.766064817, 3900508.3792349175], [14111254.402160756, 3900610.368760547], [14111462.038255833, 3900726.9293071674], [14111694.102127243, 3900828.9207385355], [14111901.738223149, 3900945.483462624], [14112109.374317858, 3901062.047348597], [14112317.010413814, 3901178.6123951804], [14112536.86039789, 3901295.1786045753], [14112732.282604866, 3901411.745975727], [14112939.91870098, 3901542.8856554995], [14113147.554796439, 3901659.455495108], [14113342.977004122, 3901776.026497272], [14113562.826987848, 3901907.170262538], [14113758.249194663, 3902023.7437324016], [14113965.885290999, 3902154.8902756744], [14114161.307498729, 3902300.6103812656], [14114552.151913268, 3902562.911149271], [14114942.996328058, 3902839.790564282], [14115333.840744248, 3903131.2496635215], [14115712.471271036, 3903408.142537925], [14116091.101798624, 3903714.189658899], [14116457.518437536, 3904020.244793169], [14116823.935076432, 3904326.30794037], [14117190.351715742, 3904646.9541180977], [14117544.554466687, 3904967.6090920256], [14117874.329442928, 3905288.272862804], [14118228.532193437, 3905623.52166774], [14118558.307168506, 3905973.3567619547], [14118900.296031933, 3906323.2023288426], [14119217.857119897, 3906687.6359336833], [14119535.41820684, 3907022.924889553], [14119852.979293982, 3907387.380320937], [14120146.112605767, 3907766.426031002], [14120329.320924878, 3907999.69104288], [14120659.095900508, 3908145.484040798], [14120891.159772767, 3908247.5402225223], [14121098.79586814, 3908364.1769507537], [14121306.43196353, 3908466.2350431], [14121538.49583534, 3908568.294027048], [14121746.13193052, 3908699.514031146], [14121953.768026086, 3908801.575054541], [14122161.404121136, 3908918.217315232], [14122381.254104782, 3909034.860741095], [14122588.890200352, 3909166.0859882417], [14122784.312407838, 3909282.7318893564], [14122991.948504105, 3909399.378956402], [14123187.370711256, 3909530.6082997248], [14123407.22069531, 3909661.8391170804], [14123602.64290326, 3909778.4899717756], [14123675.926230324, 3909822.2343429914], [14123871.348437805, 3909851.3973478205], [14124103.412310153, 3909909.7235756326], [14124335.476181583, 3909953.4684379986], [14124579.753940387, 3909997.2134634694], [14124799.603924207, 3910040.9586544754], [14125043.88168351, 3910099.2858296824], [14125263.731667727, 3910143.0314019676], [14125508.009426983, 3910201.3590867375], [14125752.28718599, 3910259.6870635124], [14125972.137169205, 3910332.59744405], [14126204.201041877, 3910390.92607536], [14126424.051024832, 3910449.254999375], [14126668.328784036, 3910507.584213994], [14126888.17876787, 3910565.913720107], [14127120.242639447, 3910638.8260132936], [14127340.092622804, 3910726.3213651967], [14127584.370382065, 3910799.234659938], [14127816.434254477, 3910872.148409907], [14128036.284238072, 3910945.0626162733], [14128268.348108647, 3911017.9772767853], [14128488.198092327, 3911090.892392731], [14128720.261964472, 3911192.974320413], [14128940.111948168, 3911280.4738266575], [14129147.748043166, 3911367.973988523], [14129379.811914971, 3911440.891291154], [14129599.661898108, 3911557.559922042], [14129819.511881288, 3911645.0621605986], [14130039.361864883, 3911732.5650563217], [14130271.425736733, 3911820.068606416], [14130479.061832469, 3911936.741027793], [14130686.697928369, 3912038.8303543963], [14130918.761799408, 3912140.920572288], [14131126.397895552, 3912243.0116843027], [14131334.033990381, 3912359.6883341745], [14131566.09786253, 3912461.7813591575], [14131773.73395724, 3912578.460196206], [14131981.370053304, 3912680.5551354536], [14132189.006148996, 3912811.821370374], [14132408.856131978, 3912913.918351353], [14132604.278340138, 3913030.6017091586], [14132811.914436067, 3913161.8718802584], [14133019.55053132, 3913278.557718527], [14133214.972738754, 3913409.8306803126], [14133422.608834058, 3913526.518996874], [14133618.03104185, 3913657.7947483873], [14133837.881025733, 3913789.0719763637], [14134033.303232925, 3913920.3506824095], [14134424.147647737, 3914197.4994676104], [14134814.99206299, 3914474.654837102], [14135205.836479066, 3914737.2291565286], [14135584.467006274, 3915043.5733327474], [14135950.883645028, 3915335.3371710163], [14136329.514172366, 3915641.697056373], [14136695.93081197, 3915948.0649892436], [14137050.133562554, 3916269.0305042304], [14137416.550201891, 3916590.0048536095], [14137758.539065152, 3916925.578393816], [14138112.741816988, 3917261.161591096], [14138124.955704955, 3917290.3431960098], [14138222.66680882, 3917363.297525027], [14138576.869559862, 3917669.710695894], [14138931.072311323, 3918005.315314095], [14139297.488950564, 3918326.3374690292], [14139627.263925616, 3918676.5535388705], [14139969.25278898, 3919012.187147011], [14140286.813875556, 3919362.423825106], [14140616.588851899, 3919712.671030513], [14140934.149938418, 3920077.5230633905], [14141251.711026246, 3920442.3865206414], [14141557.058225727, 3920821.8566365796], [14141850.191537393, 3921186.743403776], [14142143.324847814, 3921580.833950086], [14142412.030383276, 3921960.3411501986], [14142705.163695073, 3922354.4578704755], [14142973.869230365, 3922748.5879268395], [14143108.221998872, 3922952.9569106484], [14143230.360877866, 3923157.3294808976], [14143364.71364529, 3923347.1072222115], [14143486.852526005, 3923566.085382383], [14143621.205292745, 3923770.468714929], [14143743.344173217, 3923974.8556338386], [14143853.269164307, 3924179.246141352], [14143975.40804453, 3924398.239951181], [14144085.333036179, 3924602.6378918802], [14144207.471916584, 3924807.039420669], [14144329.61079611, 3925011.4445379367], [14144427.321899917, 3925245.054782049], [14144537.246891692, 3925449.467591055], [14144659.38577103, 3925668.485299123], [14144757.096875027, 3925872.905545292], [14144867.021866404, 3926091.931221066], [14144976.946858248, 3926310.9610188995], [14145062.444074264, 3926529.9949393696], [14145172.369066445, 3926749.032982101], [14145257.866282122, 3926968.07514812], [14145343.36349802, 3927187.1214373973], [14145453.288489206, 3927420.7753580655], [14145538.785705116, 3927639.830169832], [14145636.496809188, 3927858.8891063603], [14145709.78013733, 3928077.9521672083], [14145831.91901659, 3928194.7874860945], [14146173.907880068, 3928516.090663645], [14146528.11063154, 3928837.402716595], [14146857.885606105, 3929187.935079745], [14147199.87446994, 3929538.4780067294], [14147529.649444804, 3929874.424892384], [14147847.210531974, 3930224.988511853], [14148164.771620288, 3930590.1701873746], [14148470.118818687, 3930955.363331506], [14148763.252130508, 3931335.176369344], [14149068.599329932, 3931715.0018159356], [14149349.518753031, 3932094.839672609], [14149642.652064433, 3932489.2998140086], [14149911.357600631, 3932869.1629744656], [14150167.84924795, 3933263.649396584], [14150302.202015493, 3933482.814306461], [14150436.554783072, 3933672.760570551], [14150558.69366279, 3933891.9331968473], [14150693.04643046, 3934081.88614829], [14150815.185310263, 3934301.0664916416], [14150937.324189857, 3934505.638541194], [14151059.463069875, 3934710.2141920165], [14151169.388061073, 3934914.793446248], [14151279.313053045, 3935133.9895019485], [14151401.45193336, 3935338.576218246], [14151499.163036935, 3935557.7802698156], [14151621.301916642, 3935762.3744509714], [14151731.226908179, 3935981.586499216], [14151841.151899958, 3936186.188145459], [14151938.863003807, 3936405.408193718], [14152195.354651982, 3936726.938415809], [14152488.487962838, 3937121.555854616], [14152757.193498401, 3937516.186704223], [14153013.685146198, 3937910.830965573], [14153148.037913712, 3938130.0835739793], [14153282.39068159, 3938320.1058491296], [14153404.52956081, 3938539.3661864866], [14153526.668440903, 3938729.3951621894], [14153661.021208057, 3938934.045228578], [14153783.16008862, 3939153.3171624425], [14153893.085079862, 3939343.356187306], [14154003.010072157, 3939562.635853141], [14154125.148951644, 3939767.300611019], [14154247.287830727, 3939986.588286231], [14154344.998935373, 3940191.260520088], [14154454.923926366, 3940410.5562055036], [14154577.062806187, 3940615.235917028], [14154686.987798307, 3940834.5396143007], [14154784.698902179, 3941053.847455504], [14154894.623893438, 3941273.159443157], [14154980.121109651, 3941477.8543701675], [14155090.046101721, 3941711.795852182], [14155175.543317659, 3941916.4985195934], [14155285.468309484, 3942150.4488461153], [14155370.965524651, 3942369.7815635717], [14155468.676628448, 3942589.1184271937], [14155554.173845042, 3942808.4594375123], [14155639.671059819, 3943042.427754295], [14155725.168275682, 3943261.7773370524], [14155810.665492112, 3943481.131068717], [14155883.948820263, 3943700.4889500365], [14155957.232147578, 3943934.475261892], [14156030.51547582, 3944153.84171713], [14156116.012691723, 3944387.8371760393], [14156189.296019575, 3944621.8373581613], [14156262.579347137, 3944855.842261796], [14156335.86267503, 3945075.226148344], [14156396.932115378, 3945309.240202267], [14156458.001554107, 3945543.2589806733], [14156531.284882791, 3945777.2824817533], [14156580.140434783, 3945996.683805485], [14156641.209874306, 3946245.343659381], [14156702.279314281, 3946479.3813347146], [14156763.348753486, 3946713.4237364368], [14156836.632082347, 3947093.7527188975], [14156922.129297458, 3947210.779532316], [14157044.268176915, 3947415.5792986215], [14157166.407056939, 3947620.3826843738], [14157300.759824937, 3947825.1896898304], [14157422.89870435, 3948030.000315225], [14157545.037583863, 3948234.8145606047], [14157667.17646415, 3948454.2624128303], [14157764.887568416, 3948659.084157569], [14157887.026448287, 3948878.5400449373], [14157996.951438919, 3949083.369290671], [14158119.090319661, 3949288.202157963], [14158216.801423091, 3949507.6699629608], [14158326.726414407, 3949727.1419266723], [14158424.437519114, 3949931.986177989], [14158534.362510465, 3950166.098329143], [14158644.287502103, 3950370.950342997], [14158729.78471748, 3950590.4386638855], [14158815.281933218, 3950809.931145622], [14158925.206925157, 3951029.427785851], [14159010.704141628, 3951248.9285875857], [14159108.415245011, 3951483.0673614168], [14159193.912461305, 3951702.576762513], [14159291.6235645, 3951936.724709892], [14159364.906892387, 3952141.6080482737], [14159450.404108545, 3952375.764874187], [14159523.687435796, 3952595.291199293], [14159621.398540307, 3952829.4572015903], [14159694.681868514, 3953048.992130602], [14159767.965195222, 3953283.1673107734], [14159841.248523328, 3953502.71084457], [14159914.531851163, 3953751.5318834265], [14159975.601291291, 3953971.084300843], [14160036.670731131, 3954205.278137325], [14160097.740171447, 3954424.8391631977], [14160171.023498941, 3954673.6800276935], [14160232.092939438, 3954893.249940999], [14160293.162378157, 3955127.4624392823], [14160342.017930873, 3955361.679679122], [14160403.087371092, 3955610.540691105], [14160451.942922773, 3955830.128382198], [14160500.798474764, 3956064.3598468173], [14160549.65402654, 3956298.596053535], [14160598.509577785, 3956547.477220151], [14160622.937354516, 3956693.880406732], [14160636.617809776, 3956751.2753614364], [14160696.220681982, 3957001.3331325892], [14160757.290121438, 3957220.947224554], [14160818.359561661, 3957469.8482368095], [14160867.215113139, 3957704.112907182], [14160916.070665386, 3957938.3823223934], [14160952.712329246, 3958172.656482196], [14161001.567881363, 3958406.9353898573], [14161050.423433455, 3958641.2190423324], [14161099.278985005, 3958890.1506262408], [14161135.920648871, 3959109.800590544], [14161172.562312365, 3959358.7422623085], [14161209.203976428, 3959593.0452026036], [14161245.845640494, 3959827.352890803], [14161270.273416875, 3960061.6653290996], [14161306.915080808, 3960310.627497995], [14161319.128968159, 3960544.9497317653], [14161343.55674493, 3960779.276716447], [14161380.198409086, 3961013.6084507345], [14161392.412296837, 3961262.5911258464], [14161404.626184527, 3961511.5791632985], [14161429.053960387, 3961745.92574739], [14161441.267848562, 3961994.924199761], [14161453.481735952, 3962214.63317148], [14161453.481736058, 3962463.6417239243], [14161465.695623817, 3962698.007616471], [14161465.695623918, 3962947.0265865847], [14161490.123400616, 3963181.402284813], [14161490.123400327, 3963430.4316741815], [14161490.12340009, 3963664.817180731], [14161490.123400327, 3963913.856991753], [14161465.695623929, 3964148.2523065866], [14161453.481736246, 3964382.6523781596], [14161453.481736366, 3964631.7076659855], [14161441.267847814, 3964866.11754977], [14161429.053960953, 3965100.5321910167], [14161429.053960415, 3965349.602961888], [14161392.412296638, 3965584.027417495], [14161380.198408043, 3965833.1086162445], [14161343.556744935, 3966067.542888706], [14161343.556744233, 3966155.4569668346], [14161331.342857195, 3966316.6345174764], [14160989.35399289, 3968675.9452985157], [14160366.445706693, 3971035.738514718], [14159609.18465247, 3973249.3995169974], [14158717.57082961, 3975478.1498437845], [14157471.754256403, 3977560.6613335563], [14155041.190549271, 3980831.8443643325], [14152329.70741956, 3983619.6844641836], [14151987.71855616, 3983825.1309771356], [14148885.391009744, 3986188.029616028], [14146809.030054526, 3987362.3212974994], [14144659.385770556, 3988331.2022260944], [14142448.672047747, 3989109.3020907817], [14140018.108340502, 3989667.217435894], [14137673.041849403, 3990078.3302842528], [14136695.930811903, 3990151.744841114], [14135095.911487218, 3991458.6024279883], [14131847.017286118, 3993896.5093908194], [14128537.053643916, 3995644.4841260226], [14126998.103759484, 3996261.479934003], [14128940.111947352, 3996599.372644142], [14130882.120135639, 3997025.425458683], [14132824.128323132, 3997436.8018056713], [14133581.38937842, 3997642.4955095113], [14135169.19481456, 3997995.1218662434], [14137111.20300309, 3998215.51884181], [14138979.92786301, 3998421.2265061126], [14140848.652722916, 3998685.713208066], [14142790.66091092, 3998906.123450371], [14144732.669099433, 3999170.621330323], [14146601.39395857, 3999317.5672330167], [14148543.402146455, 3999596.769632924], [14150412.12700758, 3999875.9788301843], [14152329.70741913, 4000081.7162713646], [14154271.715607245, 4000302.153339294], [14155041.190549305, 4000360.9372721342], [14156983.19873771, 4000640.165071943], [14158912.993037248, 4000919.399671307], [14160781.717897676, 4001125.1558314012], [14162797.009413889, 4001345.612956999], [14164580.237058092, 4001610.1671023793], [14166522.245245533, 4001815.935658166], [14167120.725757152, 4001933.519348061], [14167987.911802663, 4002036.4060658], [14173362.02251159, 4002697.8427284476], [14178760.46447004, 4003210.7735754605], [14183572.832858339, 4003668.0188718894], [14206901.358889744, 4005932.0826477716], [14210370.103074845, 4006505.5204501776], [14216550.330390768, 4008623.077985625], [14220983.971725512, 4010961.6694398993], [14225038.982532345, 4014271.8137577237], [14230608.515449705, 4021896.1459267535], [14237533.789931284, 4030556.9660328445], [14248477.43355596, 4044607.966866741], [14248819.422419427, 4044725.9920548676], [14248894.821504684, 4044751.2460003477], [14332384.43799789, 4072715.0099619655], [14339458.685080042, 4075084.437250298], [14339432.408530401, 4075075.6362608722], [14347742.667809354, 4070231.176358625], [14347779.925183792, 4070274.6906909533], [14337204.573632397, 4057923.327535437], [14332384.437499993, 4053484.2600356997], [14325516.027051724, 4047158.849973145], [14297318.800024424, 4007338.115534628], [14296205.605126167, 4000638.8920895318], [14289147.949369663, 3995496.544127517], [14248894.821533248, 3948046.126943193], [14240260.255669821, 3937867.6942487117], [14234601.398877025, 3931197.0296311476], [14233310.092817476, 3925646.324512699], [14227187.520809716, 3914566.141529868], [14215309.731123524, 3889770.2929695556], [14213829.181949753, 3888669.1786231115], [14154273.254398886, 3837917.649432496], [14085910.608311396, 3787395.72363925], [14026312.49388151, 3787395.72363925]]]]}}]} \ No newline at end of file diff --git a/prediction/data/zones/특정어업수역Ⅲ.json b/prediction/data/zones/특정어업수역Ⅲ.json new file mode 100644 index 0000000..186078b --- /dev/null +++ b/prediction/data/zones/특정어업수역Ⅲ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed3", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2162", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[13817590.293393573, 4163976.6556012244], [13935718.55954324, 4163976.6556012244], [13935619.01320107, 4163881.1438622964], [13923844.505073382, 4152583.8553656973], [13918348.255484505, 4147059.076131751], [13915673.414018149, 4144230.73373971], [13914293.244677586, 4142310.8465966308], [13912412.305929314, 4139320.052336007], [13910836.71437977, 4135854.113539339], [13910055.02554902, 4132998.86846704], [13908345.081232697, 4127825.5937968497], [13907722.172945773, 4125120.982583305], [13907392.39797139, 4121971.379578588], [13905548.100886794, 4109218.5915996092], [13903996.937114129, 4098184.7930658716], [13902433.559452614, 4086362.162986858], [13899270.162467, 4063581.503750727], [13896473.182120506, 4043914.5936157014], [13897865.565350175, 4034226.54128085], [13899148.023587234, 4029334.0367157785], [13901432.020639004, 4024841.2655827063], [13904314.498200562, 4020453.314503754], [13908809.208975887, 4016346.6591829173], [13913084.069767442, 4012962.3532931497], [13916381.819520816, 4010770.4459119253], [13921743.716342429, 4008961.335245877], [13958947.219114931, 4002242.1822701376], [13979370.589551304, 3998601.864321506], [14011454.723518057, 3992883.0996783567], [14012407.40678011, 3992706.863639312], [14013372.303930415, 3992530.6303036474], [14014324.987193013, 3992369.0854542954], [14015277.670454111, 3992192.8573017977], [14016230.35371629, 3992031.3172021704], [14017195.250866242, 3991855.094230278], [14018147.934129067, 3991678.873962591], [14019100.617390765, 3991502.656396452], [14020053.300652908, 3991341.126002646], [14021018.197802687, 3991179.5978787914], [14021909.811624419, 3991018.0720274393], [14022813.639334908, 3990856.5484449966], [14023766.32259687, 3990650.976261227], [14024731.219746705, 3990445.407755316], [14025525.122466035, 3990298.575359531], [14026477.805727897, 3990063.6474289736], [14027442.702878024, 3989828.724300673], [14027845.761180786, 3989740.629365874], [14028798.444442954, 3989505.7128408654], [14029763.341592584, 3989270.80111587], [14030716.024854451, 3989050.57573502], [14031668.70811654, 3988815.6733126394], [14033073.30523366, 3988478.0094901477], [14033024.449681701, 3988213.757764475], [14033000.021905882, 3987949.512112862], [14032963.380242558, 3987582.5143390666], [14032938.952466676, 3987332.9625437283], [14032914.524689825, 3987098.09521391], [14032902.310802005, 3986863.232680407], [14032877.883026825, 3986613.69649413], [14032877.883025693, 3986378.843854627], [14032841.241362942, 3986143.996010515], [14032841.241361871, 3985894.47543004], [14032829.027473792, 3985659.6374763353], [14032829.027474709, 3985410.127403529], [14032829.027473787, 3985175.2993370066], [14032829.02747455, 3984940.4760656278], [14032816.813586919, 3984690.9815914035], [14032829.027474267, 3984456.1682059844], [14032829.027474323, 3984221.3596118884], [14032829.02747492, 3983971.8807323813], [14032841.24136266, 3983737.082022118], [14032841.24136211, 3983487.613641918], [14032865.66913836, 3983252.8248136323], [14032877.88302635, 3983003.3669318845], [14032902.310802022, 3982783.2615266712], [14032914.52469013, 3982533.813822452], [14032938.952465724, 3982299.044452602], [14032963.38024159, 3982064.2798706265], [14032975.59412962, 3981814.847749137], [14033012.23579437, 3981594.76507107], [14033036.663569707, 3981345.3431236055], [14033073.305233562, 3981110.597991455], [14033097.733010307, 3980861.18653368], [14033146.588561533, 3980641.122086566], [14033171.016337542, 3980391.7207993036], [14033219.871889306, 3980171.665326035], [14033268.727441864, 3979922.2742063804], [14033305.369105555, 3979687.5580867655], [14033354.224656736, 3979452.846750307], [14033403.080208756, 3979218.140198897], [14033464.149649367, 3978968.769728522], [14033500.79131351, 3978748.741443538], [14033561.860753022, 3978514.0492399754], [14033622.930192148, 3978279.3618192296], [14033683.999632282, 3978044.6791785043], [14033745.06907279, 3977824.6685465015], [14033806.138512583, 3977575.3282415215], [14033867.207951993, 3977355.3265721146], [14033940.49128036, 3977120.6627559136], [14034013.774608184, 3976886.0037188698], [14034074.84404724, 3976651.349460947], [14034148.127375823, 3976431.365434353], [14034233.624591732, 3976211.385607544], [14034306.907918751, 3975976.745086343], [14034380.191247718, 3975756.7739374605], [14034465.688463435, 3975522.1426732005], [14034563.399566704, 3975302.1802014867], [14034636.682894846, 3975067.5581912696], [14034722.18011089, 3974862.2678504367], [14034819.89121484, 3974627.654795317], [14034905.388430584, 3974407.7093927874], [14035015.313421464, 3974187.7681863704], [14035100.810637798, 3973967.831176343], [14035198.52174194, 3973747.8983624075], [14035296.232845142, 3973527.9697442516], [14035406.157837268, 3973322.706818716], [14035503.868941093, 3973102.7863105624], [14035564.938380616, 3972970.836018713], [14035430.585612448, 3972882.8699968406], [14035051.955086311, 3972589.6547698523], [14034685.538446855, 3972281.7868043673], [14034306.907918967, 3971988.5868595946], [14033928.277392257, 3971680.7349405857], [14033561.860752953, 3971358.232219437], [14033207.658001786, 3971050.3971231086], [14032865.66913857, 3970727.9120247746], [14032511.46638746, 3970376.1203815714], [14032169.477523897, 3970053.654131018], [14031851.916436315, 3969701.8830453446], [14031522.141461194, 3969350.1226848057], [14031192.36648596, 3969013.029068952], [14030874.80539861, 3968646.634131671], [14030569.4581991, 3968265.5957370186], [14030276.324888123, 3967899.224527907], [14029970.977688076, 3967518.2108068704], [14029677.844376301, 3967137.2096621543], [14029409.138841135, 3966756.2210914562], [14029140.433306115, 3966360.592421976], [14028871.727769978, 3965964.977308897], [14028737.375003058, 3965759.848882349], [14028615.236123221, 3965554.724099639], [14028480.883355275, 3965349.6029609945], [14028358.744475357, 3965144.4854657836], [14028224.391707785, 3964939.371614421], [14028102.252828015, 3964734.2614048333], [14027980.11394778, 3964529.1548381275], [14027857.975069236, 3964324.051914157], [14027760.26396438, 3964104.302822176], [14027638.125085097, 3963899.207441546], [14027528.200093549, 3963679.4664326143], [14027406.061214069, 3963474.378595102], [14027308.350109257, 3963254.6456658943], [14027198.425118413, 3963049.5653696535], [14027088.500126269, 3962815.1923459424], [14027003.00291053, 3962610.11984976], [14026893.077918677, 3962390.4033569284], [14026795.366814636, 3962170.6910431627], [14026697.655711418, 3961965.629985329], [14026599.944607599, 3961731.278946997], [14026502.233502936, 3961511.5791640463], [14026416.736287547, 3961291.883556766], [14026331.239071513, 3961072.192127874], [14026245.741855916, 3960837.859204179], [14026148.030752573, 3960603.531032333], [14026050.319648793, 3960515.6591924117], [14025696.116896924, 3960208.113014822], [14025354.12803417, 3959885.930555696], [14024987.711394375, 3959549.113039522], [14024645.722530954, 3959226.948944662], [14024291.51977964, 3958890.150626061], [14023961.74480434, 3958538.7193601266], [14023644.183716903, 3958172.6564824986], [14023314.408742214, 3957835.8888684427], [14023009.061542125, 3957469.8482367387], [14022703.714343427, 3957103.8191892453], [14022398.367143923, 3956723.1612672796], [14022117.447720738, 3956342.515870507], [14021812.100520544, 3955947.2435213765], [14021543.394986114, 3955566.6236525774], [14021250.261673959, 3955171.377811075], [14020993.770026248, 3954776.145468736], [14020859.417258942, 3954556.577778357], [14020737.27837941, 3954366.2891494785], [14020602.925611844, 3954176.0036486983], [14020480.786732143, 3953956.4473441243], [14020370.861740284, 3953751.53188359], [14020248.722860433, 3953546.620051208], [14020126.583980937, 3953341.7118456624], [14020004.445100708, 3953122.171365647], [14019882.306221237, 3952917.270673172], [14019784.595117368, 3952697.7382422695], [14019662.456236959, 3952492.8450626153], [14019552.531245224, 3952273.3206794215], [14019442.606254242, 3952068.435010681], [14019344.89515069, 3951834.2844000966], [14019234.97015812, 3951629.406499703], [14019137.259054784, 3951424.532224624], [14019027.33406315, 3951190.394634004], [14018941.836846726, 3950985.528125728], [14018831.911855552, 3950751.3994105365], [14018734.200752072, 3950546.540667035], [14018648.703535682, 3950312.4208263634], [14018563.206319205, 3950092.937773037], [14018465.495216299, 3949858.827100896], [14018379.99800022, 3949639.352642628], [14018306.71467212, 3949405.251136331], [14018209.003568063, 3949185.785271415], [14018135.72024025, 3948951.692931064], [14018062.43691314, 3948732.2356575206], [14017989.153585482, 3948498.1524819196], [14017915.87025767, 3948278.7037986964], [14017842.586929433, 3948044.6297840844], [14017757.089713955, 3947825.1896894635], [14017696.02027415, 3947591.124836192], [14017622.736946309, 3947371.6933298754], [14017561.667506203, 3947123.0093109384], [14017500.598066194, 3946888.958639055], [14017451.742514167, 3946654.9126930023], [14017390.673074305, 3946420.8714734227], [14017329.603633871, 3946157.5807487303], [14017195.25086627, 3945791.9091077414], [14017109.753650622, 3945557.885310957], [14017036.470322493, 3945338.4922908037], [14016963.186994748, 3945104.477646201], [14016889.903667673, 3944885.0932069574], [14016816.620339664, 3944651.0877120267], [14016743.337011732, 3944431.7118500136], [14016682.267571904, 3944197.715505999], [14016621.198132234, 3943963.7238824223], [14016547.914804032, 3943729.736980628], [14016486.845363976, 3943510.378546276], [14016425.775924595, 3943261.7773378296], [14016364.70648495, 3943042.427753915], [14016315.850932516, 3942793.8365748394], [14016254.781492097, 3942574.4958398864], [14016193.712052723, 3942325.9146883073], [14016157.070389032, 3942106.582801053], [14016108.214837069, 3941858.0116748144], [14016059.359284986, 3941624.0672444003], [14016034.931508765, 3941390.1275309804], [14015986.075957134, 3941141.571752944], [14015937.22040512, 3940922.2622533315], [14015912.792629696, 3940673.7164975833], [14015876.150965236, 3940439.7959427596], [14015839.509301143, 3940191.260519465], [14015802.867637865, 3939971.9689781107], [14015790.653749412, 3939723.443572689], [14015754.012085313, 3939489.542170439], [14015729.584309753, 3939241.0270946748], [14015717.37042194, 3939007.1354134823], [14015680.728758091, 3938758.630664511], [14015668.514870305, 3938510.131235958], [14015656.300982298, 3938290.871450771], [14015644.087093962, 3938042.3820334156], [14015644.087093726, 3937808.5144985737], [14015619.659317939, 3937560.035403366], [14015619.659317758, 3937326.1775837634], [14015607.445430323, 3937077.708810477], [14015607.445430165, 3936843.86070301], [14015607.44543046, 3936610.017304439], [14015607.445429819, 3936361.563852228], [14015619.659318132, 3936127.730163849], [14015619.65931785, 3935879.2870282456], [14015619.659318443, 3935864.672891335], [14015644.087093579, 3935645.4630488665], [14015644.087094065, 3935397.030227534], [14015656.300982174, 3935163.215955291], [14015668.51486993, 3934914.7934458014], [14015680.728758322, 3934680.988878479], [14015717.370422252, 3934432.576680492], [14015729.584309453, 3934213.393858009], [14015754.012085263, 3933964.991656822], [14015790.653749326, 3933716.5947647644], [14015802.867637549, 3933482.8143059933], [14015839.509301828, 3933234.4277204718], [14015863.937076895, 3933000.6569608934], [14015912.79262938, 3932766.8909023306], [14015937.220405562, 3932533.1295448784], [14015986.075957565, 3932299.3728892817], [14016010.503732808, 3932065.6209347122], [14016059.359284472, 3931817.2646324276], [14016108.214836905, 3931583.522371577], [14016169.284276439, 3931349.784810239], [14016205.925940333, 3931101.4438007176], [14016254.781492744, 3930882.3237842843], [14016315.85093268, 3930633.992758967], [14016364.706484258, 3930414.8815508736], [14016425.77592429, 3930166.5605083024], [14016486.845364062, 3929947.45810794], [14016547.91480444, 3929699.147045077], [14016621.19813177, 3929480.053450812], [14016682.267571429, 3929246.358166313], [14016743.337011438, 3929012.667577336], [14016816.620339306, 3928793.5869140886], [14016889.903667254, 3928545.3004834815], [14016963.186994506, 3928326.228621952], [14017036.470323365, 3928092.5565168317], [14017109.753650554, 3927873.4931812496], [14017195.250866888, 3927639.8301689653], [14017280.748082507, 3927420.7753582653], [14017354.031410536, 3927187.121436862], [14017451.742513658, 3926982.678106103], [14017537.239729747, 3926749.032981505], [14017622.736945918, 3926529.994938449], [14017720.448049057, 3926296.358903417], [14017818.159152932, 3926091.931220958], [14017915.870256566, 3925858.3039796036], [14018001.367473086, 3925653.883990288], [14018111.292464526, 3925420.265541152], [14018209.00356852, 3925201.2525036694], [14018318.928560698, 3924996.844052992], [14018428.853552194, 3924763.2387903365], [14018526.564656094, 3924558.8380302167], [14018636.489648066, 3924339.8411988374], [14018758.628527954, 3924135.447872815], [14018856.339631462, 3923916.459004483], [14018978.478511153, 3923712.073110865], [14019100.617391173, 3923493.0922050807], [14019222.756270064, 3923288.713741847], [14019344.895150287, 3923084.338865018], [14019467.034029689, 3922879.9675755682], [14019589.172909968, 3922661.0023167264], [14019723.525677131, 3922471.235755037], [14019833.45066894, 3922252.2781808744], [14019980.017325126, 3922062.518278752], [14020114.370092534, 3921872.7614680813], [14020383.075627644, 3921464.06499056], [14020651.781162936, 3921084.5739564244], [14020932.700586699, 3920690.500196857], [14021225.833898693, 3920311.0343588293], [14021518.967209466, 3919931.5808786163], [14021824.31440906, 3919552.1397545147], [14021873.169961166, 3919508.3588811457], [14022007.522728465, 3919158.1178169283], [14022117.447720494, 3918939.222496516], [14022215.158823974, 3918720.331287067], [14022312.869927935, 3918486.851860621], [14022410.581031999, 3918282.5611990155], [14022520.506023144, 3918063.6823207946], [14022618.217127495, 3917844.807552783], [14022728.14211945, 3917625.9368926086], [14022838.06711147, 3917421.6613179413], [14022960.205990292, 3917202.798602683], [14023057.917095127, 3916998.53044049], [14023180.055974487, 3916779.675667872], [14023302.194854327, 3916575.4149192595], [14023424.333733585, 3916356.568086974], [14023546.472613554, 3916166.904154751], [14023656.397605527, 3915948.0649889517], [14023778.536485165, 3915758.4077001447], [14023912.889252687, 3915539.5761998114], [14024035.028132746, 3915335.337170445], [14024303.73366759, 3914941.4577129018], [14024560.225315124, 3914547.5915551204], [14024853.358626463, 3914153.7386953356], [14025134.278049812, 3913759.899131608], [14025427.411361214, 3913380.6587829227], [14025720.544672925, 3913001.430759424], [14026025.891872894, 3912636.800052067], [14026272.928939708, 3912341.809856742], [14026312.49388151, 3787395.72363925], [13961312.04903013, 3787395.72363925], [13947389.033479117, 3802232.5362782693], [13947389.033479117, 3802232.5362782683], [13822584.485054709, 3935228.2723445967], [13818455.7465445, 3939627.988734125], [13804540.810181938, 4028802.026185181], [13817531.794596978, 4163881.1427735523], [13817531.794716273, 4163881.144013962], [13817590.293393573, 4163976.6556012244]]]]}}]} \ No newline at end of file diff --git a/prediction/data/zones/특정어업수역Ⅳ.json b/prediction/data/zones/특정어업수역Ⅳ.json new file mode 100644 index 0000000..1ce6f88 --- /dev/null +++ b/prediction/data/zones/특정어업수역Ⅳ.json @@ -0,0 +1 @@ +{"type": "FeatureCollection", "name": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed4", "crs": {"type": "name", "properties": {"name": "urn:ogc:def:crs:OGC:1.3:CRS84"}}, "features": [{"type": "Feature", "properties": {"fid": 0, "GML_ID": null, "OBJECTID": null, "ZONE_NM": null, "MNCT_NO": null, "MNCT_SCALE": null, "MNCT_NM": null, "RELREGLTN": null, "RELGOAG": null, "REVIYR": null, "ZONE_DESC": null, "PHOTO1_PAT": null, "ID": -2147483647, "CATE_CD": null, "ADR_CD": null, "ADR_KNM": null, "ORIGIN": null, "ORIYR": null, "ORIORG": null, "NAME": "\ud2b9\uc815\uc5b4\uc5c5\uc218\uc5ed\u2163", "WARD_NM": null, "WARD_ID": null, "GISID": null, "FID_2": null, "NAME_2": null, "FID_3": null, "NAME_3": null, "GID": null, "NAME_4": null, "FID_4": null, "NAME_5": null, "FID_5": null, "NAME_6": null}, "geometry": {"type": "MultiPolygon", "coordinates": [[[[13859276.603817873, 4232038.462456921], [13859276.603762543, 4321218.244482412], [13859276.603710985, 4404317.064005076], [13840719.645028654, 4439106.786523586], [13884632.712472571, 4439106.787250583], [13884632.712472571, 4439504.084564682], [13940418.269436067, 4439504.375880923], [13969123.924724836, 4439504.525783945], [13968718.329494288, 4438626.439593866], [13962623.599395147, 4425543.915710401], [13960437.31344761, 4420657.3891166765], [13958238.813611617, 4416093.569832627], [13958143.094601436, 4415900.994484875], [13958143.094601437, 4415900.994484875], [13957298.344237303, 4414201.456484755], [13953878.455604602, 4406316.186534493], [13949652.450365951, 4397019.979821594], [13948553.200448176, 4393395.13065616], [13947612.731073817, 4389132.176741289], [13947612.731072996, 4387549.226905922], [13947466.164417507, 4385829.556682826], [13947783.725505754, 4381721.729468383], [13948260.06713652, 4379835.70012994], [13949359.317054221, 4375897.403884492], [13951093.689146286, 4371808.582233328], [13954867.780530114, 4365670.678186072], [13964809.885341855, 4351190.629491161], [13978342.873219142, 4331838.456925102], [13980382.592510404, 4329007.496874151], [13981728.043604897, 4327079.749205159], [13985775.34591557, 4321280.81855131], [13997066.763484716, 4305102.598482491], [13999424.043863578, 4300225.286038025], [14003039.354703771, 4290447.064438686], [14005091.287883686, 4284626.561498255], [14006520.312777169, 4279426.932176922], [14007631.77658257, 4275178.643476352], [14008242.470981453, 4271549.325573796], [14009378.362562515, 4262248.123573576], [14009427.990871342, 4261704.85208626], [14009708.137538105, 4258638.140769343], [14009854.704193696, 4257224.555715567], [14009378.362562606, 4254698.603440943], [14005347.779531531, 4240996.452433007], [14002367.590864772, 4231511.1380338315], [14001280.554835469, 4227266.412716273], [14000486.652116666, 4225212.134400094], [13998047.81589918, 4222926.459154359], [13991387.305576058, 4216684.234498038], [13970721.407121927, 4197120.494488488], [13958654.085803084, 4185745.4565721145], [13956602.15262321, 4184012.5742896623], [13944065.033685392, 4171984.566055202], [13940467.606607554, 4168533.224265296], [13935619.01320107, 4163881.1438622964], [13935718.55954324, 4163976.6556012244], [13817590.293393573, 4163976.6556012244], [13859276.603817873, 4232038.462456921]]]]}}]} \ No newline at end of file diff --git a/prediction/fleet_tracker.py b/prediction/fleet_tracker.py new file mode 100644 index 0000000..981d7db --- /dev/null +++ b/prediction/fleet_tracker.py @@ -0,0 +1,322 @@ +"""등록 선단 기반 추적기.""" +import logging +import re +import time +from datetime import datetime, timezone +from typing import Optional + +import pandas as pd + +logger = logging.getLogger(__name__) + +# 어구 이름 패턴 +GEAR_PATTERN = re.compile(r'^(.+?)_(\d+)_(\d*)$') +GEAR_PATTERN_PCT = re.compile(r'^(.+?)%$') + +_REGISTRY_CACHE_SEC = 3600 + + +class FleetTracker: + def __init__(self) -> None: + self._companies: dict[int, dict] = {} # id → {name_cn, name_en} + self._vessels: dict[int, dict] = {} # id → {permit_no, name_cn, ...} + self._name_cn_map: dict[str, int] = {} # name_cn → vessel_id + self._name_en_map: dict[str, int] = {} # name_en(lowercase) → vessel_id + self._mmsi_to_vid: dict[str, int] = {} # mmsi → vessel_id (매칭된 것만) + self._gear_active: dict[str, dict] = {} # mmsi → {name, parent_mmsi, ...} + self._last_registry_load: float = 0.0 + + def load_registry(self, conn) -> None: + """DB에서 fleet_companies + fleet_vessels 로드. 1시간 캐시.""" + if time.time() - self._last_registry_load < _REGISTRY_CACHE_SEC: + return + + cur = conn.cursor() + cur.execute('SELECT id, name_cn, name_en FROM kcg.fleet_companies') + self._companies = {r[0]: {'name_cn': r[1], 'name_en': r[2]} for r in cur.fetchall()} + + cur.execute( + """SELECT id, company_id, permit_no, name_cn, name_en, tonnage, + gear_code, fleet_role, pair_vessel_id, mmsi + FROM kcg.fleet_vessels""" + ) + self._vessels = {} + self._name_cn_map = {} + self._name_en_map = {} + self._mmsi_to_vid = {} + + for r in cur.fetchall(): + vid = r[0] + v: dict = { + 'id': vid, + 'company_id': r[1], + 'permit_no': r[2], + 'name_cn': r[3], + 'name_en': r[4], + 'tonnage': r[5], + 'gear_code': r[6], + 'fleet_role': r[7], + 'pair_vessel_id': r[8], + 'mmsi': r[9], + } + self._vessels[vid] = v + if r[3]: + self._name_cn_map[r[3]] = vid + if r[4]: + self._name_en_map[r[4].lower().strip()] = vid + if r[9]: + self._mmsi_to_vid[r[9]] = vid + + cur.close() + self._last_registry_load = time.time() + logger.info( + 'fleet registry loaded: %d companies, %d vessels', + len(self._companies), + len(self._vessels), + ) + + def match_ais_to_registry(self, ais_vessels: list[dict], conn) -> None: + """AIS 선박을 등록 선단에 매칭. DB 업데이트. + + ais_vessels: [{mmsi, name, lat, lon, sog, cog}, ...] + """ + cur = conn.cursor() + matched = 0 + + for v in ais_vessels: + mmsi = v.get('mmsi', '') + name = v.get('name', '') + if not mmsi or not name: + continue + + # 이미 매칭됨 → last_seen_at 업데이트 + if mmsi in self._mmsi_to_vid: + cur.execute( + 'UPDATE kcg.fleet_vessels SET last_seen_at = NOW() WHERE id = %s', + (self._mmsi_to_vid[mmsi],), + ) + continue + + # NAME_EXACT 매칭 + vid: Optional[int] = self._name_cn_map.get(name) + if not vid: + vid = self._name_en_map.get(name.lower().strip()) + + if vid: + cur.execute( + """UPDATE kcg.fleet_vessels + SET mmsi = %s, match_confidence = 0.95, match_method = 'NAME_EXACT', + last_seen_at = NOW(), updated_at = NOW() + WHERE id = %s AND (mmsi IS NULL OR mmsi = %s)""", + (mmsi, vid, mmsi), + ) + self._mmsi_to_vid[mmsi] = vid + matched += 1 + + conn.commit() + cur.close() + if matched > 0: + logger.info('AIS→registry matched: %d vessels', matched) + + def track_gear_identity(self, gear_signals: list[dict], conn) -> None: + """어구/어망 정체성 추적. + + gear_signals: [{mmsi, name, lat, lon}, ...] — 이름이 XXX_숫자_숫자 패턴인 AIS 신호 + """ + cur = conn.cursor() + now = datetime.now(timezone.utc) + + for g in gear_signals: + mmsi = g['mmsi'] + name = g['name'] + lat = g.get('lat', 0) + lon = g.get('lon', 0) + + # 모선명 + 인덱스 추출 + parent_name: Optional[str] = None + idx1: Optional[int] = None + idx2: Optional[int] = None + + m = GEAR_PATTERN.match(name) + if m: + parent_name = m.group(1).strip() + idx1 = int(m.group(2)) + idx2 = int(m.group(3)) if m.group(3) else None + else: + m2 = GEAR_PATTERN_PCT.match(name) + if m2: + parent_name = m2.group(1).strip() + + # 모선 매칭 + parent_mmsi: Optional[str] = None + parent_vid: Optional[int] = None + if parent_name: + vid = self._name_cn_map.get(parent_name) + if not vid: + vid = self._name_en_map.get(parent_name.lower()) + if vid: + parent_vid = vid + parent_mmsi = self._vessels[vid].get('mmsi') + + match_method: Optional[str] = 'NAME_PARENT' if parent_vid else None + confidence = 0.9 if parent_vid else 0.0 + + # 기존 활성 행 조회 + cur.execute( + """SELECT id, name FROM kcg.gear_identity_log + WHERE mmsi = %s AND is_active = TRUE""", + (mmsi,), + ) + existing = cur.fetchone() + + if existing: + if existing[1] == name: + # 같은 MMSI + 같은 이름 → 위치/시간 업데이트 + cur.execute( + """UPDATE kcg.gear_identity_log + SET last_seen_at = %s, lat = %s, lon = %s + WHERE id = %s""", + (now, lat, lon, existing[0]), + ) + else: + # 같은 MMSI + 다른 이름 → 이전 비활성화 + 새 행 + cur.execute( + 'UPDATE kcg.gear_identity_log SET is_active = FALSE WHERE id = %s', + (existing[0],), + ) + cur.execute( + """INSERT INTO kcg.gear_identity_log + (mmsi, name, parent_name, parent_mmsi, parent_vessel_id, + gear_index_1, gear_index_2, lat, lon, + match_method, match_confidence, first_seen_at, last_seen_at) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", + (mmsi, name, parent_name, parent_mmsi, parent_vid, + idx1, idx2, lat, lon, + match_method, confidence, now, now), + ) + else: + # 새 MMSI → 같은 이름이 다른 MMSI로 있는지 확인 + cur.execute( + """SELECT id, mmsi FROM kcg.gear_identity_log + WHERE name = %s AND is_active = TRUE AND mmsi != %s""", + (name, mmsi), + ) + old_mmsi_row = cur.fetchone() + if old_mmsi_row: + # 같은 이름 + 다른 MMSI → MMSI 변경 + cur.execute( + 'UPDATE kcg.gear_identity_log SET is_active = FALSE WHERE id = %s', + (old_mmsi_row[0],), + ) + logger.info('gear MMSI change: %s → %s (name=%s)', old_mmsi_row[1], mmsi, name) + + cur.execute( + """INSERT INTO kcg.gear_identity_log + (mmsi, name, parent_name, parent_mmsi, parent_vessel_id, + gear_index_1, gear_index_2, lat, lon, + match_method, match_confidence, first_seen_at, last_seen_at) + VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)""", + (mmsi, name, parent_name, parent_mmsi, parent_vid, + idx1, idx2, lat, lon, + match_method, confidence, now, now), + ) + + conn.commit() + cur.close() + + def build_fleet_clusters(self, vessel_dfs: dict[str, pd.DataFrame]) -> dict[str, dict]: + """등록 선단 기준으로 cluster 정보 구성. + + Returns: {mmsi → {cluster_id, cluster_size, is_leader, fleet_role}} + cluster_id = company_id (등록 선단 기준) + """ + results: dict[str, dict] = {} + + # 회사별로 현재 AIS 수신 중인 선박 그룹핑 + company_vessels: dict[int, list[str]] = {} + for mmsi, vid in self._mmsi_to_vid.items(): + v = self._vessels.get(vid) + if not v or mmsi not in vessel_dfs: + continue + cid = v['company_id'] + company_vessels.setdefault(cid, []).append(mmsi) + + for cid, mmsis in company_vessels.items(): + if len(mmsis) < 2: + # 단독 선박 → NOISE + for mmsi in mmsis: + v = self._vessels.get(self._mmsi_to_vid.get(mmsi, -1), {}) + results[mmsi] = { + 'cluster_id': -1, + 'cluster_size': 1, + 'is_leader': False, + 'fleet_role': v.get('fleet_role', 'NOISE'), + } + continue + + # 2척 이상 → 등록 선단 클러스터 + for mmsi in mmsis: + vid = self._mmsi_to_vid[mmsi] + v = self._vessels[vid] + results[mmsi] = { + 'cluster_id': cid, + 'cluster_size': len(mmsis), + 'is_leader': v['fleet_role'] == 'MAIN', + 'fleet_role': v['fleet_role'], + } + + # 매칭 안 된 선박 → NOISE + for mmsi in vessel_dfs: + if mmsi not in results: + results[mmsi] = { + 'cluster_id': -1, + 'cluster_size': 0, + 'is_leader': False, + 'fleet_role': 'NOISE', + } + + return results + + def save_snapshot(self, vessel_dfs: dict[str, pd.DataFrame], conn) -> None: + """fleet_tracking_snapshot 저장.""" + now = datetime.now(timezone.utc) + cur = conn.cursor() + + company_vessels: dict[int, list[str]] = {} + for mmsi, vid in self._mmsi_to_vid.items(): + v = self._vessels.get(vid) + if not v or mmsi not in vessel_dfs: + continue + company_vessels.setdefault(v['company_id'], []).append(mmsi) + + for cid, mmsis in company_vessels.items(): + active = len(mmsis) + total = sum(1 for v in self._vessels.values() if v['company_id'] == cid) + + lats: list[float] = [] + lons: list[float] = [] + for mmsi in mmsis: + df = vessel_dfs.get(mmsi) + if df is not None and len(df) > 0: + last = df.iloc[-1] + lats.append(float(last['lat'])) + lons.append(float(last['lon'])) + + center_lat = sum(lats) / len(lats) if lats else None + center_lon = sum(lons) / len(lons) if lons else None + + cur.execute( + """INSERT INTO kcg.fleet_tracking_snapshot + (company_id, snapshot_time, total_vessels, active_vessels, + center_lat, center_lon) + VALUES (%s, %s, %s, %s, %s, %s)""", + (cid, now, total, active, center_lat, center_lon), + ) + + conn.commit() + cur.close() + logger.info('fleet snapshot saved: %d companies', len(company_vessels)) + + +# 싱글턴 +fleet_tracker = FleetTracker() diff --git a/prediction/models/result.py b/prediction/models/result.py index 9792351..e1680a5 100644 --- a/prediction/models/result.py +++ b/prediction/models/result.py @@ -56,29 +56,41 @@ class AnalysisResult: def to_db_tuple(self) -> tuple: import json + + def _f(v: object) -> float: + """numpy float → Python float 변환.""" + return float(v) if v is not None else 0.0 + + def _i(v: object) -> int: + """numpy int → Python int 변환.""" + return int(v) if v is not None else 0 + + # features dict 내부 numpy 값도 변환 + safe_features = {k: float(v) for k, v in self.features.items()} if self.features else {} + return ( - self.mmsi, + str(self.mmsi), self.timestamp, - self.vessel_type, - self.confidence, - self.fishing_pct, - self.cluster_id, - self.season, - self.zone, - self.dist_to_baseline_nm, - self.activity_state, - self.ucaf_score, - self.ucft_score, - self.is_dark, - self.gap_duration_min, - self.spoofing_score, - self.bd09_offset_m, - self.speed_jump_count, - self.cluster_size, - self.is_leader, - self.fleet_role, - self.risk_score, - self.risk_level, - json.dumps(self.features), + str(self.vessel_type), + _f(self.confidence), + _f(self.fishing_pct), + _i(self.cluster_id), + str(self.season), + str(self.zone), + _f(self.dist_to_baseline_nm), + str(self.activity_state), + _f(self.ucaf_score), + _f(self.ucft_score), + bool(self.is_dark), + _i(self.gap_duration_min), + _f(self.spoofing_score), + _f(self.bd09_offset_m), + _i(self.speed_jump_count), + _i(self.cluster_size), + bool(self.is_leader), + str(self.fleet_role), + _i(self.risk_score), + str(self.risk_level), + json.dumps(safe_features), self.analyzed_at, ) diff --git a/prediction/scheduler.py b/prediction/scheduler.py index 1b6f755..9d889d3 100644 --- a/prediction/scheduler.py +++ b/prediction/scheduler.py @@ -25,6 +25,7 @@ def get_last_run() -> dict: def run_analysis_cycle(): """5분 주기 분석 사이클 — 인메모리 캐시 기반.""" + import re as _re from cache.vessel_store import vessel_store from db import snpdb, kcgdb from pipeline.orchestrator import ChineseFishingVesselPipeline @@ -32,8 +33,8 @@ def run_analysis_cycle(): from algorithms.fishing_pattern import compute_ucaf_score, compute_ucft_score from algorithms.dark_vessel import is_dark_vessel from algorithms.spoofing import compute_spoofing_score, count_speed_jumps, compute_bd09_offset - from algorithms.fleet import assign_fleet_roles from algorithms.risk import compute_vessel_risk_score + from fleet_tracker import fleet_tracker from models.result import AnalysisResult start = time.time() @@ -71,9 +72,30 @@ def run_analysis_cycle(): _last_run['vessel_count'] = 0 return - # 4. 선단 역할 분석 - cluster_map = {c['mmsi']: c['cluster_id'] for c in classifications} - fleet_roles = assign_fleet_roles(vessel_dfs, cluster_map) + # 4. 등록 선단 기반 fleet 분석 + _gear_re = _re.compile(r'^.+_\d+_\d*$|%$') + with kcgdb.get_conn() as kcg_conn: + fleet_tracker.load_registry(kcg_conn) + + all_ais = [] + for mmsi, df in vessel_dfs.items(): + if len(df) > 0: + last = df.iloc[-1] + all_ais.append({ + 'mmsi': mmsi, + 'name': vessel_store.get_vessel_info(mmsi).get('name', ''), + 'lat': float(last['lat']), + 'lon': float(last['lon']), + }) + + fleet_tracker.match_ais_to_registry(all_ais, kcg_conn) + + gear_signals = [v for v in all_ais if _gear_re.match(v.get('name', ''))] + fleet_tracker.track_gear_identity(gear_signals, kcg_conn) + + fleet_roles = fleet_tracker.build_fleet_clusters(vessel_dfs) + + fleet_tracker.save_snapshot(vessel_dfs, kcg_conn) # 5. 선박별 추가 알고리즘 → AnalysisResult 생성 results = [] @@ -116,7 +138,7 @@ def run_analysis_cycle(): vessel_type=c['vessel_type'], confidence=c['confidence'], fishing_pct=c['fishing_pct'], - cluster_id=c['cluster_id'], + cluster_id=fleet_info.get('cluster_id', -1), season=c['season'], zone=zone_info.get('zone', 'EEZ_OR_BEYOND'), dist_to_baseline_nm=zone_info.get('dist_from_baseline_nm', 999.0), diff --git a/prediction/scripts/load_fleet_registry.py b/prediction/scripts/load_fleet_registry.py new file mode 100644 index 0000000..c1cf479 --- /dev/null +++ b/prediction/scripts/load_fleet_registry.py @@ -0,0 +1,176 @@ +"""선단 구성 JSX → kcgdb fleet_companies + fleet_vessels 적재. + +Usage: python3 prediction/scripts/load_fleet_registry.py +""" + +import json +import re +import sys +from pathlib import Path + +import psycopg2 +import psycopg2.extras + +# JSX 파일에서 D 배열 추출 +JSX_PATH = Path(__file__).parent.parent.parent.parent / 'gc-wing-dev' / 'legacy' / '선단구성_906척_어업수역 (1).jsx' + +# kcgdb 접속 — prediction/.env 또는 환경변수 +DB_HOST = '211.208.115.83' +DB_PORT = 5432 +DB_NAME = 'kcgdb' +DB_USER = 'kcg_app' +DB_SCHEMA = 'kcg' + + +def parse_jsx(path: Path) -> list[list]: + """JSX 파일에서 D=[ ... ] 배열을 파싱.""" + text = path.read_text(encoding='utf-8') + + # const D=[ 부터 ]; 까지 추출 + m = re.search(r'const\s+D\s*=\s*\[', text) + if not m: + raise ValueError('D 배열을 찾을 수 없습니다') + + start = m.end() - 1 # [ 위치 + # 중첩 배열을 추적하여 닫는 ] 찾기 + depth = 0 + end = start + for i in range(start, len(text)): + if text[i] == '[': + depth += 1 + elif text[i] == ']': + depth -= 1 + if depth == 0: + end = i + 1 + break + + raw = text[start:end] + + # JavaScript → JSON 변환 (trailing comma 제거) + raw = re.sub(r',\s*]', ']', raw) + raw = re.sub(r',\s*}', '}', raw) + + return json.loads(raw) + + +def load_to_db(data: list[list], db_password: str): + """파싱된 데이터를 DB에 적재.""" + conn = psycopg2.connect( + host=DB_HOST, port=DB_PORT, dbname=DB_NAME, + user=DB_USER, password=db_password, + options=f'-c search_path={DB_SCHEMA}', + ) + conn.autocommit = False + cur = conn.cursor() + + try: + # 기존 데이터 초기화 + cur.execute('DELETE FROM fleet_vessels') + cur.execute('DELETE FROM fleet_companies') + + company_count = 0 + vessel_count = 0 + pair_links = [] # (vessel_id, pair_vessel_id) 후처리 + + for row in data: + if len(row) < 7: + continue + + name_cn = row[0] + name_en = row[1] + + # 회사 INSERT + cur.execute( + 'INSERT INTO fleet_companies (name_cn, name_en) VALUES (%s, %s) RETURNING id', + (name_cn, name_en), + ) + company_id = cur.fetchone()[0] + company_count += 1 + + # 인덱스: 0=own, 1=ownEn, 2=pairs, 3=gn, 4=ot, 5=ps, 6=fc, 7=upt, 8=upts + pairs = row[2] if len(row) > 2 and isinstance(row[2], list) else [] + gn = row[3] if len(row) > 3 and isinstance(row[3], list) else [] + ot = row[4] if len(row) > 4 and isinstance(row[4], list) else [] + ps = row[5] if len(row) > 5 and isinstance(row[5], list) else [] + fc = row[6] if len(row) > 6 and isinstance(row[6], list) else [] + upt = row[7] if len(row) > 7 and isinstance(row[7], list) else [] + upts = row[8] if len(row) > 8 and isinstance(row[8], list) else [] + + def insert_vessel(v, gear_code, role): + nonlocal vessel_count + if not isinstance(v, list) or len(v) < 4: + return None + cur.execute( + '''INSERT INTO fleet_vessels + (company_id, permit_no, name_cn, name_en, tonnage, gear_code, fleet_role) + VALUES (%s, %s, %s, %s, %s, %s, %s) RETURNING id''', + (company_id, v[0], v[1], v[2], v[3], gear_code, role), + ) + vessel_count += 1 + return cur.fetchone()[0] + + # PT 본선쌍 (pairs) + for pair in pairs: + if not isinstance(pair, list) or len(pair) < 2: + continue + main_id = insert_vessel(pair[0], 'C21', 'MAIN') + sub_id = insert_vessel(pair[1], 'C21', 'SUB') + if main_id and sub_id: + pair_links.append((main_id, sub_id)) + + # GN 유자망 + for v in gn: + insert_vessel(v, 'C25', 'GN') + + # OT 기타 + for v in ot: + insert_vessel(v, 'C22', 'OT') + + # PS 선망 + for v in ps: + insert_vessel(v, 'C23', 'PS') + + # FC 운반선 + for v in fc: + insert_vessel(v, 'C40', 'FC') + + # UPT 단독 본선 + for v in upt: + insert_vessel(v, 'C21', 'MAIN_SOLO') + + # UPTS 단독 부속선 + for v in upts: + insert_vessel(v, 'C21', 'SUB_SOLO') + + # PT 쌍 상호 참조 설정 + for main_id, sub_id in pair_links: + cur.execute('UPDATE fleet_vessels SET pair_vessel_id = %s WHERE id = %s', (sub_id, main_id)) + cur.execute('UPDATE fleet_vessels SET pair_vessel_id = %s WHERE id = %s', (main_id, sub_id)) + + conn.commit() + print(f'적재 완료: {company_count}개 회사, {vessel_count}척 선박, {len(pair_links)}쌍 PT') + + except Exception as e: + conn.rollback() + print(f'적재 실패: {e}', file=sys.stderr) + raise + finally: + cur.close() + conn.close() + + +if __name__ == '__main__': + if not JSX_PATH.exists(): + print(f'파일을 찾을 수 없습니다: {JSX_PATH}', file=sys.stderr) + sys.exit(1) + + # DB 비밀번호 — 환경변수 또는 직접 입력 + import os + password = os.environ.get('KCGDB_PASSWORD', 'Kcg2026monitor') + + print(f'JSX 파싱: {JSX_PATH}') + data = parse_jsx(JSX_PATH) + print(f'파싱 완료: {len(data)}개 회사') + + print('DB 적재 시작...') + load_to_db(data, password)