feat: 불법어선 필터 시 수역 폴리곤 오버레이 + 선박 마커 가시성 개선
- WGS84 사전 변환 GeoJSON 생성 (런타임 변환 제거) - FishingZoneLayer: 수역별 색상 fill/line + 이름 라벨 - AnalysisOverlay: 마커 크기 확대, 한글 라벨, 선박명 표시 - fishingAnalysis.ts: EPSG:3857 변환 로직 제거, WGS84 JSON 직접 사용 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
부모
95f320c9f3
커밋
af02ad12ff
@ -9,6 +9,19 @@ const RISK_COLORS: Record<string, string> = {
|
|||||||
LOW: '#22c55e',
|
LOW: '#22c55e',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RISK_LABEL: Record<string, string> = {
|
||||||
|
CRITICAL: '긴급',
|
||||||
|
HIGH: '경고',
|
||||||
|
MEDIUM: '주의',
|
||||||
|
LOW: '정상',
|
||||||
|
};
|
||||||
|
|
||||||
|
const RISK_MARKER_SIZE: Record<string, number> = {
|
||||||
|
CRITICAL: 18,
|
||||||
|
HIGH: 14,
|
||||||
|
MEDIUM: 12,
|
||||||
|
};
|
||||||
|
|
||||||
const RISK_PRIORITY: Record<string, number> = {
|
const RISK_PRIORITY: Record<string, number> = {
|
||||||
CRITICAL: 0,
|
CRITICAL: 0,
|
||||||
HIGH: 1,
|
HIGH: 1,
|
||||||
@ -31,9 +44,10 @@ interface AnalyzedShip {
|
|||||||
/** 위험도 펄스 애니메이션 인라인 스타일 */
|
/** 위험도 펄스 애니메이션 인라인 스타일 */
|
||||||
function riskPulseStyle(riskLevel: string): React.CSSProperties {
|
function riskPulseStyle(riskLevel: string): React.CSSProperties {
|
||||||
const color = RISK_COLORS[riskLevel] ?? RISK_COLORS['LOW'];
|
const color = RISK_COLORS[riskLevel] ?? RISK_COLORS['LOW'];
|
||||||
|
const size = RISK_MARKER_SIZE[riskLevel] ?? 10;
|
||||||
return {
|
return {
|
||||||
width: 10,
|
width: size,
|
||||||
height: 10,
|
height: size,
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
boxShadow: `0 0 6px 2px ${color}88`,
|
boxShadow: `0 0 6px 2px ${color}88`,
|
||||||
@ -171,21 +185,38 @@ export function AnalysisOverlay({ ships, analysisMap, clusters, activeFilter }:
|
|||||||
{riskMarkers.map(({ ship, dto }) => {
|
{riskMarkers.map(({ ship, dto }) => {
|
||||||
const level = dto.algorithms.riskScore.level;
|
const level = dto.algorithms.riskScore.level;
|
||||||
const color = RISK_COLORS[level] ?? RISK_COLORS['LOW'];
|
const color = RISK_COLORS[level] ?? RISK_COLORS['LOW'];
|
||||||
|
const size = RISK_MARKER_SIZE[level] ?? 12;
|
||||||
|
const halfBase = Math.round(size * 0.5);
|
||||||
|
const triHeight = Math.round(size * 0.9);
|
||||||
return (
|
return (
|
||||||
<Marker key={`risk-${ship.mmsi}`} longitude={ship.lng} latitude={ship.lat} anchor="center">
|
<Marker key={`risk-${ship.mmsi}`} longitude={ship.lng} latitude={ship.lat} anchor="center">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', pointerEvents: 'none' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', pointerEvents: 'none' }}>
|
||||||
|
{/* 선박명 */}
|
||||||
|
{ship.name && (
|
||||||
|
<div style={{
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: '#fff',
|
||||||
|
textShadow: '0 0 2px #000, 0 0 2px #000',
|
||||||
|
textAlign: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
marginBottom: 2,
|
||||||
|
}}>
|
||||||
|
{ship.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 삼각형 아이콘 */}
|
{/* 삼각형 아이콘 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
borderLeft: '5px solid transparent',
|
borderLeft: `${halfBase}px solid transparent`,
|
||||||
borderRight: '5px solid transparent',
|
borderRight: `${halfBase}px solid transparent`,
|
||||||
borderBottom: `9px solid ${color}`,
|
borderBottom: `${triHeight}px solid ${color}`,
|
||||||
filter: `drop-shadow(0 0 3px ${color}88)`,
|
filter: `drop-shadow(0 0 3px ${color}88)`,
|
||||||
}} />
|
}} />
|
||||||
{/* 위험도 텍스트 */}
|
{/* 위험도 텍스트 (한글) */}
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 5,
|
fontSize: 8,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color,
|
color,
|
||||||
textShadow: '0 0 2px #000, 0 0 2px #000',
|
textShadow: '0 0 2px #000, 0 0 2px #000',
|
||||||
@ -193,7 +224,7 @@ export function AnalysisOverlay({ ships, analysisMap, clusters, activeFilter }:
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
}}>
|
}}>
|
||||||
{level}
|
{RISK_LABEL[level] ?? level}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
@ -215,6 +246,20 @@ export function AnalysisOverlay({ ships, analysisMap, clusters, activeFilter }:
|
|||||||
return (
|
return (
|
||||||
<Marker key={`dark-${ship.mmsi}`} longitude={ship.lng} latitude={ship.lat} anchor="center">
|
<Marker key={`dark-${ship.mmsi}`} longitude={ship.lng} latitude={ship.lat} anchor="center">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', pointerEvents: 'none' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', pointerEvents: 'none' }}>
|
||||||
|
{/* 선박명 */}
|
||||||
|
{ship.name && (
|
||||||
|
<div style={{
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: '#fff',
|
||||||
|
textShadow: '0 0 2px #000, 0 0 2px #000',
|
||||||
|
textAlign: 'center',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
marginBottom: 2,
|
||||||
|
}}>
|
||||||
|
{ship.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 보라 점선 원 */}
|
{/* 보라 점선 원 */}
|
||||||
<div style={{
|
<div style={{
|
||||||
width: 16,
|
width: 16,
|
||||||
@ -223,9 +268,9 @@ export function AnalysisOverlay({ ships, analysisMap, clusters, activeFilter }:
|
|||||||
border: '2px dashed #a855f7',
|
border: '2px dashed #a855f7',
|
||||||
boxShadow: '0 0 4px #a855f788',
|
boxShadow: '0 0 4px #a855f788',
|
||||||
}} />
|
}} />
|
||||||
{/* gap 라벨 */}
|
{/* gap 라벨: "AIS 소실 N분" */}
|
||||||
<div style={{
|
<div style={{
|
||||||
fontSize: 5,
|
fontSize: 8,
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: '#a855f7',
|
color: '#a855f7',
|
||||||
textShadow: '0 0 2px #000, 0 0 2px #000',
|
textShadow: '0 0 2px #000, 0 0 2px #000',
|
||||||
@ -233,7 +278,7 @@ export function AnalysisOverlay({ ships, analysisMap, clusters, activeFilter }:
|
|||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
}}>
|
}}>
|
||||||
{gapMin > 0 ? `${Math.round(gapMin)}분` : 'DARK'}
|
{gapMin > 0 ? `AIS 소실 ${Math.round(gapMin)}분` : 'DARK'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
@ -241,24 +286,43 @@ export function AnalysisOverlay({ ships, analysisMap, clusters, activeFilter }:
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* GPS 스푸핑 배지 */}
|
{/* GPS 스푸핑 배지 */}
|
||||||
{spoofingMarkers.map(({ ship }) => (
|
{spoofingMarkers.map(({ ship, dto }) => {
|
||||||
<Marker key={`spoof-${ship.mmsi}`} longitude={ship.lng} latitude={ship.lat} anchor="bottom">
|
const pct = Math.round(dto.algorithms.gpsSpoofing.spoofingScore * 100);
|
||||||
<div style={{
|
return (
|
||||||
marginBottom: 14,
|
<Marker key={`spoof-${ship.mmsi}`} longitude={ship.lng} latitude={ship.lat} anchor="bottom">
|
||||||
fontSize: 6,
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', pointerEvents: 'none' }}>
|
||||||
fontWeight: 700,
|
{/* 선박명 */}
|
||||||
color: '#fff',
|
{ship.name && (
|
||||||
backgroundColor: '#ef4444',
|
<div style={{
|
||||||
borderRadius: 2,
|
fontSize: 9,
|
||||||
padding: '0 2px',
|
fontWeight: 700,
|
||||||
textShadow: 'none',
|
color: '#fff',
|
||||||
whiteSpace: 'nowrap',
|
textShadow: '0 0 2px #000, 0 0 2px #000',
|
||||||
pointerEvents: 'none',
|
textAlign: 'center',
|
||||||
}}>
|
whiteSpace: 'nowrap',
|
||||||
GPS
|
marginBottom: 2,
|
||||||
</div>
|
}}>
|
||||||
</Marker>
|
{ship.name}
|
||||||
))}
|
</div>
|
||||||
|
)}
|
||||||
|
{/* 스푸핑 배지 */}
|
||||||
|
<div style={{
|
||||||
|
marginBottom: 14,
|
||||||
|
fontSize: 8,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: '#fff',
|
||||||
|
backgroundColor: '#ef4444',
|
||||||
|
borderRadius: 2,
|
||||||
|
padding: '0 3px',
|
||||||
|
textShadow: 'none',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{`GPS ${pct}%`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
{/* 선단 leader 별 아이콘 */}
|
{/* 선단 leader 별 아이콘 */}
|
||||||
{leaderShips.map(({ ship }) => (
|
{leaderShips.map(({ ship }) => (
|
||||||
|
|||||||
93
frontend/src/components/korea/FishingZoneLayer.tsx
Normal file
93
frontend/src/components/korea/FishingZoneLayer.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Source, Layer, Marker } from 'react-map-gl/maplibre';
|
||||||
|
import fishingZonesData from '../../data/zones/fishing-zones-wgs84.json';
|
||||||
|
|
||||||
|
const ZONE_FILL: Record<string, string> = {
|
||||||
|
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<string, string> = {
|
||||||
|
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)',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 폴리곤 중심점 (좌표 평균) */
|
||||||
|
function centroid(coordinates: number[][][][]): [number, number] {
|
||||||
|
let sLng = 0, sLat = 0, n = 0;
|
||||||
|
for (const poly of coordinates) {
|
||||||
|
for (const ring of poly) {
|
||||||
|
for (const [lng, lat] of ring) {
|
||||||
|
sLng += lng; sLat += lat; n++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n > 0 ? [sLng / n, sLat / n] : [0, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
const labels = useMemo(() =>
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
fishingZonesData.features.map((f: any) => {
|
||||||
|
const [lng, lat] = centroid(f.geometry.coordinates);
|
||||||
|
return { id: f.properties.id as string, name: f.properties.name as string, lng, lat };
|
||||||
|
}), []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Source id="fishing-zones" type="geojson" data={fishingZonesData as GeoJSON.FeatureCollection}>
|
||||||
|
<Layer
|
||||||
|
id="fishing-zone-fill"
|
||||||
|
type="fill"
|
||||||
|
paint={{ 'fill-color': fillColor, 'fill-opacity': 1 }}
|
||||||
|
/>
|
||||||
|
<Layer
|
||||||
|
id="fishing-zone-line"
|
||||||
|
type="line"
|
||||||
|
paint={{
|
||||||
|
'line-color': lineColor,
|
||||||
|
'line-opacity': 1,
|
||||||
|
'line-width': 1.5,
|
||||||
|
'line-dasharray': [4, 2],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Source>
|
||||||
|
|
||||||
|
{labels.map(({ id, name, lng, lat }) => (
|
||||||
|
<Marker key={`zone-${id}`} longitude={lng} latitude={lat} anchor="center">
|
||||||
|
<div style={{
|
||||||
|
fontSize: 10, fontWeight: 700, color: '#fff',
|
||||||
|
textShadow: '0 0 3px #000, 0 0 3px #000',
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.45)',
|
||||||
|
borderRadius: 3, padding: '1px 5px',
|
||||||
|
whiteSpace: 'nowrap', pointerEvents: 'none',
|
||||||
|
}}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ import { NKLaunchLayer } from './NKLaunchLayer';
|
|||||||
import { NKMissileEventLayer } from './NKMissileEventLayer';
|
import { NKMissileEventLayer } from './NKMissileEventLayer';
|
||||||
import { ChineseFishingOverlay } from './ChineseFishingOverlay';
|
import { ChineseFishingOverlay } from './ChineseFishingOverlay';
|
||||||
import { AnalysisOverlay } from './AnalysisOverlay';
|
import { AnalysisOverlay } from './AnalysisOverlay';
|
||||||
|
import { FishingZoneLayer } from './FishingZoneLayer';
|
||||||
import { AnalysisStatsPanel } from './AnalysisStatsPanel';
|
import { AnalysisStatsPanel } from './AnalysisStatsPanel';
|
||||||
import { fetchKoreaInfra } from '../../services/infra';
|
import { fetchKoreaInfra } from '../../services/infra';
|
||||||
import type { PowerFacility } from '../../services/infra';
|
import type { PowerFacility } from '../../services/infra';
|
||||||
@ -272,6 +273,7 @@ export function KoreaMap({ ships, aircraft, satellites, layers, osintFeed, curre
|
|||||||
{layers.govBuildings && <GovBuildingLayer />}
|
{layers.govBuildings && <GovBuildingLayer />}
|
||||||
{layers.nkLaunch && <NKLaunchLayer />}
|
{layers.nkLaunch && <NKLaunchLayer />}
|
||||||
{layers.nkMissile && <NKMissileEventLayer ships={ships} />}
|
{layers.nkMissile && <NKMissileEventLayer ships={ships} />}
|
||||||
|
{koreaFilters.illegalFishing && <FishingZoneLayer />}
|
||||||
{layers.cnFishing && <ChineseFishingOverlay ships={ships} analysisMap={vesselAnalysis?.analysisMap} />}
|
{layers.cnFishing && <ChineseFishingOverlay ships={ships} analysisMap={vesselAnalysis?.analysisMap} />}
|
||||||
{vesselAnalysis && vesselAnalysis.analysisMap.size > 0 && (
|
{vesselAnalysis && vesselAnalysis.analysisMap.size > 0 && (
|
||||||
<AnalysisOverlay
|
<AnalysisOverlay
|
||||||
|
|||||||
1
frontend/src/data/zones/fishing-zones-wgs84.json
Normal file
1
frontend/src/data/zones/fishing-zones-wgs84.json
Normal file
File diff suppressed because one or more lines are too long
@ -3,13 +3,10 @@
|
|||||||
|
|
||||||
import type { Ship } from '../types';
|
import type { Ship } from '../types';
|
||||||
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
|
import booleanPointInPolygon from '@turf/boolean-point-in-polygon';
|
||||||
import { point, multiPolygon } from '@turf/helpers';
|
import { point } from '@turf/helpers';
|
||||||
import type { Feature, MultiPolygon } from 'geojson';
|
import type { Feature, MultiPolygon } from 'geojson';
|
||||||
|
|
||||||
import zone1Data from '../data/zones/특정어업수역Ⅰ.json';
|
import fishingZonesWgs84 from '../data/zones/fishing-zones-wgs84.json';
|
||||||
import zone2Data from '../data/zones/특정어업수역Ⅱ.json';
|
|
||||||
import zone3Data from '../data/zones/특정어업수역Ⅲ.json';
|
|
||||||
import zone4Data from '../data/zones/특정어업수역Ⅳ.json';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 중국 허가 업종 코드 (허가번호 접두사)
|
* 중국 허가 업종 코드 (허가번호 접두사)
|
||||||
@ -50,28 +47,6 @@ const GEAR_META: Record<FishingGearType, {
|
|||||||
|
|
||||||
export { GEAR_META as GEAR_LABELS };
|
export { GEAR_META as GEAR_LABELS };
|
||||||
|
|
||||||
/**
|
|
||||||
* EPSG:3857 → WGS84 좌표 변환
|
|
||||||
*/
|
|
||||||
function epsg3857ToWgs84(x: number, y: number): [number, number] {
|
|
||||||
const lon = (x / (Math.PI * 6378137)) * 180;
|
|
||||||
const lat = Math.atan(Math.exp(y / 6378137)) * (360 / Math.PI) - 90;
|
|
||||||
return [lon, lat]; // GeoJSON [lng, lat] 순서
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
function convertZoneToWgs84(data: any): Feature<MultiPolygon> {
|
|
||||||
const feat = data.features[0];
|
|
||||||
const multiCoords: number[][][][] = feat.geometry.coordinates.map(
|
|
||||||
(poly: number[][][]) => poly.map(
|
|
||||||
(ring: number[][]) => ring.map(([x, y]: number[]) => epsg3857ToWgs84(x, y)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return multiPolygon(multiCoords).geometry
|
|
||||||
? { type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates: multiCoords } }
|
|
||||||
: multiPolygon(multiCoords) as unknown as Feature<MultiPolygon>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FishingZoneId = 'ZONE_I' | 'ZONE_II' | 'ZONE_III' | 'ZONE_IV' | 'OUTSIDE';
|
export type FishingZoneId = 'ZONE_I' | 'ZONE_II' | 'ZONE_III' | 'ZONE_IV' | 'OUTSIDE';
|
||||||
|
|
||||||
export interface FishingZoneInfo {
|
export interface FishingZoneInfo {
|
||||||
@ -80,15 +55,23 @@ export interface FishingZoneInfo {
|
|||||||
allowed: string[];
|
allowed: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 수역별 허가 업종 */
|
||||||
|
const ZONE_ALLOWED: Record<string, string[]> = {
|
||||||
|
ZONE_I: ['PS', 'FC'],
|
||||||
|
ZONE_II: ['PT', 'OT', 'GN', 'PS', 'FC'],
|
||||||
|
ZONE_III: ['PT', 'OT', 'GN', 'PS', 'FC'],
|
||||||
|
ZONE_IV: ['GN', 'PS', 'FC'],
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정어업수역 Ⅰ~Ⅳ 폴리곤 (WGS84 변환 캐시)
|
* 특정어업수역 Ⅰ~Ⅳ 폴리곤 (사전 변환된 WGS84 GeoJSON)
|
||||||
*/
|
*/
|
||||||
const ZONE_POLYGONS: { id: FishingZoneId; name: string; allowed: string[]; geojson: Feature<MultiPolygon> }[] = [
|
export const ZONE_POLYGONS = fishingZonesWgs84.features.map(f => ({
|
||||||
{ id: 'ZONE_I', name: '수역Ⅰ(동해)', allowed: ['PS', 'FC'], geojson: convertZoneToWgs84(zone1Data) },
|
id: f.properties.id as FishingZoneId,
|
||||||
{ id: 'ZONE_II', name: '수역Ⅱ(남해)', allowed: ['PT', 'OT', 'GN', 'PS', 'FC'], geojson: convertZoneToWgs84(zone2Data) },
|
name: f.properties.name,
|
||||||
{ id: 'ZONE_III', name: '수역Ⅲ(서남해)', allowed: ['PT', 'OT', 'GN', 'PS', 'FC'], geojson: convertZoneToWgs84(zone3Data) },
|
allowed: ZONE_ALLOWED[f.properties.id] ?? [],
|
||||||
{ id: 'ZONE_IV', name: '수역Ⅳ(서해)', allowed: ['GN', 'PS', 'FC'], geojson: convertZoneToWgs84(zone4Data) },
|
geojson: f as unknown as Feature<MultiPolygon>,
|
||||||
];
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 특정어업수역 폴리곤 기반 수역 분류
|
* 특정어업수역 폴리곤 기반 수역 분류
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user