From f4f0cb274f09b67dc6b04c452d76439059044110 Mon Sep 17 00:00:00 2001 From: HeungTak Lee Date: Fri, 30 Jan 2026 13:01:54 +0900 Subject: [PATCH] =?UTF-8?q?dark=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20=ED=98=84=EC=9E=AC=20=EC=83=81=ED=83=9C=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=EC=83=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Vite 마이그레이션, OpenLayers+Deck.gl 지도 연동 - STOMP WebSocket 선박 실시간 데이터 수신 - 선박 범례/필터/카운트, 다크시그널 처리 - Ctrl+Drag 박스선택, 우클릭 컨텍스트 메뉴 - 측정도구, 상세모달, 호버 툴팁 - darkSignalIds Set 패턴, INSHORE/OFFSHORE 타임아웃 Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 225 ++++ index.html | 15 + package.json | 61 +- public/favicon.ico | Bin 0 -> 1150 bytes src/App.jsx | 28 + src/api/signalApi.js | 51 + src/assets/data/largeTrench.json | 1 + src/assets/data/shiptype.js | 182 +++ src/assets/img/default-ship.png | Bin 0 -> 1293 bytes src/assets/img/icon/atlas.png | Bin 0 -> 10207 bytes .../img/shipDetail/detailKindIcon/cargo.svg | 1 + .../img/shipDetail/detailKindIcon/etc.svg | 1 + .../img/shipDetail/detailKindIcon/fishing.svg | 1 + .../img/shipDetail/detailKindIcon/gov.svg | 1 + .../img/shipDetail/detailKindIcon/kcgv.svg | 1 + .../shipDetail/detailKindIcon/passenger.svg | 1 + .../img/shipDetail/detailKindIcon/tanker.svg | 1 + src/assets/img/shipKindIcons/bouy.svg | 1 + src/assets/img/shipKindIcons/cargo.svg | 1 + src/assets/img/shipKindIcons/etc.svg | 1 + src/assets/img/shipKindIcons/fishing.svg | 1 + src/assets/img/shipKindIcons/gov.svg | 1 + src/assets/img/shipKindIcons/hazard.svg | 1 + src/assets/img/shipKindIcons/kcgv.svg | 1 + src/assets/img/shipKindIcons/pass.svg | 1 + ...slht87IdeaProjectsdarksrcassetsimgshipKindIcons | 1 + ...slht87IdeaProjectsdarksrcassetsimgshipKindIcons | 1 + ...slht87IdeaProjectsdarksrcassetsimgshipKindIcons | 1 + ...slht87IdeaProjectsdarksrcassetsimgshipKindIcons | 1 + src/common/stompClient.js | 237 ++++ src/component/wrap/ToolComponent.jsx | 19 +- src/component/wrap/side/DisplayComponent.jsx | 306 +++-- src/components/layout/Header.jsx | 42 + src/components/layout/MainLayout.jsx | 24 + src/components/layout/SideNav.jsx | 76 ++ src/components/layout/Sidebar.jsx | 86 ++ src/components/layout/ToolBar.jsx | 201 ++++ src/components/ship/ShipContextMenu.jsx | 82 ++ src/components/ship/ShipContextMenu.scss | 34 + src/components/ship/ShipDetailModal.jsx | 325 ++++++ src/components/ship/ShipDetailModal.scss | 203 ++++ src/components/ship/ShipLegend.jsx | 139 +++ src/components/ship/ShipLegend.scss | 179 +++ src/components/ship/ShipTooltip.jsx | 40 + src/components/ship/ShipTooltip.scss | 46 + src/hooks/useShipData.js | 231 ++++ src/hooks/useShipLayer.js | 273 +++++ src/main.jsx | 16 + src/map/MapContainer.jsx | 296 +++++ src/map/MapContainer.scss | 100 ++ src/map/ShipBatchRenderer.js | 618 ++++++++++ src/map/layers/baseLayer.js | 70 ++ src/map/layers/shipLayer.js | 1031 +++++++++++++++++ src/map/measure/measure.js | 392 +++++++ src/map/measure/measure.scss | 41 + src/map/measure/useMeasure.js | 72 ++ src/pages/HomePage.jsx | 13 + src/publish/PublishRoutes.jsx | 134 +++ src/publish/components/FileUpload.jsx | 35 + src/publish/components/Slider.jsx | 24 + src/publish/layouts/HeaderComponent.jsx | 36 + src/publish/layouts/MainComponent.jsx | 11 + src/publish/layouts/PublishLayout.jsx | 57 + src/publish/layouts/SideComponent.jsx | 94 ++ src/publish/layouts/ToolComponent.jsx | 131 +++ src/publish/layouts/WrapComponent.jsx | 15 + src/publish/pages/Analysis1Component.jsx | 48 + src/publish/pages/Analysis2Component.jsx | 129 +++ src/publish/pages/Analysis3Component.jsx | 198 ++++ src/publish/pages/Analysis4Component.jsx | 235 ++++ src/publish/pages/DisplayComponent.jsx | 355 ++++++ src/publish/pages/EmptyMain.jsx | 4 + src/publish/pages/LayerComponent.jsx | 91 ++ src/publish/pages/MyPageComponent.jsx | 208 ++++ src/publish/pages/NavComponent.jsx | 71 ++ src/publish/pages/Panel1Component.jsx | 727 ++++++++++++ src/publish/pages/Panel1DetailComponent.jsx | 112 ++ src/publish/pages/Panel2Component.jsx | 420 +++++++ src/publish/pages/Panel3Component.jsx | 322 +++++ src/publish/pages/Panel4Component.jsx | 521 +++++++++ src/publish/pages/Panel5Component.jsx | 7 + src/publish/pages/Panel6Component.jsx | 77 ++ src/publish/pages/Panel7Component.jsx | 7 + src/publish/pages/Panel8Component.jsx | 7 + src/publish/pages/ReplayComponent.jsx | 100 ++ src/publish/pages/Satellite1Component.jsx | 189 +++ src/publish/pages/Satellite2Component.jsx | 87 ++ src/publish/pages/Satellite3Component.jsx | 94 ++ src/publish/pages/Satellite4Component.jsx | 50 + src/publish/pages/ShipComponent.jsx | 169 +++ src/publish/pages/Signal1Component.jsx | 58 + src/publish/pages/Signal2Component.jsx | 149 +++ src/publish/pages/ToastComponent.jsx | 59 + src/publish/pages/TopComponent.jsx | 21 + src/publish/pages/TrackComponent.jsx | 41 + src/publish/pages/WeatherComponent.jsx | 71 ++ src/publish/scss/HeaderComponent.scss | 109 ++ src/publish/scss/Layout.scss | 557 +++++++++ src/publish/scss/MainComponent.scss | 188 +++ src/publish/scss/SideComponent.scss | 539 +++++++++ src/publish/scss/ToolComponent.scss | 153 +++ src/publish/scss/WrapComponent.scss | 7 + src/scss/SideComponent.scss | 17 + src/scss/ToolComponent.scss | 59 + src/scss/global.scss | 135 +++ src/stores/mapStore.js | 61 + src/stores/shipStore.js | 997 ++++++++++++++++ src/stores/uiStore.js | 28 + src/types/constants.js | 300 +++++ src/utils/csvDownload.js | 132 +++ vite.config.js | 60 + 111 files changed, 14150 insertions(+), 130 deletions(-) create mode 100644 CLAUDE.md create mode 100644 index.html create mode 100644 public/favicon.ico create mode 100644 src/App.jsx create mode 100644 src/api/signalApi.js create mode 100644 src/assets/data/largeTrench.json create mode 100644 src/assets/data/shiptype.js create mode 100644 src/assets/img/default-ship.png create mode 100644 src/assets/img/icon/atlas.png create mode 100644 src/assets/img/shipDetail/detailKindIcon/cargo.svg create mode 100644 src/assets/img/shipDetail/detailKindIcon/etc.svg create mode 100644 src/assets/img/shipDetail/detailKindIcon/fishing.svg create mode 100644 src/assets/img/shipDetail/detailKindIcon/gov.svg create mode 100644 src/assets/img/shipDetail/detailKindIcon/kcgv.svg create mode 100644 src/assets/img/shipDetail/detailKindIcon/passenger.svg create mode 100644 src/assets/img/shipDetail/detailKindIcon/tanker.svg create mode 100644 src/assets/img/shipKindIcons/bouy.svg create mode 100644 src/assets/img/shipKindIcons/cargo.svg create mode 100644 src/assets/img/shipKindIcons/etc.svg create mode 100644 src/assets/img/shipKindIcons/fishing.svg create mode 100644 src/assets/img/shipKindIcons/gov.svg create mode 100644 src/assets/img/shipKindIcons/hazard.svg create mode 100644 src/assets/img/shipKindIcons/kcgv.svg create mode 100644 src/assets/img/shipKindIcons/pass.svg create mode 100644 src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconsetc.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons create mode 100644 src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconshazard.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons create mode 100644 src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconskcgv.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons create mode 100644 src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconspass.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons create mode 100644 src/common/stompClient.js create mode 100644 src/components/layout/Header.jsx create mode 100644 src/components/layout/MainLayout.jsx create mode 100644 src/components/layout/SideNav.jsx create mode 100644 src/components/layout/Sidebar.jsx create mode 100644 src/components/layout/ToolBar.jsx create mode 100644 src/components/ship/ShipContextMenu.jsx create mode 100644 src/components/ship/ShipContextMenu.scss create mode 100644 src/components/ship/ShipDetailModal.jsx create mode 100644 src/components/ship/ShipDetailModal.scss create mode 100644 src/components/ship/ShipLegend.jsx create mode 100644 src/components/ship/ShipLegend.scss create mode 100644 src/components/ship/ShipTooltip.jsx create mode 100644 src/components/ship/ShipTooltip.scss create mode 100644 src/hooks/useShipData.js create mode 100644 src/hooks/useShipLayer.js create mode 100644 src/main.jsx create mode 100644 src/map/MapContainer.jsx create mode 100644 src/map/MapContainer.scss create mode 100644 src/map/ShipBatchRenderer.js create mode 100644 src/map/layers/baseLayer.js create mode 100644 src/map/layers/shipLayer.js create mode 100644 src/map/measure/measure.js create mode 100644 src/map/measure/measure.scss create mode 100644 src/map/measure/useMeasure.js create mode 100644 src/pages/HomePage.jsx create mode 100644 src/publish/PublishRoutes.jsx create mode 100644 src/publish/components/FileUpload.jsx create mode 100644 src/publish/components/Slider.jsx create mode 100644 src/publish/layouts/HeaderComponent.jsx create mode 100644 src/publish/layouts/MainComponent.jsx create mode 100644 src/publish/layouts/PublishLayout.jsx create mode 100644 src/publish/layouts/SideComponent.jsx create mode 100644 src/publish/layouts/ToolComponent.jsx create mode 100644 src/publish/layouts/WrapComponent.jsx create mode 100644 src/publish/pages/Analysis1Component.jsx create mode 100644 src/publish/pages/Analysis2Component.jsx create mode 100644 src/publish/pages/Analysis3Component.jsx create mode 100644 src/publish/pages/Analysis4Component.jsx create mode 100644 src/publish/pages/DisplayComponent.jsx create mode 100644 src/publish/pages/EmptyMain.jsx create mode 100644 src/publish/pages/LayerComponent.jsx create mode 100644 src/publish/pages/MyPageComponent.jsx create mode 100644 src/publish/pages/NavComponent.jsx create mode 100644 src/publish/pages/Panel1Component.jsx create mode 100644 src/publish/pages/Panel1DetailComponent.jsx create mode 100644 src/publish/pages/Panel2Component.jsx create mode 100644 src/publish/pages/Panel3Component.jsx create mode 100644 src/publish/pages/Panel4Component.jsx create mode 100644 src/publish/pages/Panel5Component.jsx create mode 100644 src/publish/pages/Panel6Component.jsx create mode 100644 src/publish/pages/Panel7Component.jsx create mode 100644 src/publish/pages/Panel8Component.jsx create mode 100644 src/publish/pages/ReplayComponent.jsx create mode 100644 src/publish/pages/Satellite1Component.jsx create mode 100644 src/publish/pages/Satellite2Component.jsx create mode 100644 src/publish/pages/Satellite3Component.jsx create mode 100644 src/publish/pages/Satellite4Component.jsx create mode 100644 src/publish/pages/ShipComponent.jsx create mode 100644 src/publish/pages/Signal1Component.jsx create mode 100644 src/publish/pages/Signal2Component.jsx create mode 100644 src/publish/pages/ToastComponent.jsx create mode 100644 src/publish/pages/TopComponent.jsx create mode 100644 src/publish/pages/TrackComponent.jsx create mode 100644 src/publish/pages/WeatherComponent.jsx create mode 100644 src/publish/scss/HeaderComponent.scss create mode 100644 src/publish/scss/Layout.scss create mode 100644 src/publish/scss/MainComponent.scss create mode 100644 src/publish/scss/SideComponent.scss create mode 100644 src/publish/scss/ToolComponent.scss create mode 100644 src/publish/scss/WrapComponent.scss create mode 100644 src/scss/global.scss create mode 100644 src/stores/mapStore.js create mode 100644 src/stores/shipStore.js create mode 100644 src/stores/uiStore.js create mode 100644 src/types/constants.js create mode 100644 src/utils/csvDownload.js create mode 100644 vite.config.js diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..31dc317a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,225 @@ +# CLAUDE.md - GIS 함정용 프로젝트 가이드 + +## 프로젝트 개요 + +| 항목 | 내용 | +|------|------| +| 프로젝트명 | dark (GIS 함정용) | +| 참조 프로젝트 | mda-react-front (메인 프로젝트) | +| 목적 | 선박위치정보 전시 및 조회 기능 프론트엔드 | +| 현재 단계 | 퍼블리싱 → 구현 전환 | + +--- + +## 메인 프로젝트(mda-react-front) 기술 스택 + +| 항목 | 기술 | +|------|------| +| 지도 엔진 | OpenLayers 9.2.4 | +| 실시간 렌더링 | Deck.gl 9.0.38 (GPU) | +| UI 프레임워크 | React 18.2.0 | +| 상태 관리 | Zustand 4.5.2 | +| 라우팅 | React Router 6.15.0 | +| 데이터 페칭 | React Query 4.32.6 | +| 실시간 통신 | STOMP WebSocket | +| 번들러 | Vite 5.2.10 | +| 타입 | TypeScript 5.0.2 | + +--- + +## 현재 프로젝트(dark) 기술 스택 + +| 항목 | 기술 | 변경 계획 | +|------|------|----------| +| 번들러 | CRA (react-scripts) | Vite 마이그레이션 검토 | +| 언어 | JavaScript | TypeScript 도입 검토 | +| 라우팅 | React Router 6.30.3 | 유지 | +| 상태관리 | React useState | Zustand 도입 | +| 스타일 | SCSS | 유지 | +| 지도 | 미연동 | OpenLayers + Deck.gl | + +--- + +## 프로젝트 구조 설계 + +``` +src/ +├── index.js # 앱 엔트리 포인트 +├── App.jsx # 라우트 정의 +│ +├── publish/ # [퍼블리싱 영역] - 기존 퍼블리시 파일 +│ ├── layouts/ # 퍼블리시 레이아웃 +│ ├── pages/ # 퍼블리시 페이지 (Panel1~8 등) +│ └── components/ # 퍼블리시 컴포넌트 +│ +├── pages/ # [구현 영역] - 실제 페이지 +│ ├── Home.jsx # 메인 페이지 +│ ├── ship/ # 선박 관련 페이지 +│ ├── satellite/ # 위성 관련 페이지 +│ ├── weather/ # 기상 관련 페이지 +│ └── analysis/ # 분석 관련 페이지 +│ +├── components/ # [공통 컴포넌트] +│ ├── common/ # 기본 UI 컴포넌트 +│ │ ├── Button/ +│ │ ├── Input/ +│ │ ├── Modal/ +│ │ └── ... +│ ├── domain/ # 도메인별 컴포넌트 +│ │ ├── ship/ # 선박 관련 +│ │ ├── map/ # 지도 관련 +│ │ └── ... +│ └── layout/ # 레이아웃 컴포넌트 +│ ├── Header/ +│ ├── Sidebar/ +│ └── ... +│ +├── map/ # [지도 모듈] +│ ├── MapContext.jsx # OpenLayers 맵 Context +│ ├── MapProvider.jsx # 맵 Provider +│ ├── layers/ # 레이어 정의 +│ │ ├── baseLayer.js # 베이스맵 +│ │ ├── shipLayer.js # 선박 레이어 +│ │ └── ... +│ ├── controls/ # 지도 컨트롤 +│ └── utils/ # 지도 유틸리티 +│ +├── stores/ # [Zustand 스토어] +│ ├── mapStore.js # 지도 상태 +│ ├── shipStore.js # 선박 상태 +│ ├── filterStore.js # 필터 상태 +│ └── uiStore.js # UI 상태 +│ +├── api/ # [API 레이어] +│ ├── client.js # API 클라이언트 +│ ├── ship.js # 선박 API +│ └── ... +│ +├── hooks/ # [커스텀 훅] +│ ├── useMap.js +│ ├── useShip.js +│ └── ... +│ +├── utils/ # [유틸리티] +│ ├── format.js +│ ├── coordinate.js +│ └── ... +│ +├── types/ # [타입 정의] (TS 도입 시) +│ └── ... +│ +├── assets/ # [정적 자원] +│ ├── images/ +│ └── ... +│ +└── scss/ # [스타일] + ├── base/ + ├── components/ + └── pages/ +``` + +--- + +## 라우팅 구조 + +``` +/ → 메인 (지도) +├── /ship → 선박 조회 +├── /satellite → 위성 +├── /weather → 기상 +├── /analysis → 분석 +├── /settings → 설정 +└── /mypage → 마이페이지 + +/publish → 퍼블리싱 미리보기 +├── /publish/panel1 → 선박 패널 +├── /publish/panel2 → 위성 패널 +├── /publish/panel3 → 기상 패널 +├── /publish/panel4 → 분석 패널 +├── /publish/panel5 → 타임라인 +├── /publish/panel6 → AI모드 +├── /publish/panel7 → 리플레이 +├── /publish/panel8 → 항적조회 +└── /publish/... → 기타 퍼블리시 페이지 +``` + +--- + +## 기능 구현 로드맵 + +### Phase 1: 기반 구축 +- [ ] 프로젝트 구조 재편 (퍼블리시/구현 분리) +- [ ] OpenLayers 연동 (베이스맵) +- [ ] Zustand 스토어 설정 +- [ ] 기본 레이아웃 구현 + +### Phase 2: 지도 핵심 기능 +- [ ] 지도 컨트롤 (줌, 패닝) +- [ ] 레이어 관리 (토글, 순서) +- [ ] 좌표 표시 +- [ ] 축척 표시 + +### Phase 3: 선박 표시 +- [ ] Deck.gl 연동 +- [ ] 선박 아이콘 렌더링 +- [ ] 선박 클릭/호버 이벤트 +- [ ] 선박 정보 팝업 + +### Phase 4: 데이터 연동 +- [ ] API 클라이언트 설정 +- [ ] 선박 데이터 조회 +- [ ] 필터링 기능 + +### Phase 5: 추가 기능 +- [ ] 항적 조회 +- [ ] 기상 레이어 +- [ ] 관심영역 설정 +- [ ] 위성 영상 + +--- + +## 메인 프로젝트 참조 파일 + +### 지도 초기화 +- `mda-react-front/src/map/MainMapContext.tsx` - OpenLayers 맵 초기화 + +### 레이어 설정 +- `mda-react-front/src/common/mapLayer.ts` - 베이스맵 타일 +- `mda-react-front/src/common/targetLayer.ts` - 선박 레이어 +- `mda-react-front/src/common/deck.ts` - Deck.gl 설정 + +### 상태 관리 +- `mda-react-front/src/shared/model/` - Zustand 스토어 + +### 스타일 +- `mda-react-front/src/map/control.css` - 지도 컨트롤 스타일 + +--- + +## 개발 명령어 + +```bash +# 개발 서버 실행 +npm start + +# 프로덕션 빌드 +npm run build + +# 테스트 +npm test +``` + +--- + +## 세션 관리 + +- 세션 핸드오버: `.claude/SESSION_HANDOVER.md` +- 자동 요약: 컨텍스트 95% 도달 시 자동 저장 + +--- + +## 변경 이력 + +| 날짜 | 내용 | +|------|------| +| 2026-01-26 | 초기 프로젝트 분석 및 워크플로우 정의 | diff --git a/index.html b/index.html new file mode 100644 index 00000000..75056d77 --- /dev/null +++ b/index.html @@ -0,0 +1,15 @@ + + + + + + GIS 함정용 + + + + + +
+ + + diff --git a/package.json b/package.json index fd493b2c..21a50d67 100644 --- a/package.json +++ b/package.json @@ -1,41 +1,36 @@ { "name": "dark", - "version": "0.1.0", "private": true, - "dependencies": { - "@testing-library/dom": "^10.4.1", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/react": "^16.3.1", - "@testing-library/user-event": "^13.5.0", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-router-dom": "^6.30.3", - "react-scripts": "5.0.1", - "sass": "^1.97.2", - "web-vitals": "^2.1.4" - }, + "version": "0.1.0", + "type": "module", "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" + "dev": "vite --port 3000", + "build": "vite build", + "build:dev": "vite build --mode dev", + "build:prod": "vite build --mode prod", + "preview": "vite preview --port 3000", + "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0" }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] + "dependencies": { + "@deck.gl/core": "^9.2.6", + "@deck.gl/layers": "^9.2.6", + "@stomp/stompjs": "^7.2.1", + "axios": "^1.4.0", + "dayjs": "^1.11.11", + "ol": "^9.2.4", + "ol-ext": "^4.0.10", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.30.3", + "zustand": "^4.5.2" }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "devDependencies": { + "@vitejs/plugin-react": "^4.0.1", + "eslint": "^8.44.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.1", + "sass": "^1.77.8", + "vite": "^5.2.10" } } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fdc9e1c18cd65145e21e322680ccdd489bc8254c GIT binary patch literal 1150 zcmZ{kZA?>F7{{MR%3M@HI;xn#T0|jLs3aKeq~J^Cwb%w*Zd=RkwOpWLKtrbi88X7q z5a+vy!K6~fnPo0>Q!!cMGGoM-7>x-ZmZ^y*hHsiBOFBL*{(X*BBSY`ax%b@X{(t8@ z_niN8i8Oqsq!3>X6iOh{6HzWV4sj$N=Nl2fOPv*Oub=r6FleVW*_c2LbBTNfSvIfP zXj8c#;Mj+>|Bne)O(T-C^d{%(irB`rRWaG9H`RY8#+W|W`@GEWRZiZsG&i{3(}aS~ zeJI?00G0k`c=K|D^*ruJ$1o<6sv~k2=2z4dtwG`78R#!PM*78vSbkyz(i$7w%k#^H zCcxVF=M{bBa}Cwy=(H|#-lWy&Vy8Z%7ewjc}a-)j~ zWy7N=9z2h&+;>$wvFz6gq_C#sNems#D9rk1k*@8h8<1jv_4Xr#?5of-lMr4y2YZ|r z^pu)WZ=8*AGC?XWf@SPaxc>#VGb7lY`-iUf0Z@j|jH*TE`-d|L*M|4u&V^w_w>S_9 zIq}=60~kHok2XE$k+=B18XXz!WoE~BLsw7yeh)p{L%lBko*IXJ{J3ug?i}sF5A6XA z1G1a R>&;={|A&$o*9k_juK>~8c1{2Q literal 0 HcmV?d00001 diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 00000000..a722a4b3 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,28 @@ +import { Routes, Route, Navigate } from 'react-router-dom'; + +// 구현 영역 - 레이아웃 +import MainLayout from './components/layout/MainLayout'; + +// 퍼블리시 영역 +import PublishLayout from './publish/layouts/PublishLayout'; +import PublishRoutes from './publish/PublishRoutes'; + +export default function App() { + return ( + + {/* ===================== + 구현 영역 (메인) + - 모든 메뉴 경로를 MainLayout으로 처리 + ===================== */} + } /> + + {/* ===================== + 퍼블리시 영역 + /publish/* 로 접근하여 퍼블리시 결과물 미리보기 + ===================== */} + }> + {PublishRoutes} + + + ); +} diff --git a/src/api/signalApi.js b/src/api/signalApi.js new file mode 100644 index 00000000..dead6053 --- /dev/null +++ b/src/api/signalApi.js @@ -0,0 +1,51 @@ +/** + * 선박 신호 API + * 참조: mda-react-front/src/api/query.ts - getAllSignals() + */ + +import { parsePipeMessage, rowToShipObject } from '../common/stompClient'; + +/** + * 12분 이내 전체 선박 신호 조회 + * STOMP 구독 전에 호출하여 초기 선박 데이터 로드 + * + * @returns {Promise} 선박 데이터 배열 + */ +export async function fetchAllSignals() { + try { + const response = await fetch('/signal-api/all/12'); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + // API 응답 구조: { data: [...] } 또는 직접 배열 + const rawData = result?.data || result || []; + + if (!Array.isArray(rawData)) { + console.warn('[fetchAllSignals] Invalid response format:', result); + return []; + } + + // 각 행을 선박 객체로 변환 + const ships = rawData.map((row) => { + // row가 문자열이면 파이프로 파싱, 배열이면 그대로 사용 + const parsed = typeof row === 'string' ? parsePipeMessage(row) : row; + return rowToShipObject(parsed); + }); + + // 좌표가 있는 선박만 필터링 + const validShips = ships.filter(s => s.longitude && s.latitude); + + console.log(`[fetchAllSignals] Loaded ${validShips.length}/${ships.length} ships (with coords)`); + + return validShips; + } catch (error) { + console.error('[fetchAllSignals] Error:', error); + return []; + } +} + +export default fetchAllSignals; diff --git a/src/assets/data/largeTrench.json b/src/assets/data/largeTrench.json new file mode 100644 index 00000000..75352eb2 --- /dev/null +++ b/src/assets/data/largeTrench.json @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"zone_name":"1"},"geometry":{"type":"Polygon","coordinates":[[[131.5,42.5],[132,42.5],[132,43],[131.5,43],[131.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"2"},"geometry":{"type":"Polygon","coordinates":[[[130,42],[130.5,42],[130.5,42.5],[130,42.5],[130,42]]]}},{"type":"Feature","properties":{"zone_name":"3"},"geometry":{"type":"Polygon","coordinates":[[[130.5,42],[131,42],[131,42.5],[130.5,42.5],[130.5,42]]]}},{"type":"Feature","properties":{"zone_name":"4"},"geometry":{"type":"Polygon","coordinates":[[[131,42],[131.5,42],[131.5,42.5],[131,42.5],[131,42]]]}},{"type":"Feature","properties":{"zone_name":"5"},"geometry":{"type":"Polygon","coordinates":[[[131.5,42],[132,42],[132,42.5],[131.5,42.5],[131.5,42]]]}},{"type":"Feature","properties":{"zone_name":"6"},"geometry":{"type":"Polygon","coordinates":[[[129.5,41.5],[130,41.5],[130,42],[129.5,42],[129.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"7"},"geometry":{"type":"Polygon","coordinates":[[[130,41.5],[130.5,41.5],[130.5,42],[130,42],[130,41.5]]]}},{"type":"Feature","properties":{"zone_name":"8"},"geometry":{"type":"Polygon","coordinates":[[[130.5,41.5],[131,41.5],[131,42],[130.5,42],[130.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"9"},"geometry":{"type":"Polygon","coordinates":[[[131,41.5],[131.5,41.5],[131.5,42],[131,42],[131,41.5]]]}},{"type":"Feature","properties":{"zone_name":"10"},"geometry":{"type":"Polygon","coordinates":[[[131.5,41.5],[132,41.5],[132,42],[131.5,42],[131.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"11"},"geometry":{"type":"Polygon","coordinates":[[[129.5,41],[130,41],[130,41.5],[129.5,41.5],[129.5,41]]]}},{"type":"Feature","properties":{"zone_name":"12"},"geometry":{"type":"Polygon","coordinates":[[[130,41],[130.5,41],[130.5,41.5],[130,41.5],[130,41]]]}},{"type":"Feature","properties":{"zone_name":"13"},"geometry":{"type":"Polygon","coordinates":[[[130.5,41],[131,41],[131,41.5],[130.5,41.5],[130.5,41]]]}},{"type":"Feature","properties":{"zone_name":"14"},"geometry":{"type":"Polygon","coordinates":[[[131,41],[131.5,41],[131.5,41.5],[131,41.5],[131,41]]]}},{"type":"Feature","properties":{"zone_name":"15"},"geometry":{"type":"Polygon","coordinates":[[[131.5,41],[132,41],[132,41.5],[131.5,41.5],[131.5,41]]]}},{"type":"Feature","properties":{"zone_name":"16"},"geometry":{"type":"Polygon","coordinates":[[[129,40.5],[129.5,40.5],[129.5,41],[129,41],[129,40.5]]]}},{"type":"Feature","properties":{"zone_name":"17"},"geometry":{"type":"Polygon","coordinates":[[[129.5,40.5],[130,40.5],[130,41],[129.5,41],[129.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"18"},"geometry":{"type":"Polygon","coordinates":[[[130,40.5],[130.5,40.5],[130.5,41],[130,41],[130,40.5]]]}},{"type":"Feature","properties":{"zone_name":"19"},"geometry":{"type":"Polygon","coordinates":[[[130.5,40.5],[131,40.5],[131,41],[130.5,41],[130.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"20"},"geometry":{"type":"Polygon","coordinates":[[[131,40.5],[131.5,40.5],[131.5,41],[131,41],[131,40.5]]]}},{"type":"Feature","properties":{"zone_name":"21"},"geometry":{"type":"Polygon","coordinates":[[[131.5,40.5],[132,40.5],[132,41],[131.5,41],[131.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"22"},"geometry":{"type":"Polygon","coordinates":[[[128.5,40],[129,40],[129,40.5],[128.5,40.5],[128.5,40]]]}},{"type":"Feature","properties":{"zone_name":"23"},"geometry":{"type":"Polygon","coordinates":[[[129,40],[129.5,40],[129.5,40.5],[129,40.5],[129,40]]]}},{"type":"Feature","properties":{"zone_name":"24"},"geometry":{"type":"Polygon","coordinates":[[[129.5,40],[130,40],[130,40.5],[129.5,40.5],[129.5,40]]]}},{"type":"Feature","properties":{"zone_name":"25"},"geometry":{"type":"Polygon","coordinates":[[[130,40],[130.5,40],[130.5,40.5],[130,40.5],[130,40]]]}},{"type":"Feature","properties":{"zone_name":"26"},"geometry":{"type":"Polygon","coordinates":[[[130.5,40],[131,40],[131,40.5],[130.5,40.5],[130.5,40]]]}},{"type":"Feature","properties":{"zone_name":"27"},"geometry":{"type":"Polygon","coordinates":[[[131,40],[131.5,40],[131.5,40.5],[131,40.5],[131,40]]]}},{"type":"Feature","properties":{"zone_name":"28"},"geometry":{"type":"Polygon","coordinates":[[[131.5,40],[132,40],[132,40.5],[131.5,40.5],[131.5,40]]]}},{"type":"Feature","properties":{"zone_name":"29"},"geometry":{"type":"Polygon","coordinates":[[[127.5,39.5],[128,39.5],[128,40],[127.5,40],[127.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"30"},"geometry":{"type":"Polygon","coordinates":[[[128,39.5],[128.5,39.5],[128.5,40],[128,40],[128,39.5]]]}},{"type":"Feature","properties":{"zone_name":"31"},"geometry":{"type":"Polygon","coordinates":[[[128.5,39.5],[129,39.5],[129,40],[128.5,40],[128.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"32"},"geometry":{"type":"Polygon","coordinates":[[[129,39.5],[129.5,39.5],[129.5,40],[129,40],[129,39.5]]]}},{"type":"Feature","properties":{"zone_name":"33"},"geometry":{"type":"Polygon","coordinates":[[[129.5,39.5],[130,39.5],[130,40],[129.5,40],[129.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"34"},"geometry":{"type":"Polygon","coordinates":[[[130,39.5],[130.5,39.5],[130.5,40],[130,40],[130,39.5]]]}},{"type":"Feature","properties":{"zone_name":"35"},"geometry":{"type":"Polygon","coordinates":[[[130.5,39.5],[131,39.5],[131,40],[130.5,40],[130.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"36"},"geometry":{"type":"Polygon","coordinates":[[[131,39.5],[131.5,39.5],[131.5,40],[131,40],[131,39.5]]]}},{"type":"Feature","properties":{"zone_name":"37"},"geometry":{"type":"Polygon","coordinates":[[[131.5,39.5],[132,39.5],[132,40],[131.5,40],[131.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"38"},"geometry":{"type":"Polygon","coordinates":[[[127.5,39],[128,39],[128,39.5],[127.5,39.5],[127.5,39]]]}},{"type":"Feature","properties":{"zone_name":"39"},"geometry":{"type":"Polygon","coordinates":[[[128,39],[128.5,39],[128.5,39.5],[128,39.5],[128,39]]]}},{"type":"Feature","properties":{"zone_name":"40"},"geometry":{"type":"Polygon","coordinates":[[[128.5,39],[129,39],[129,39.5],[128.5,39.5],[128.5,39]]]}},{"type":"Feature","properties":{"zone_name":"41"},"geometry":{"type":"Polygon","coordinates":[[[129,39],[129.5,39],[129.5,39.5],[129,39.5],[129,39]]]}},{"type":"Feature","properties":{"zone_name":"42"},"geometry":{"type":"Polygon","coordinates":[[[129.5,39],[130,39],[130,39.5],[129.5,39.5],[129.5,39]]]}},{"type":"Feature","properties":{"zone_name":"43"},"geometry":{"type":"Polygon","coordinates":[[[130,39],[130.5,39],[130.5,39.5],[130,39.5],[130,39]]]}},{"type":"Feature","properties":{"zone_name":"44"},"geometry":{"type":"Polygon","coordinates":[[[130.5,39],[131,39],[131,39.5],[130.5,39.5],[130.5,39]]]}},{"type":"Feature","properties":{"zone_name":"45"},"geometry":{"type":"Polygon","coordinates":[[[131,39],[131.5,39],[131.5,39.5],[131,39.5],[131,39]]]}},{"type":"Feature","properties":{"zone_name":"46"},"geometry":{"type":"Polygon","coordinates":[[[131.5,39],[132,39],[132,39.5],[131.5,39.5],[131.5,39]]]}},{"type":"Feature","properties":{"zone_name":"47"},"geometry":{"type":"Polygon","coordinates":[[[128,38.5],[128.5,38.5],[128.5,39],[128,39],[128,38.5]]]}},{"type":"Feature","properties":{"zone_name":"48"},"geometry":{"type":"Polygon","coordinates":[[[128.5,38.5],[129,38.5],[129,39],[128.5,39],[128.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"49"},"geometry":{"type":"Polygon","coordinates":[[[129,38.5],[129.5,38.5],[129.5,39],[129,39],[129,38.5]]]}},{"type":"Feature","properties":{"zone_name":"50"},"geometry":{"type":"Polygon","coordinates":[[[129.5,38.5],[130,38.5],[130,39],[129.5,39],[129.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"51"},"geometry":{"type":"Polygon","coordinates":[[[130,38.5],[130.5,38.5],[130.5,39],[130,39],[130,38.5]]]}},{"type":"Feature","properties":{"zone_name":"52"},"geometry":{"type":"Polygon","coordinates":[[[130.5,38.5],[131,38.5],[131,39],[130.5,39],[130.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"53"},"geometry":{"type":"Polygon","coordinates":[[[131,38.5],[131.5,38.5],[131.5,39],[131,39],[131,38.5]]]}},{"type":"Feature","properties":{"zone_name":"54"},"geometry":{"type":"Polygon","coordinates":[[[131.5,38.5],[132,38.5],[132,39],[131.5,39],[131.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"55"},"geometry":{"type":"Polygon","coordinates":[[[128.5,38],[129,38],[129,38.5],[128.5,38.5],[128.5,38]]]}},{"type":"Feature","properties":{"zone_name":"56"},"geometry":{"type":"Polygon","coordinates":[[[129,38],[129.5,38],[129.5,38.5],[129,38.5],[129,38]]]}},{"type":"Feature","properties":{"zone_name":"57"},"geometry":{"type":"Polygon","coordinates":[[[129.5,38],[130,38],[130,38.5],[129.5,38.5],[129.5,38]]]}},{"type":"Feature","properties":{"zone_name":"58"},"geometry":{"type":"Polygon","coordinates":[[[130,38],[130.5,38],[130.5,38.5],[130,38.5],[130,38]]]}},{"type":"Feature","properties":{"zone_name":"59"},"geometry":{"type":"Polygon","coordinates":[[[130.5,38],[131,38],[131,38.5],[130.5,38.5],[130.5,38]]]}},{"type":"Feature","properties":{"zone_name":"60"},"geometry":{"type":"Polygon","coordinates":[[[131,38],[131.5,38],[131.5,38.5],[131,38.5],[131,38]]]}},{"type":"Feature","properties":{"zone_name":"61"},"geometry":{"type":"Polygon","coordinates":[[[131.5,38],[132,38],[132,38.5],[131.5,38.5],[131.5,38]]]}},{"type":"Feature","properties":{"zone_name":"62"},"geometry":{"type":"Polygon","coordinates":[[[128.5,37.5],[129,37.5],[129,38],[128.5,38],[128.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"63"},"geometry":{"type":"Polygon","coordinates":[[[129,37.5],[129.5,37.5],[129.5,38],[129,38],[129,37.5]]]}},{"type":"Feature","properties":{"zone_name":"64"},"geometry":{"type":"Polygon","coordinates":[[[129.5,37.5],[130,37.5],[130,38],[129.5,38],[129.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"65"},"geometry":{"type":"Polygon","coordinates":[[[130,37.5],[130.5,37.5],[130.5,38],[130,38],[130,37.5]]]}},{"type":"Feature","properties":{"zone_name":"66"},"geometry":{"type":"Polygon","coordinates":[[[130.5,37.5],[131,37.5],[131,38],[130.5,38],[130.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"67"},"geometry":{"type":"Polygon","coordinates":[[[131,37.5],[131.5,37.5],[131.5,38],[131,38],[131,37.5]]]}},{"type":"Feature","properties":{"zone_name":"68"},"geometry":{"type":"Polygon","coordinates":[[[131.5,37.5],[132,37.5],[132,38],[131.5,38],[131.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"69"},"geometry":{"type":"Polygon","coordinates":[[[129,37],[129.5,37],[129.5,37.5],[129,37.5],[129,37]]]}},{"type":"Feature","properties":{"zone_name":"70"},"geometry":{"type":"Polygon","coordinates":[[[129.5,37],[130,37],[130,37.5],[129.5,37.5],[129.5,37]]]}},{"type":"Feature","properties":{"zone_name":"71"},"geometry":{"type":"Polygon","coordinates":[[[130,37],[130.5,37],[130.5,37.5],[130,37.5],[130,37]]]}},{"type":"Feature","properties":{"zone_name":"72"},"geometry":{"type":"Polygon","coordinates":[[[130.5,37],[131,37],[131,37.5],[130.5,37.5],[130.5,37]]]}},{"type":"Feature","properties":{"zone_name":"73"},"geometry":{"type":"Polygon","coordinates":[[[131,37],[131.5,37],[131.5,37.5],[131,37.5],[131,37]]]}},{"type":"Feature","properties":{"zone_name":"74"},"geometry":{"type":"Polygon","coordinates":[[[131.5,37],[132,37],[132,37.5],[131.5,37.5],[131.5,37]]]}},{"type":"Feature","properties":{"zone_name":"75"},"geometry":{"type":"Polygon","coordinates":[[[129,36.5],[129.5,36.5],[129.5,37],[129,37],[129,36.5]]]}},{"type":"Feature","properties":{"zone_name":"76"},"geometry":{"type":"Polygon","coordinates":[[[129.5,36.5],[130,36.5],[130,37],[129.5,37],[129.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"77"},"geometry":{"type":"Polygon","coordinates":[[[130,36.5],[130.5,36.5],[130.5,37],[130,37],[130,36.5]]]}},{"type":"Feature","properties":{"zone_name":"78"},"geometry":{"type":"Polygon","coordinates":[[[130.5,36.5],[131,36.5],[131,37],[130.5,37],[130.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"79"},"geometry":{"type":"Polygon","coordinates":[[[131,36.5],[131.5,36.5],[131.5,37],[131,37],[131,36.5]]]}},{"type":"Feature","properties":{"zone_name":"80"},"geometry":{"type":"Polygon","coordinates":[[[131.5,36.5],[132,36.5],[132,37],[131.5,37],[131.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"81"},"geometry":{"type":"Polygon","coordinates":[[[129,36],[129.5,36],[129.5,36.5],[129,36.5],[129,36]]]}},{"type":"Feature","properties":{"zone_name":"82"},"geometry":{"type":"Polygon","coordinates":[[[129.5,36],[130,36],[130,36.5],[129.5,36.5],[129.5,36]]]}},{"type":"Feature","properties":{"zone_name":"83"},"geometry":{"type":"Polygon","coordinates":[[[130,36],[130.5,36],[130.5,36.5],[130,36.5],[130,36]]]}},{"type":"Feature","properties":{"zone_name":"84"},"geometry":{"type":"Polygon","coordinates":[[[130.5,36],[131,36],[131,36.5],[130.5,36.5],[130.5,36]]]}},{"type":"Feature","properties":{"zone_name":"85"},"geometry":{"type":"Polygon","coordinates":[[[131,36],[131.5,36],[131.5,36.5],[131,36.5],[131,36]]]}},{"type":"Feature","properties":{"zone_name":"86"},"geometry":{"type":"Polygon","coordinates":[[[131.5,36],[132,36],[132,36.5],[131.5,36.5],[131.5,36]]]}},{"type":"Feature","properties":{"zone_name":"87"},"geometry":{"type":"Polygon","coordinates":[[[129.5,35.5],[130,35.5],[130,36],[129.5,36],[129.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"88"},"geometry":{"type":"Polygon","coordinates":[[[130,35.5],[130.5,35.5],[130.5,36],[130,36],[130,35.5]]]}},{"type":"Feature","properties":{"zone_name":"89"},"geometry":{"type":"Polygon","coordinates":[[[130.5,35.5],[131,35.5],[131,36],[130.5,36],[130.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"90"},"geometry":{"type":"Polygon","coordinates":[[[131,35.5],[131.5,35.5],[131.5,36],[131,36],[131,35.5]]]}},{"type":"Feature","properties":{"zone_name":"91"},"geometry":{"type":"Polygon","coordinates":[[[131.5,35.5],[132,35.5],[132,36],[131.5,36],[131.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"92"},"geometry":{"type":"Polygon","coordinates":[[[129,35],[129.5,35],[129.5,35.5],[129,35.5],[129,35]]]}},{"type":"Feature","properties":{"zone_name":"93"},"geometry":{"type":"Polygon","coordinates":[[[129.5,35],[130,35],[130,35.5],[129.5,35.5],[129.5,35]]]}},{"type":"Feature","properties":{"zone_name":"94"},"geometry":{"type":"Polygon","coordinates":[[[130,35],[130.5,35],[130.5,35.5],[130,35.5],[130,35]]]}},{"type":"Feature","properties":{"zone_name":"95"},"geometry":{"type":"Polygon","coordinates":[[[130.5,35],[131,35],[131,35.5],[130.5,35.5],[130.5,35]]]}},{"type":"Feature","properties":{"zone_name":"96"},"geometry":{"type":"Polygon","coordinates":[[[131,35],[131.5,35],[131.5,35.5],[131,35.5],[131,35]]]}},{"type":"Feature","properties":{"zone_name":"97"},"geometry":{"type":"Polygon","coordinates":[[[127.5,34.5],[128,34.5],[128,35],[127.5,35],[127.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"98"},"geometry":{"type":"Polygon","coordinates":[[[128,34.5],[128.5,34.5],[128.5,35],[128,35],[128,34.5]]]}},{"type":"Feature","properties":{"zone_name":"99"},"geometry":{"type":"Polygon","coordinates":[[[128.5,34.5],[129,34.5],[129,35],[128.5,35],[128.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"100"},"geometry":{"type":"Polygon","coordinates":[[[129,34.5],[129.5,34.5],[129.5,35],[129,35],[129,34.5]]]}},{"type":"Feature","properties":{"zone_name":"101"},"geometry":{"type":"Polygon","coordinates":[[[129.5,34.5],[130,34.5],[130,35],[129.5,35],[129.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"102"},"geometry":{"type":"Polygon","coordinates":[[[130,34.5],[130.5,34.5],[130.5,35],[130,35],[130,34.5]]]}},{"type":"Feature","properties":{"zone_name":"103"},"geometry":{"type":"Polygon","coordinates":[[[130.5,34.5],[131,34.5],[131,35],[130.5,35],[130.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"104"},"geometry":{"type":"Polygon","coordinates":[[[127.5,34],[128,34],[128,34.5],[127.5,34.5],[127.5,34]]]}},{"type":"Feature","properties":{"zone_name":"105"},"geometry":{"type":"Polygon","coordinates":[[[128,34],[128.5,34],[128.5,34.5],[128,34.5],[128,34]]]}},{"type":"Feature","properties":{"zone_name":"106"},"geometry":{"type":"Polygon","coordinates":[[[128.5,34],[129,34],[129,34.5],[128.5,34.5],[128.5,34]]]}},{"type":"Feature","properties":{"zone_name":"107"},"geometry":{"type":"Polygon","coordinates":[[[129,34],[129.5,34],[129.5,34.5],[129,34.5],[129,34]]]}},{"type":"Feature","properties":{"zone_name":"108"},"geometry":{"type":"Polygon","coordinates":[[[129.5,34],[130,34],[130,34.5],[129.5,34.5],[129.5,34]]]}},{"type":"Feature","properties":{"zone_name":"109"},"geometry":{"type":"Polygon","coordinates":[[[130,34],[130.5,34],[130.5,34.5],[130,34.5],[130,34]]]}},{"type":"Feature","properties":{"zone_name":"110"},"geometry":{"type":"Polygon","coordinates":[[[127.5,33.5],[128,33.5],[128,34],[127.5,34],[127.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"111"},"geometry":{"type":"Polygon","coordinates":[[[128,33.5],[128.5,33.5],[128.5,34],[128,34],[128,33.5]]]}},{"type":"Feature","properties":{"zone_name":"112"},"geometry":{"type":"Polygon","coordinates":[[[128.5,33.5],[129,33.5],[129,34],[128.5,34],[128.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"113"},"geometry":{"type":"Polygon","coordinates":[[[127.5,33],[128,33],[128,33.5],[127.5,33.5],[127.5,33]]]}},{"type":"Feature","properties":{"zone_name":"114"},"geometry":{"type":"Polygon","coordinates":[[[128,33],[128.5,33],[128.5,33.5],[128,33.5],[128,33]]]}},{"type":"Feature","properties":{"zone_name":"115"},"geometry":{"type":"Polygon","coordinates":[[[127.5,32.5],[128,32.5],[128,33],[127.5,33],[127.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"116"},"geometry":{"type":"Polygon","coordinates":[[[123,39.5],[123.5,39.5],[123.5,40],[123,40],[123,39.5]]]}},{"type":"Feature","properties":{"zone_name":"117"},"geometry":{"type":"Polygon","coordinates":[[[123.5,39.5],[124,39.5],[124,40],[123.5,40],[123.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"118"},"geometry":{"type":"Polygon","coordinates":[[[124,39.5],[124.5,39.5],[124.5,40],[124,40],[124,39.5]]]}},{"type":"Feature","properties":{"zone_name":"119"},"geometry":{"type":"Polygon","coordinates":[[[122.5,39],[123,39],[123,39.5],[122.5,39.5],[122.5,39]]]}},{"type":"Feature","properties":{"zone_name":"120"},"geometry":{"type":"Polygon","coordinates":[[[123,39],[123.5,39],[123.5,39.5],[123,39.5],[123,39]]]}},{"type":"Feature","properties":{"zone_name":"121"},"geometry":{"type":"Polygon","coordinates":[[[123.5,39],[124,39],[124,39.5],[123.5,39.5],[123.5,39]]]}},{"type":"Feature","properties":{"zone_name":"122"},"geometry":{"type":"Polygon","coordinates":[[[124,39],[124.5,39],[124.5,39.5],[124,39.5],[124,39]]]}},{"type":"Feature","properties":{"zone_name":"123"},"geometry":{"type":"Polygon","coordinates":[[[124.5,39],[125,39],[125,39.5],[124.5,39.5],[124.5,39]]]}},{"type":"Feature","properties":{"zone_name":"124"},"geometry":{"type":"Polygon","coordinates":[[[125,39],[125.5,39],[125.5,39.5],[125,39.5],[125,39]]]}},{"type":"Feature","properties":{"zone_name":"125"},"geometry":{"type":"Polygon","coordinates":[[[122,38.5],[122.5,38.5],[122.5,39],[122,39],[122,38.5]]]}},{"type":"Feature","properties":{"zone_name":"126"},"geometry":{"type":"Polygon","coordinates":[[[122.5,38.5],[123,38.5],[123,39],[122.5,39],[122.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"127"},"geometry":{"type":"Polygon","coordinates":[[[123,38.5],[123.5,38.5],[123.5,39],[123,39],[123,38.5]]]}},{"type":"Feature","properties":{"zone_name":"128"},"geometry":{"type":"Polygon","coordinates":[[[123.5,38.5],[124,38.5],[124,39],[123.5,39],[123.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"129"},"geometry":{"type":"Polygon","coordinates":[[[124,38.5],[124.5,38.5],[124.5,39],[124,39],[124,38.5]]]}},{"type":"Feature","properties":{"zone_name":"130"},"geometry":{"type":"Polygon","coordinates":[[[124.5,38.5],[125,38.5],[125,39],[124.5,39],[124.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"131"},"geometry":{"type":"Polygon","coordinates":[[[121.5,38],[122,38],[122,38.5],[121.5,38.5],[121.5,38]]]}},{"type":"Feature","properties":{"zone_name":"132"},"geometry":{"type":"Polygon","coordinates":[[[122,38],[122.5,38],[122.5,38.5],[122,38.5],[122,38]]]}},{"type":"Feature","properties":{"zone_name":"133"},"geometry":{"type":"Polygon","coordinates":[[[122.5,38],[123,38],[123,38.5],[122.5,38.5],[122.5,38]]]}},{"type":"Feature","properties":{"zone_name":"134"},"geometry":{"type":"Polygon","coordinates":[[[123,38],[123.5,38],[123.5,38.5],[123,38.5],[123,38]]]}},{"type":"Feature","properties":{"zone_name":"135"},"geometry":{"type":"Polygon","coordinates":[[[123.5,38],[124,38],[124,38.5],[123.5,38.5],[123.5,38]]]}},{"type":"Feature","properties":{"zone_name":"136"},"geometry":{"type":"Polygon","coordinates":[[[124,38],[124.5,38],[124.5,38.5],[124,38.5],[124,38]]]}},{"type":"Feature","properties":{"zone_name":"137"},"geometry":{"type":"Polygon","coordinates":[[[124.5,38],[125,38],[125,38.5],[124.5,38.5],[124.5,38]]]}},{"type":"Feature","properties":{"zone_name":"138"},"geometry":{"type":"Polygon","coordinates":[[[121.5,37.5],[122,37.5],[122,38],[121.5,38],[121.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"139"},"geometry":{"type":"Polygon","coordinates":[[[122,37.5],[122.5,37.5],[122.5,38],[122,38],[122,37.5]]]}},{"type":"Feature","properties":{"zone_name":"140"},"geometry":{"type":"Polygon","coordinates":[[[122.5,37.5],[123,37.5],[123,38],[122.5,38],[122.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"141"},"geometry":{"type":"Polygon","coordinates":[[[123,37.5],[123.5,37.5],[123.5,38],[123,38],[123,37.5]]]}},{"type":"Feature","properties":{"zone_name":"142"},"geometry":{"type":"Polygon","coordinates":[[[123.5,37.5],[124,37.5],[124,38],[123.5,38],[123.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"143"},"geometry":{"type":"Polygon","coordinates":[[[124,37.5],[124.5,37.5],[124.5,38],[124,38],[124,37.5]]]}},{"type":"Feature","properties":{"zone_name":"144"},"geometry":{"type":"Polygon","coordinates":[[[124.5,37.5],[125,37.5],[125,38],[124.5,38],[124.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"145"},"geometry":{"type":"Polygon","coordinates":[[[125,37.5],[125.5,37.5],[125.5,38],[125,38],[125,37.5]]]}},{"type":"Feature","properties":{"zone_name":"146"},"geometry":{"type":"Polygon","coordinates":[[[125.5,37.5],[126,37.5],[126,38],[125.5,38],[125.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"147"},"geometry":{"type":"Polygon","coordinates":[[[122.5,37],[123,37],[123,37.5],[122.5,37.5],[122.5,37]]]}},{"type":"Feature","properties":{"zone_name":"148"},"geometry":{"type":"Polygon","coordinates":[[[123,37],[123.5,37],[123.5,37.5],[123,37.5],[123,37]]]}},{"type":"Feature","properties":{"zone_name":"149"},"geometry":{"type":"Polygon","coordinates":[[[123.5,37],[124,37],[124,37.5],[123.5,37.5],[123.5,37]]]}},{"type":"Feature","properties":{"zone_name":"150"},"geometry":{"type":"Polygon","coordinates":[[[124,37],[124.5,37],[124.5,37.5],[124,37.5],[124,37]]]}},{"type":"Feature","properties":{"zone_name":"151"},"geometry":{"type":"Polygon","coordinates":[[[124.5,37],[125,37],[125,37.5],[124.5,37.5],[124.5,37]]]}},{"type":"Feature","properties":{"zone_name":"152"},"geometry":{"type":"Polygon","coordinates":[[[125,37],[125.5,37],[125.5,37.5],[125,37.5],[125,37]]]}},{"type":"Feature","properties":{"zone_name":"153"},"geometry":{"type":"Polygon","coordinates":[[[125.5,37],[126,37],[126,37.5],[125.5,37.5],[125.5,37]]]}},{"type":"Feature","properties":{"zone_name":"154"},"geometry":{"type":"Polygon","coordinates":[[[126,37],[126.5,37],[126.5,37.5],[126,37.5],[126,37]]]}},{"type":"Feature","properties":{"zone_name":"155"},"geometry":{"type":"Polygon","coordinates":[[[121.5,36.5],[122,36.5],[122,37],[121.5,37],[121.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"156"},"geometry":{"type":"Polygon","coordinates":[[[122,36.5],[122.5,36.5],[122.5,37],[122,37],[122,36.5]]]}},{"type":"Feature","properties":{"zone_name":"157"},"geometry":{"type":"Polygon","coordinates":[[[122.5,36.5],[123,36.5],[123,37],[122.5,37],[122.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"158"},"geometry":{"type":"Polygon","coordinates":[[[123,36.5],[123.5,36.5],[123.5,37],[123,37],[123,36.5]]]}},{"type":"Feature","properties":{"zone_name":"159"},"geometry":{"type":"Polygon","coordinates":[[[123.5,36.5],[124,36.5],[124,37],[123.5,37],[123.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"160"},"geometry":{"type":"Polygon","coordinates":[[[124,36.5],[124.5,36.5],[124.5,37],[124,37],[124,36.5]]]}},{"type":"Feature","properties":{"zone_name":"161"},"geometry":{"type":"Polygon","coordinates":[[[124.5,36.5],[125,36.5],[125,37],[124.5,37],[124.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"162"},"geometry":{"type":"Polygon","coordinates":[[[125,36.5],[125.5,36.5],[125.5,37],[125,37],[125,36.5]]]}},{"type":"Feature","properties":{"zone_name":"163"},"geometry":{"type":"Polygon","coordinates":[[[125.5,36.5],[126,36.5],[126,37],[125.5,37],[125.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"164"},"geometry":{"type":"Polygon","coordinates":[[[126,36.5],[126.5,36.5],[126.5,37],[126,37],[126,36.5]]]}},{"type":"Feature","properties":{"zone_name":"165"},"geometry":{"type":"Polygon","coordinates":[[[121.5,36],[122,36],[122,36.5],[121.5,36.5],[121.5,36]]]}},{"type":"Feature","properties":{"zone_name":"166"},"geometry":{"type":"Polygon","coordinates":[[[122,36],[122.5,36],[122.5,36.5],[122,36.5],[122,36]]]}},{"type":"Feature","properties":{"zone_name":"167"},"geometry":{"type":"Polygon","coordinates":[[[122.5,36],[123,36],[123,36.5],[122.5,36.5],[122.5,36]]]}},{"type":"Feature","properties":{"zone_name":"168"},"geometry":{"type":"Polygon","coordinates":[[[123,36],[123.5,36],[123.5,36.5],[123,36.5],[123,36]]]}},{"type":"Feature","properties":{"zone_name":"169"},"geometry":{"type":"Polygon","coordinates":[[[123.5,36],[124,36],[124,36.5],[123.5,36.5],[123.5,36]]]}},{"type":"Feature","properties":{"zone_name":"170"},"geometry":{"type":"Polygon","coordinates":[[[124,36],[124.5,36],[124.5,36.5],[124,36.5],[124,36]]]}},{"type":"Feature","properties":{"zone_name":"171"},"geometry":{"type":"Polygon","coordinates":[[[124.5,36],[125,36],[125,36.5],[124.5,36.5],[124.5,36]]]}},{"type":"Feature","properties":{"zone_name":"172"},"geometry":{"type":"Polygon","coordinates":[[[125,36],[125.5,36],[125.5,36.5],[125,36.5],[125,36]]]}},{"type":"Feature","properties":{"zone_name":"173"},"geometry":{"type":"Polygon","coordinates":[[[125.5,36],[126,36],[126,36.5],[125.5,36.5],[125.5,36]]]}},{"type":"Feature","properties":{"zone_name":"174"},"geometry":{"type":"Polygon","coordinates":[[[126,36],[126.5,36],[126.5,36.5],[126,36.5],[126,36]]]}},{"type":"Feature","properties":{"zone_name":"175"},"geometry":{"type":"Polygon","coordinates":[[[121.5,35.5],[122,35.5],[122,36],[121.5,36],[121.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"176"},"geometry":{"type":"Polygon","coordinates":[[[122,35.5],[122.5,35.5],[122.5,36],[122,36],[122,35.5]]]}},{"type":"Feature","properties":{"zone_name":"177"},"geometry":{"type":"Polygon","coordinates":[[[122.5,35.5],[123,35.5],[123,36],[122.5,36],[122.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"178"},"geometry":{"type":"Polygon","coordinates":[[[123,35.5],[123.5,35.5],[123.5,36],[123,36],[123,35.5]]]}},{"type":"Feature","properties":{"zone_name":"179"},"geometry":{"type":"Polygon","coordinates":[[[123.5,35.5],[124,35.5],[124,36],[123.5,36],[123.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"180"},"geometry":{"type":"Polygon","coordinates":[[[124,35.5],[124.5,35.5],[124.5,36],[124,36],[124,35.5]]]}},{"type":"Feature","properties":{"zone_name":"181"},"geometry":{"type":"Polygon","coordinates":[[[124.5,35.5],[125,35.5],[125,36],[124.5,36],[124.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"182"},"geometry":{"type":"Polygon","coordinates":[[[125,35.5],[125.5,35.5],[125.5,36],[125,36],[125,35.5]]]}},{"type":"Feature","properties":{"zone_name":"183"},"geometry":{"type":"Polygon","coordinates":[[[125.5,35.5],[126,35.5],[126,36],[125.5,36],[125.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"184"},"geometry":{"type":"Polygon","coordinates":[[[126,35.5],[126.5,35.5],[126.5,36],[126,36],[126,35.5]]]}},{"type":"Feature","properties":{"zone_name":"185"},"geometry":{"type":"Polygon","coordinates":[[[121.5,35],[122,35],[122,35.5],[121.5,35.5],[121.5,35]]]}},{"type":"Feature","properties":{"zone_name":"186"},"geometry":{"type":"Polygon","coordinates":[[[122,35],[122.5,35],[122.5,35.5],[122,35.5],[122,35]]]}},{"type":"Feature","properties":{"zone_name":"187"},"geometry":{"type":"Polygon","coordinates":[[[122.5,35],[123,35],[123,35.5],[122.5,35.5],[122.5,35]]]}},{"type":"Feature","properties":{"zone_name":"188"},"geometry":{"type":"Polygon","coordinates":[[[123,35],[123.5,35],[123.5,35.5],[123,35.5],[123,35]]]}},{"type":"Feature","properties":{"zone_name":"189"},"geometry":{"type":"Polygon","coordinates":[[[123.5,35],[124,35],[124,35.5],[123.5,35.5],[123.5,35]]]}},{"type":"Feature","properties":{"zone_name":"190"},"geometry":{"type":"Polygon","coordinates":[[[124,35],[124.5,35],[124.5,35.5],[124,35.5],[124,35]]]}},{"type":"Feature","properties":{"zone_name":"191"},"geometry":{"type":"Polygon","coordinates":[[[124.5,35],[125,35],[125,35.5],[124.5,35.5],[124.5,35]]]}},{"type":"Feature","properties":{"zone_name":"192"},"geometry":{"type":"Polygon","coordinates":[[[125,35],[125.5,35],[125.5,35.5],[125,35.5],[125,35]]]}},{"type":"Feature","properties":{"zone_name":"193"},"geometry":{"type":"Polygon","coordinates":[[[125.5,35],[126,35],[126,35.5],[125.5,35.5],[125.5,35]]]}},{"type":"Feature","properties":{"zone_name":"194"},"geometry":{"type":"Polygon","coordinates":[[[126,35],[126.5,35],[126.5,35.5],[126,35.5],[126,35]]]}},{"type":"Feature","properties":{"zone_name":"195"},"geometry":{"type":"Polygon","coordinates":[[[121.5,34.5],[122,34.5],[122,35],[121.5,35],[121.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"196"},"geometry":{"type":"Polygon","coordinates":[[[122,34.5],[122.5,34.5],[122.5,35],[122,35],[122,34.5]]]}},{"type":"Feature","properties":{"zone_name":"197"},"geometry":{"type":"Polygon","coordinates":[[[122.5,34.5],[123,34.5],[123,35],[122.5,35],[122.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"198"},"geometry":{"type":"Polygon","coordinates":[[[123,34.5],[123.5,34.5],[123.5,35],[123,35],[123,34.5]]]}},{"type":"Feature","properties":{"zone_name":"199"},"geometry":{"type":"Polygon","coordinates":[[[123.5,34.5],[124,34.5],[124,35],[123.5,35],[123.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"200"},"geometry":{"type":"Polygon","coordinates":[[[124,34.5],[124.5,34.5],[124.5,35],[124,35],[124,34.5]]]}},{"type":"Feature","properties":{"zone_name":"201"},"geometry":{"type":"Polygon","coordinates":[[[124.5,34.5],[125,34.5],[125,35],[124.5,35],[124.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"202"},"geometry":{"type":"Polygon","coordinates":[[[125,34.5],[125.5,34.5],[125.5,35],[125,35],[125,34.5]]]}},{"type":"Feature","properties":{"zone_name":"203"},"geometry":{"type":"Polygon","coordinates":[[[125.5,34.5],[126,34.5],[126,35],[125.5,35],[125.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"204"},"geometry":{"type":"Polygon","coordinates":[[[126,34.5],[126.5,34.5],[126.5,35],[126,35],[126,34.5]]]}},{"type":"Feature","properties":{"zone_name":"205"},"geometry":{"type":"Polygon","coordinates":[[[122.5,34],[123,34],[123,34.5],[122.5,34.5],[122.5,34]]]}},{"type":"Feature","properties":{"zone_name":"206"},"geometry":{"type":"Polygon","coordinates":[[[123,34],[123.5,34],[123.5,34.5],[123,34.5],[123,34]]]}},{"type":"Feature","properties":{"zone_name":"207"},"geometry":{"type":"Polygon","coordinates":[[[123.5,34],[124,34],[124,34.5],[123.5,34.5],[123.5,34]]]}},{"type":"Feature","properties":{"zone_name":"208"},"geometry":{"type":"Polygon","coordinates":[[[124,34],[124.5,34],[124.5,34.5],[124,34.5],[124,34]]]}},{"type":"Feature","properties":{"zone_name":"209"},"geometry":{"type":"Polygon","coordinates":[[[124.5,34],[125,34],[125,34.5],[124.5,34.5],[124.5,34]]]}},{"type":"Feature","properties":{"zone_name":"210"},"geometry":{"type":"Polygon","coordinates":[[[125,34],[125.5,34],[125.5,34.5],[125,34.5],[125,34]]]}},{"type":"Feature","properties":{"zone_name":"211"},"geometry":{"type":"Polygon","coordinates":[[[125.5,34],[126,34],[126,34.5],[125.5,34.5],[125.5,34]]]}},{"type":"Feature","properties":{"zone_name":"212"},"geometry":{"type":"Polygon","coordinates":[[[126,34],[126.5,34],[126.5,34.5],[126,34.5],[126,34]]]}},{"type":"Feature","properties":{"zone_name":"213"},"geometry":{"type":"Polygon","coordinates":[[[126.5,34],[127,34],[127,34.5],[126.5,34.5],[126.5,34]]]}},{"type":"Feature","properties":{"zone_name":"214"},"geometry":{"type":"Polygon","coordinates":[[[127,34],[127.5,34],[127.5,34.5],[127,34.5],[127,34]]]}},{"type":"Feature","properties":{"zone_name":"215"},"geometry":{"type":"Polygon","coordinates":[[[122.5,33.5],[123,33.5],[123,34],[122.5,34],[122.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"216"},"geometry":{"type":"Polygon","coordinates":[[[123,33.5],[123.5,33.5],[123.5,34],[123,34],[123,33.5]]]}},{"type":"Feature","properties":{"zone_name":"217"},"geometry":{"type":"Polygon","coordinates":[[[123.5,33.5],[124,33.5],[124,34],[123.5,34],[123.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"218"},"geometry":{"type":"Polygon","coordinates":[[[124,33.5],[124.5,33.5],[124.5,34],[124,34],[124,33.5]]]}},{"type":"Feature","properties":{"zone_name":"219"},"geometry":{"type":"Polygon","coordinates":[[[124.5,33.5],[125,33.5],[125,34],[124.5,34],[124.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"220"},"geometry":{"type":"Polygon","coordinates":[[[125,33.5],[125.5,33.5],[125.5,34],[125,34],[125,33.5]]]}},{"type":"Feature","properties":{"zone_name":"221"},"geometry":{"type":"Polygon","coordinates":[[[125.5,33.5],[126,33.5],[126,34],[125.5,34],[125.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"222"},"geometry":{"type":"Polygon","coordinates":[[[126,33.5],[126.5,33.5],[126.5,34],[126,34],[126,33.5]]]}},{"type":"Feature","properties":{"zone_name":"223"},"geometry":{"type":"Polygon","coordinates":[[[126.5,33.5],[127,33.5],[127,34],[126.5,34],[126.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"224"},"geometry":{"type":"Polygon","coordinates":[[[127,33.5],[127.5,33.5],[127.5,34],[127,34],[127,33.5]]]}},{"type":"Feature","properties":{"zone_name":"225"},"geometry":{"type":"Polygon","coordinates":[[[122.5,33],[123,33],[123,33.5],[122.5,33.5],[122.5,33]]]}},{"type":"Feature","properties":{"zone_name":"226"},"geometry":{"type":"Polygon","coordinates":[[[123,33],[123.5,33],[123.5,33.5],[123,33.5],[123,33]]]}},{"type":"Feature","properties":{"zone_name":"227"},"geometry":{"type":"Polygon","coordinates":[[[123.5,33],[124,33],[124,33.5],[123.5,33.5],[123.5,33]]]}},{"type":"Feature","properties":{"zone_name":"228"},"geometry":{"type":"Polygon","coordinates":[[[124,33],[124.5,33],[124.5,33.5],[124,33.5],[124,33]]]}},{"type":"Feature","properties":{"zone_name":"229"},"geometry":{"type":"Polygon","coordinates":[[[124.5,33],[125,33],[125,33.5],[124.5,33.5],[124.5,33]]]}},{"type":"Feature","properties":{"zone_name":"230"},"geometry":{"type":"Polygon","coordinates":[[[125,33],[125.5,33],[125.5,33.5],[125,33.5],[125,33]]]}},{"type":"Feature","properties":{"zone_name":"231"},"geometry":{"type":"Polygon","coordinates":[[[125.5,33],[126,33],[126,33.5],[125.5,33.5],[125.5,33]]]}},{"type":"Feature","properties":{"zone_name":"232"},"geometry":{"type":"Polygon","coordinates":[[[126,33],[126.5,33],[126.5,33.5],[126,33.5],[126,33]]]}},{"type":"Feature","properties":{"zone_name":"233"},"geometry":{"type":"Polygon","coordinates":[[[126.5,33],[127,33],[127,33.5],[126.5,33.5],[126.5,33]]]}},{"type":"Feature","properties":{"zone_name":"234"},"geometry":{"type":"Polygon","coordinates":[[[127,33],[127.5,33],[127.5,33.5],[127,33.5],[127,33]]]}},{"type":"Feature","properties":{"zone_name":"235"},"geometry":{"type":"Polygon","coordinates":[[[122.5,32.5],[123,32.5],[123,33],[122.5,33],[122.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"236"},"geometry":{"type":"Polygon","coordinates":[[[123,32.5],[123.5,32.5],[123.5,33],[123,33],[123,32.5]]]}},{"type":"Feature","properties":{"zone_name":"237"},"geometry":{"type":"Polygon","coordinates":[[[123.5,32.5],[124,32.5],[124,33],[123.5,33],[123.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"238"},"geometry":{"type":"Polygon","coordinates":[[[124,32.5],[124.5,32.5],[124.5,33],[124,33],[124,32.5]]]}},{"type":"Feature","properties":{"zone_name":"239"},"geometry":{"type":"Polygon","coordinates":[[[124.5,32.5],[125,32.5],[125,33],[124.5,33],[124.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"240"},"geometry":{"type":"Polygon","coordinates":[[[125,32.5],[125.5,32.5],[125.5,33],[125,33],[125,32.5]]]}},{"type":"Feature","properties":{"zone_name":"241"},"geometry":{"type":"Polygon","coordinates":[[[125.5,32.5],[126,32.5],[126,33],[125.5,33],[125.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"242"},"geometry":{"type":"Polygon","coordinates":[[[126,32.5],[126.5,32.5],[126.5,33],[126,33],[126,32.5]]]}},{"type":"Feature","properties":{"zone_name":"243"},"geometry":{"type":"Polygon","coordinates":[[[126.5,32.5],[127,32.5],[127,33],[126.5,33],[126.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"244"},"geometry":{"type":"Polygon","coordinates":[[[127,32.5],[127.5,32.5],[127.5,33],[127,33],[127,32.5]]]}},{"type":"Feature","properties":{"zone_name":"245"},"geometry":{"type":"Polygon","coordinates":[[[122.5,32],[123,32],[123,32.5],[122.5,32.5],[122.5,32]]]}},{"type":"Feature","properties":{"zone_name":"246"},"geometry":{"type":"Polygon","coordinates":[[[123,32],[123.5,32],[123.5,32.5],[123,32.5],[123,32]]]}},{"type":"Feature","properties":{"zone_name":"247"},"geometry":{"type":"Polygon","coordinates":[[[123.5,32],[124,32],[124,32.5],[123.5,32.5],[123.5,32]]]}},{"type":"Feature","properties":{"zone_name":"248"},"geometry":{"type":"Polygon","coordinates":[[[124,32],[124.5,32],[124.5,32.5],[124,32.5],[124,32]]]}},{"type":"Feature","properties":{"zone_name":"249"},"geometry":{"type":"Polygon","coordinates":[[[124.5,32],[125,32],[125,32.5],[124.5,32.5],[124.5,32]]]}},{"type":"Feature","properties":{"zone_name":"250"},"geometry":{"type":"Polygon","coordinates":[[[125,32],[125.5,32],[125.5,32.5],[125,32.5],[125,32]]]}},{"type":"Feature","properties":{"zone_name":"251"},"geometry":{"type":"Polygon","coordinates":[[[125.5,32],[126,32],[126,32.5],[125.5,32.5],[125.5,32]]]}},{"type":"Feature","properties":{"zone_name":"252"},"geometry":{"type":"Polygon","coordinates":[[[126,32],[126.5,32],[126.5,32.5],[126,32.5],[126,32]]]}},{"type":"Feature","properties":{"zone_name":"253"},"geometry":{"type":"Polygon","coordinates":[[[126.5,32],[127,32],[127,32.5],[126.5,32.5],[126.5,32]]]}},{"type":"Feature","properties":{"zone_name":"254"},"geometry":{"type":"Polygon","coordinates":[[[127,32],[127.5,32],[127.5,32.5],[127,32.5],[127,32]]]}},{"type":"Feature","properties":{"zone_name":"258"},"geometry":{"type":"Polygon","coordinates":[[[135,28],[135.5,28],[135.5,28.5],[135,28.5],[135,28]]]}},{"type":"Feature","properties":{"zone_name":"259"},"geometry":{"type":"Polygon","coordinates":[[[135.5,28],[136,28],[136,28.5],[135.5,28.5],[135.5,28]]]}},{"type":"Feature","properties":{"zone_name":"260"},"geometry":{"type":"Polygon","coordinates":[[[136,28],[136.5,28],[136.5,28.5],[136,28.5],[136,28]]]}},{"type":"Feature","properties":{"zone_name":"261"},"geometry":{"type":"Polygon","coordinates":[[[136.5,28],[137,28],[137,28.5],[136.5,28.5],[136.5,28]]]}},{"type":"Feature","properties":{"zone_name":"262"},"geometry":{"type":"Polygon","coordinates":[[[137,28],[137.5,28],[137.5,28.5],[137,28.5],[137,28]]]}},{"type":"Feature","properties":{"zone_name":"263"},"geometry":{"type":"Polygon","coordinates":[[[137.5,28],[138,28],[138,28.5],[137.5,28.5],[137.5,28]]]}},{"type":"Feature","properties":{"zone_name":"264"},"geometry":{"type":"Polygon","coordinates":[[[135,27.5],[135.5,27.5],[135.5,28],[135,28],[135,27.5]]]}},{"type":"Feature","properties":{"zone_name":"265"},"geometry":{"type":"Polygon","coordinates":[[[135.5,27.5],[136,27.5],[136,28],[135.5,28],[135.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"266"},"geometry":{"type":"Polygon","coordinates":[[[136,27.5],[136.5,27.5],[136.5,28],[136,28],[136,27.5]]]}},{"type":"Feature","properties":{"zone_name":"267"},"geometry":{"type":"Polygon","coordinates":[[[136.5,27.5],[137,27.5],[137,28],[136.5,28],[136.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"268"},"geometry":{"type":"Polygon","coordinates":[[[137,27.5],[137.5,27.5],[137.5,28],[137,28],[137,27.5]]]}},{"type":"Feature","properties":{"zone_name":"269"},"geometry":{"type":"Polygon","coordinates":[[[137.5,27.5],[138,27.5],[138,28],[137.5,28],[137.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"270"},"geometry":{"type":"Polygon","coordinates":[[[135,27],[135.5,27],[135.5,27.5],[135,27.5],[135,27]]]}},{"type":"Feature","properties":{"zone_name":"271"},"geometry":{"type":"Polygon","coordinates":[[[135.5,27],[136,27],[136,27.5],[135.5,27.5],[135.5,27]]]}},{"type":"Feature","properties":{"zone_name":"272"},"geometry":{"type":"Polygon","coordinates":[[[136,27],[136.5,27],[136.5,27.5],[136,27.5],[136,27]]]}},{"type":"Feature","properties":{"zone_name":"273"},"geometry":{"type":"Polygon","coordinates":[[[136.5,27],[137,27],[137,27.5],[136.5,27.5],[136.5,27]]]}},{"type":"Feature","properties":{"zone_name":"274"},"geometry":{"type":"Polygon","coordinates":[[[137,27],[137.5,27],[137.5,27.5],[137,27.5],[137,27]]]}},{"type":"Feature","properties":{"zone_name":"275"},"geometry":{"type":"Polygon","coordinates":[[[137.5,27],[138,27],[138,27.5],[137.5,27.5],[137.5,27]]]}},{"type":"Feature","properties":{"zone_name":"276"},"geometry":{"type":"Polygon","coordinates":[[[135,26.5],[135.5,26.5],[135.5,27],[135,27],[135,26.5]]]}},{"type":"Feature","properties":{"zone_name":"277"},"geometry":{"type":"Polygon","coordinates":[[[135.5,26.5],[136,26.5],[136,27],[135.5,27],[135.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"278"},"geometry":{"type":"Polygon","coordinates":[[[136,26.5],[136.5,26.5],[136.5,27],[136,27],[136,26.5]]]}},{"type":"Feature","properties":{"zone_name":"279"},"geometry":{"type":"Polygon","coordinates":[[[136.5,26.5],[137,26.5],[137,27],[136.5,27],[136.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"280"},"geometry":{"type":"Polygon","coordinates":[[[137,26.5],[137.5,26.5],[137.5,27],[137,27],[137,26.5]]]}},{"type":"Feature","properties":{"zone_name":"281"},"geometry":{"type":"Polygon","coordinates":[[[137.5,26.5],[138,26.5],[138,27],[137.5,27],[137.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"282"},"geometry":{"type":"Polygon","coordinates":[[[135,26],[135.5,26],[135.5,26.5],[135,26.5],[135,26]]]}},{"type":"Feature","properties":{"zone_name":"283"},"geometry":{"type":"Polygon","coordinates":[[[135.5,26],[136,26],[136,26.5],[135.5,26.5],[135.5,26]]]}},{"type":"Feature","properties":{"zone_name":"284"},"geometry":{"type":"Polygon","coordinates":[[[136,26],[136.5,26],[136.5,26.5],[136,26.5],[136,26]]]}},{"type":"Feature","properties":{"zone_name":"285"},"geometry":{"type":"Polygon","coordinates":[[[136.5,26],[137,26],[137,26.5],[136.5,26.5],[136.5,26]]]}},{"type":"Feature","properties":{"zone_name":"286"},"geometry":{"type":"Polygon","coordinates":[[[137,26],[137.5,26],[137.5,26.5],[137,26.5],[137,26]]]}},{"type":"Feature","properties":{"zone_name":"287"},"geometry":{"type":"Polygon","coordinates":[[[137.5,26],[138,26],[138,26.5],[137.5,26.5],[137.5,26]]]}},{"type":"Feature","properties":{"zone_name":"288"},"geometry":{"type":"Polygon","coordinates":[[[135,25.5],[135.5,25.5],[135.5,26],[135,26],[135,25.5]]]}},{"type":"Feature","properties":{"zone_name":"289"},"geometry":{"type":"Polygon","coordinates":[[[135.5,25.5],[136,25.5],[136,26],[135.5,26],[135.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"290"},"geometry":{"type":"Polygon","coordinates":[[[136,25.5],[136.5,25.5],[136.5,26],[136,26],[136,25.5]]]}},{"type":"Feature","properties":{"zone_name":"291"},"geometry":{"type":"Polygon","coordinates":[[[136.5,25.5],[137,25.5],[137,26],[136.5,26],[136.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"292"},"geometry":{"type":"Polygon","coordinates":[[[137,25.5],[137.5,25.5],[137.5,26],[137,26],[137,25.5]]]}},{"type":"Feature","properties":{"zone_name":"293"},"geometry":{"type":"Polygon","coordinates":[[[137.5,25.5],[138,25.5],[138,26],[137.5,26],[137.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"294"},"geometry":{"type":"Polygon","coordinates":[[[135,25],[135.5,25],[135.5,25.5],[135,25.5],[135,25]]]}},{"type":"Feature","properties":{"zone_name":"295"},"geometry":{"type":"Polygon","coordinates":[[[135.5,25],[136,25],[136,25.5],[135.5,25.5],[135.5,25]]]}},{"type":"Feature","properties":{"zone_name":"296"},"geometry":{"type":"Polygon","coordinates":[[[136,25],[136.5,25],[136.5,25.5],[136,25.5],[136,25]]]}},{"type":"Feature","properties":{"zone_name":"297"},"geometry":{"type":"Polygon","coordinates":[[[136.5,25],[137,25],[137,25.5],[136.5,25.5],[136.5,25]]]}},{"type":"Feature","properties":{"zone_name":"298"},"geometry":{"type":"Polygon","coordinates":[[[137,25],[137.5,25],[137.5,25.5],[137,25.5],[137,25]]]}},{"type":"Feature","properties":{"zone_name":"299"},"geometry":{"type":"Polygon","coordinates":[[[137.5,25],[138,25],[138,25.5],[137.5,25.5],[137.5,25]]]}},{"type":"Feature","properties":{"zone_name":"302"},"geometry":{"type":"Polygon","coordinates":[[[132,42.5],[132.5,42.5],[132.5,43],[132,43],[132,42.5]]]}},{"type":"Feature","properties":{"zone_name":"303"},"geometry":{"type":"Polygon","coordinates":[[[132.5,42.5],[133,42.5],[133,43],[132.5,43],[132.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"304"},"geometry":{"type":"Polygon","coordinates":[[[133,42.5],[133.5,42.5],[133.5,43],[133,43],[133,42.5]]]}},{"type":"Feature","properties":{"zone_name":"305"},"geometry":{"type":"Polygon","coordinates":[[[133.5,42.5],[134,42.5],[134,43],[133.5,43],[133.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"306"},"geometry":{"type":"Polygon","coordinates":[[[132,42],[132.5,42],[132.5,42.5],[132,42.5],[132,42]]]}},{"type":"Feature","properties":{"zone_name":"307"},"geometry":{"type":"Polygon","coordinates":[[[132.5,42],[133,42],[133,42.5],[132.5,42.5],[132.5,42]]]}},{"type":"Feature","properties":{"zone_name":"308"},"geometry":{"type":"Polygon","coordinates":[[[133,42],[133.5,42],[133.5,42.5],[133,42.5],[133,42]]]}},{"type":"Feature","properties":{"zone_name":"309"},"geometry":{"type":"Polygon","coordinates":[[[133.5,42],[134,42],[134,42.5],[133.5,42.5],[133.5,42]]]}},{"type":"Feature","properties":{"zone_name":"310"},"geometry":{"type":"Polygon","coordinates":[[[132,41.5],[132.5,41.5],[132.5,42],[132,42],[132,41.5]]]}},{"type":"Feature","properties":{"zone_name":"311"},"geometry":{"type":"Polygon","coordinates":[[[132.5,41.5],[133,41.5],[133,42],[132.5,42],[132.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"312"},"geometry":{"type":"Polygon","coordinates":[[[133,41.5],[133.5,41.5],[133.5,42],[133,42],[133,41.5]]]}},{"type":"Feature","properties":{"zone_name":"313"},"geometry":{"type":"Polygon","coordinates":[[[133.5,41.5],[134,41.5],[134,42],[133.5,42],[133.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"314"},"geometry":{"type":"Polygon","coordinates":[[[132,41],[132.5,41],[132.5,41.5],[132,41.5],[132,41]]]}},{"type":"Feature","properties":{"zone_name":"315"},"geometry":{"type":"Polygon","coordinates":[[[132.5,41],[133,41],[133,41.5],[132.5,41.5],[132.5,41]]]}},{"type":"Feature","properties":{"zone_name":"316"},"geometry":{"type":"Polygon","coordinates":[[[133,41],[133.5,41],[133.5,41.5],[133,41.5],[133,41]]]}},{"type":"Feature","properties":{"zone_name":"317"},"geometry":{"type":"Polygon","coordinates":[[[133.5,41],[134,41],[134,41.5],[133.5,41.5],[133.5,41]]]}},{"type":"Feature","properties":{"zone_name":"318"},"geometry":{"type":"Polygon","coordinates":[[[132,40.5],[132.5,40.5],[132.5,41],[132,41],[132,40.5]]]}},{"type":"Feature","properties":{"zone_name":"319"},"geometry":{"type":"Polygon","coordinates":[[[132.5,40.5],[133,40.5],[133,41],[132.5,41],[132.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"320"},"geometry":{"type":"Polygon","coordinates":[[[133,40.5],[133.5,40.5],[133.5,41],[133,41],[133,40.5]]]}},{"type":"Feature","properties":{"zone_name":"321"},"geometry":{"type":"Polygon","coordinates":[[[133.5,40.5],[134,40.5],[134,41],[133.5,41],[133.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"322"},"geometry":{"type":"Polygon","coordinates":[[[132,40],[132.5,40],[132.5,40.5],[132,40.5],[132,40]]]}},{"type":"Feature","properties":{"zone_name":"323"},"geometry":{"type":"Polygon","coordinates":[[[132.5,40],[133,40],[133,40.5],[132.5,40.5],[132.5,40]]]}},{"type":"Feature","properties":{"zone_name":"324"},"geometry":{"type":"Polygon","coordinates":[[[133,40],[133.5,40],[133.5,40.5],[133,40.5],[133,40]]]}},{"type":"Feature","properties":{"zone_name":"325"},"geometry":{"type":"Polygon","coordinates":[[[133.5,40],[134,40],[134,40.5],[133.5,40.5],[133.5,40]]]}},{"type":"Feature","properties":{"zone_name":"326"},"geometry":{"type":"Polygon","coordinates":[[[132,39.5],[132.5,39.5],[132.5,40],[132,40],[132,39.5]]]}},{"type":"Feature","properties":{"zone_name":"327"},"geometry":{"type":"Polygon","coordinates":[[[132.5,39.5],[133,39.5],[133,40],[132.5,40],[132.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"328"},"geometry":{"type":"Polygon","coordinates":[[[133,39.5],[133.5,39.5],[133.5,40],[133,40],[133,39.5]]]}},{"type":"Feature","properties":{"zone_name":"329"},"geometry":{"type":"Polygon","coordinates":[[[133.5,39.5],[134,39.5],[134,40],[133.5,40],[133.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"330"},"geometry":{"type":"Polygon","coordinates":[[[132,39],[132.5,39],[132.5,39.5],[132,39.5],[132,39]]]}},{"type":"Feature","properties":{"zone_name":"331"},"geometry":{"type":"Polygon","coordinates":[[[132.5,39],[133,39],[133,39.5],[132.5,39.5],[132.5,39]]]}},{"type":"Feature","properties":{"zone_name":"332"},"geometry":{"type":"Polygon","coordinates":[[[133,39],[133.5,39],[133.5,39.5],[133,39.5],[133,39]]]}},{"type":"Feature","properties":{"zone_name":"333"},"geometry":{"type":"Polygon","coordinates":[[[133.5,39],[134,39],[134,39.5],[133.5,39.5],[133.5,39]]]}},{"type":"Feature","properties":{"zone_name":"334"},"geometry":{"type":"Polygon","coordinates":[[[132,38.5],[132.5,38.5],[132.5,39],[132,39],[132,38.5]]]}},{"type":"Feature","properties":{"zone_name":"335"},"geometry":{"type":"Polygon","coordinates":[[[132.5,38.5],[133,38.5],[133,39],[132.5,39],[132.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"336"},"geometry":{"type":"Polygon","coordinates":[[[133,38.5],[133.5,38.5],[133.5,39],[133,39],[133,38.5]]]}},{"type":"Feature","properties":{"zone_name":"337"},"geometry":{"type":"Polygon","coordinates":[[[133.5,38.5],[134,38.5],[134,39],[133.5,39],[133.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"338"},"geometry":{"type":"Polygon","coordinates":[[[132,38],[132.5,38],[132.5,38.5],[132,38.5],[132,38]]]}},{"type":"Feature","properties":{"zone_name":"339"},"geometry":{"type":"Polygon","coordinates":[[[132.5,38],[133,38],[133,38.5],[132.5,38.5],[132.5,38]]]}},{"type":"Feature","properties":{"zone_name":"340"},"geometry":{"type":"Polygon","coordinates":[[[133,38],[133.5,38],[133.5,38.5],[133,38.5],[133,38]]]}},{"type":"Feature","properties":{"zone_name":"341"},"geometry":{"type":"Polygon","coordinates":[[[133.5,38],[134,38],[134,38.5],[133.5,38.5],[133.5,38]]]}},{"type":"Feature","properties":{"zone_name":"342"},"geometry":{"type":"Polygon","coordinates":[[[132,37.5],[132.5,37.5],[132.5,38],[132,38],[132,37.5]]]}},{"type":"Feature","properties":{"zone_name":"343"},"geometry":{"type":"Polygon","coordinates":[[[132.5,37.5],[133,37.5],[133,38],[132.5,38],[132.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"344"},"geometry":{"type":"Polygon","coordinates":[[[133,37.5],[133.5,37.5],[133.5,38],[133,38],[133,37.5]]]}},{"type":"Feature","properties":{"zone_name":"345"},"geometry":{"type":"Polygon","coordinates":[[[133.5,37.5],[134,37.5],[134,38],[133.5,38],[133.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"346"},"geometry":{"type":"Polygon","coordinates":[[[132,37],[132.5,37],[132.5,37.5],[132,37.5],[132,37]]]}},{"type":"Feature","properties":{"zone_name":"347"},"geometry":{"type":"Polygon","coordinates":[[[132.5,37],[133,37],[133,37.5],[132.5,37.5],[132.5,37]]]}},{"type":"Feature","properties":{"zone_name":"348"},"geometry":{"type":"Polygon","coordinates":[[[133,37],[133.5,37],[133.5,37.5],[133,37.5],[133,37]]]}},{"type":"Feature","properties":{"zone_name":"349"},"geometry":{"type":"Polygon","coordinates":[[[133.5,37],[134,37],[134,37.5],[133.5,37.5],[133.5,37]]]}},{"type":"Feature","properties":{"zone_name":"350"},"geometry":{"type":"Polygon","coordinates":[[[132,36.5],[132.5,36.5],[132.5,37],[132,37],[132,36.5]]]}},{"type":"Feature","properties":{"zone_name":"351"},"geometry":{"type":"Polygon","coordinates":[[[132.5,36.5],[133,36.5],[133,37],[132.5,37],[132.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"352"},"geometry":{"type":"Polygon","coordinates":[[[133,36.5],[133.5,36.5],[133.5,37],[133,37],[133,36.5]]]}},{"type":"Feature","properties":{"zone_name":"353"},"geometry":{"type":"Polygon","coordinates":[[[133.5,36.5],[134,36.5],[134,37],[133.5,37],[133.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"354"},"geometry":{"type":"Polygon","coordinates":[[[132,36],[132.5,36],[132.5,36.5],[132,36.5],[132,36]]]}},{"type":"Feature","properties":{"zone_name":"355"},"geometry":{"type":"Polygon","coordinates":[[[132.5,36],[133,36],[133,36.5],[132.5,36.5],[132.5,36]]]}},{"type":"Feature","properties":{"zone_name":"356"},"geometry":{"type":"Polygon","coordinates":[[[133,36],[133.5,36],[133.5,36.5],[133,36.5],[133,36]]]}},{"type":"Feature","properties":{"zone_name":"357"},"geometry":{"type":"Polygon","coordinates":[[[133.5,36],[134,36],[134,36.5],[133.5,36.5],[133.5,36]]]}},{"type":"Feature","properties":{"zone_name":"358"},"geometry":{"type":"Polygon","coordinates":[[[132,35.5],[132.5,35.5],[132.5,36],[132,36],[132,35.5]]]}},{"type":"Feature","properties":{"zone_name":"359"},"geometry":{"type":"Polygon","coordinates":[[[132.5,35.5],[133,35.5],[133,36],[132.5,36],[132.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"360"},"geometry":{"type":"Polygon","coordinates":[[[133,35.5],[133.5,35.5],[133.5,36],[133,36],[133,35.5]]]}},{"type":"Feature","properties":{"zone_name":"361"},"geometry":{"type":"Polygon","coordinates":[[[133.5,35.5],[134,35.5],[134,36],[133.5,36],[133.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"362"},"geometry":{"type":"Polygon","coordinates":[[[131.5,35],[132,35],[132,35.5],[131.5,35.5],[131.5,35]]]}},{"type":"Feature","properties":{"zone_name":"363"},"geometry":{"type":"Polygon","coordinates":[[[132,35],[132.5,35],[132.5,35.5],[132,35.5],[132,35]]]}},{"type":"Feature","properties":{"zone_name":"365"},"geometry":{"type":"Polygon","coordinates":[[[131,34.5],[131.5,34.5],[131.5,35],[131,35],[131,34.5]]]}},{"type":"Feature","properties":{"zone_name":"366"},"geometry":{"type":"Polygon","coordinates":[[[131.5,34.5],[132,34.5],[132,35],[131.5,35],[131.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"367"},"geometry":{"type":"Polygon","coordinates":[[[130.5,34],[131,34],[131,34.5],[130.5,34.5],[130.5,34]]]}},{"type":"Feature","properties":{"zone_name":"368"},"geometry":{"type":"Polygon","coordinates":[[[129,33.5],[129.5,33.5],[129.5,34],[129,34],[129,33.5]]]}},{"type":"Feature","properties":{"zone_name":"369"},"geometry":{"type":"Polygon","coordinates":[[[129.5,33.5],[130,33.5],[130,34],[129.5,34],[129.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"370"},"geometry":{"type":"Polygon","coordinates":[[[130,33.5],[130.5,33.5],[130.5,34],[130,34],[130,33.5]]]}},{"type":"Feature","properties":{"zone_name":"371"},"geometry":{"type":"Polygon","coordinates":[[[128.5,33],[129,33],[129,33.5],[128.5,33.5],[128.5,33]]]}},{"type":"Feature","properties":{"zone_name":"372"},"geometry":{"type":"Polygon","coordinates":[[[129,33],[129.5,33],[129.5,33.5],[129,33.5],[129,33]]]}},{"type":"Feature","properties":{"zone_name":"374"},"geometry":{"type":"Polygon","coordinates":[[[128,32.5],[128.5,32.5],[128.5,33],[128,33],[128,32.5]]]}},{"type":"Feature","properties":{"zone_name":"375"},"geometry":{"type":"Polygon","coordinates":[[[128.5,32.5],[129,32.5],[129,33],[128.5,33],[128.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"376"},"geometry":{"type":"Polygon","coordinates":[[[129,32.5],[129.5,32.5],[129.5,33],[129,33],[129,32.5]]]}},{"type":"Feature","properties":{"zone_name":"377"},"geometry":{"type":"Polygon","coordinates":[[[129.5,32.5],[130,32.5],[130,33],[129.5,33],[129.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"378"},"geometry":{"type":"Polygon","coordinates":[[[127.5,32],[128,32],[128,32.5],[127.5,32.5],[127.5,32]]]}},{"type":"Feature","properties":{"zone_name":"379"},"geometry":{"type":"Polygon","coordinates":[[[128,32],[128.5,32],[128.5,32.5],[128,32.5],[128,32]]]}},{"type":"Feature","properties":{"zone_name":"380"},"geometry":{"type":"Polygon","coordinates":[[[128.5,32],[129,32],[129,32.5],[128.5,32.5],[128.5,32]]]}},{"type":"Feature","properties":{"zone_name":"381"},"geometry":{"type":"Polygon","coordinates":[[[129,32],[129.5,32],[129.5,32.5],[129,32.5],[129,32]]]}},{"type":"Feature","properties":{"zone_name":"382"},"geometry":{"type":"Polygon","coordinates":[[[129.5,32],[130,32],[130,32.5],[129.5,32.5],[129.5,32]]]}},{"type":"Feature","properties":{"zone_name":"384"},"geometry":{"type":"Polygon","coordinates":[[[133.5,34.5],[134,34.5],[134,35],[133.5,35],[133.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"385"},"geometry":{"type":"Polygon","coordinates":[[[134,34.5],[134.5,34.5],[134.5,35],[134,35],[134,34.5]]]}},{"type":"Feature","properties":{"zone_name":"386"},"geometry":{"type":"Polygon","coordinates":[[[134.5,34.5],[135,34.5],[135,35],[134.5,35],[134.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"387"},"geometry":{"type":"Polygon","coordinates":[[[135,34.5],[135.5,34.5],[135.5,35],[135,35],[135,34.5]]]}},{"type":"Feature","properties":{"zone_name":"388"},"geometry":{"type":"Polygon","coordinates":[[[136.5,34.5],[137,34.5],[137,35],[136.5,35],[136.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"389"},"geometry":{"type":"Polygon","coordinates":[[[137,34.5],[137.5,34.5],[137.5,35],[137,35],[137,34.5]]]}},{"type":"Feature","properties":{"zone_name":"390"},"geometry":{"type":"Polygon","coordinates":[[[137.5,34.5],[138,34.5],[138,35],[137.5,35],[137.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"391"},"geometry":{"type":"Polygon","coordinates":[[[132,34],[132.5,34],[132.5,34.5],[132,34.5],[132,34]]]}},{"type":"Feature","properties":{"zone_name":"392"},"geometry":{"type":"Polygon","coordinates":[[[132.5,34],[133,34],[133,34.5],[132.5,34.5],[132.5,34]]]}},{"type":"Feature","properties":{"zone_name":"393"},"geometry":{"type":"Polygon","coordinates":[[[133,34],[133.5,34],[133.5,34.5],[133,34.5],[133,34]]]}},{"type":"Feature","properties":{"zone_name":"394"},"geometry":{"type":"Polygon","coordinates":[[[133.5,34],[134,34],[134,34.5],[133.5,34.5],[133.5,34]]]}},{"type":"Feature","properties":{"zone_name":"395"},"geometry":{"type":"Polygon","coordinates":[[[134,34],[134.5,34],[134.5,34.5],[134,34.5],[134,34]]]}},{"type":"Feature","properties":{"zone_name":"396"},"geometry":{"type":"Polygon","coordinates":[[[134.5,34],[135,34],[135,34.5],[134.5,34.5],[134.5,34]]]}},{"type":"Feature","properties":{"zone_name":"397"},"geometry":{"type":"Polygon","coordinates":[[[135,34],[135.5,34],[135.5,34.5],[135,34.5],[135,34]]]}},{"type":"Feature","properties":{"zone_name":"398"},"geometry":{"type":"Polygon","coordinates":[[[136,34],[136.5,34],[136.5,34.5],[136,34.5],[136,34]]]}},{"type":"Feature","properties":{"zone_name":"399"},"geometry":{"type":"Polygon","coordinates":[[[136.5,34],[137,34],[137,34.5],[136.5,34.5],[136.5,34]]]}},{"type":"Feature","properties":{"zone_name":"400"},"geometry":{"type":"Polygon","coordinates":[[[137,34],[137.5,34],[137.5,34.5],[137,34.5],[137,34]]]}},{"type":"Feature","properties":{"zone_name":"401"},"geometry":{"type":"Polygon","coordinates":[[[121.5,34],[122,34],[122,34.5],[121.5,34.5],[121.5,34]]]}},{"type":"Feature","properties":{"zone_name":"402"},"geometry":{"type":"Polygon","coordinates":[[[122,34],[122.5,34],[122.5,34.5],[122,34.5],[122,34]]]}},{"type":"Feature","properties":{"zone_name":"403"},"geometry":{"type":"Polygon","coordinates":[[[121.5,33.5],[122,33.5],[122,34],[121.5,34],[121.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"404"},"geometry":{"type":"Polygon","coordinates":[[[122,33.5],[122.5,33.5],[122.5,34],[122,34],[122,33.5]]]}},{"type":"Feature","properties":{"zone_name":"405"},"geometry":{"type":"Polygon","coordinates":[[[121.5,33],[122,33],[122,33.5],[121.5,33.5],[121.5,33]]]}},{"type":"Feature","properties":{"zone_name":"406"},"geometry":{"type":"Polygon","coordinates":[[[122,33],[122.5,33],[122.5,33.5],[122,33.5],[122,33]]]}},{"type":"Feature","properties":{"zone_name":"407"},"geometry":{"type":"Polygon","coordinates":[[[122,32.5],[122.5,32.5],[122.5,33],[122,33],[122,32.5]]]}},{"type":"Feature","properties":{"zone_name":"408"},"geometry":{"type":"Polygon","coordinates":[[[122,32],[122.5,32],[122.5,32.5],[122,32.5],[122,32]]]}},{"type":"Feature","properties":{"zone_name":"409"},"geometry":{"type":"Polygon","coordinates":[[[137.5,34],[138,34],[138,34.5],[137.5,34.5],[137.5,34]]]}},{"type":"Feature","properties":{"zone_name":"410"},"geometry":{"type":"Polygon","coordinates":[[[131.5,34],[132,34],[132,34.5],[131.5,34.5],[131.5,34]]]}},{"type":"Feature","properties":{"zone_name":"411"},"geometry":{"type":"Polygon","coordinates":[[[131,33.5],[131.5,33.5],[131.5,34],[131,34],[131,33.5]]]}},{"type":"Feature","properties":{"zone_name":"412"},"geometry":{"type":"Polygon","coordinates":[[[131.5,33.5],[132,33.5],[132,34],[131.5,34],[131.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"413"},"geometry":{"type":"Polygon","coordinates":[[[132,33.5],[132.5,33.5],[132.5,34],[132,34],[132,33.5]]]}},{"type":"Feature","properties":{"zone_name":"414"},"geometry":{"type":"Polygon","coordinates":[[[132.5,33.5],[133,33.5],[133,34],[132.5,34],[132.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"415"},"geometry":{"type":"Polygon","coordinates":[[[134.5,33.5],[135,33.5],[135,34],[134.5,34],[134.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"416"},"geometry":{"type":"Polygon","coordinates":[[[135,33.5],[135.5,33.5],[135.5,34],[135,34],[135,33.5]]]}},{"type":"Feature","properties":{"zone_name":"417"},"geometry":{"type":"Polygon","coordinates":[[[136,33.5],[136.5,33.5],[136.5,34],[136,34],[136,33.5]]]}},{"type":"Feature","properties":{"zone_name":"418"},"geometry":{"type":"Polygon","coordinates":[[[136.5,33.5],[137,33.5],[137,34],[136.5,34],[136.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"419"},"geometry":{"type":"Polygon","coordinates":[[[137,33.5],[137.5,33.5],[137.5,34],[137,34],[137,33.5]]]}},{"type":"Feature","properties":{"zone_name":"420"},"geometry":{"type":"Polygon","coordinates":[[[137.5,33.5],[138,33.5],[138,34],[137.5,34],[137.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"421"},"geometry":{"type":"Polygon","coordinates":[[[132,33],[132.5,33],[132.5,33.5],[132,33.5],[132,33]]]}},{"type":"Feature","properties":{"zone_name":"422"},"geometry":{"type":"Polygon","coordinates":[[[133.5,33],[134,33],[134,33.5],[133.5,33.5],[133.5,33]]]}},{"type":"Feature","properties":{"zone_name":"423"},"geometry":{"type":"Polygon","coordinates":[[[134,33],[134.5,33],[134.5,33.5],[134,33.5],[134,33]]]}},{"type":"Feature","properties":{"zone_name":"424"},"geometry":{"type":"Polygon","coordinates":[[[134.5,33],[135,33],[135,33.5],[134.5,33.5],[134.5,33]]]}},{"type":"Feature","properties":{"zone_name":"425"},"geometry":{"type":"Polygon","coordinates":[[[135,33],[135.5,33],[135.5,33.5],[135,33.5],[135,33]]]}},{"type":"Feature","properties":{"zone_name":"426"},"geometry":{"type":"Polygon","coordinates":[[[135.5,33],[136,33],[136,33.5],[135.5,33.5],[135.5,33]]]}},{"type":"Feature","properties":{"zone_name":"427"},"geometry":{"type":"Polygon","coordinates":[[[136,33],[136.5,33],[136.5,33.5],[136,33.5],[136,33]]]}},{"type":"Feature","properties":{"zone_name":"428"},"geometry":{"type":"Polygon","coordinates":[[[136.5,33],[137,33],[137,33.5],[136.5,33.5],[136.5,33]]]}},{"type":"Feature","properties":{"zone_name":"429"},"geometry":{"type":"Polygon","coordinates":[[[137,33],[137.5,33],[137.5,33.5],[137,33.5],[137,33]]]}},{"type":"Feature","properties":{"zone_name":"430"},"geometry":{"type":"Polygon","coordinates":[[[137.5,33],[138,33],[138,33.5],[137.5,33.5],[137.5,33]]]}},{"type":"Feature","properties":{"zone_name":"432"},"geometry":{"type":"Polygon","coordinates":[[[132,32.5],[132.5,32.5],[132.5,33],[132,33],[132,32.5]]]}},{"type":"Feature","properties":{"zone_name":"433"},"geometry":{"type":"Polygon","coordinates":[[[132.5,32.5],[133,32.5],[133,33],[132.5,33],[132.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"434"},"geometry":{"type":"Polygon","coordinates":[[[133,32.5],[133.5,32.5],[133.5,33],[133,33],[133,32.5]]]}},{"type":"Feature","properties":{"zone_name":"435"},"geometry":{"type":"Polygon","coordinates":[[[133.5,32.5],[134,32.5],[134,33],[133.5,33],[133.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"436"},"geometry":{"type":"Polygon","coordinates":[[[134,32.5],[134.5,32.5],[134.5,33],[134,33],[134,32.5]]]}},{"type":"Feature","properties":{"zone_name":"437"},"geometry":{"type":"Polygon","coordinates":[[[134.5,32.5],[135,32.5],[135,33],[134.5,33],[134.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"438"},"geometry":{"type":"Polygon","coordinates":[[[135,32.5],[135.5,32.5],[135.5,33],[135,33],[135,32.5]]]}},{"type":"Feature","properties":{"zone_name":"439"},"geometry":{"type":"Polygon","coordinates":[[[135.5,32.5],[136,32.5],[136,33],[135.5,33],[135.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"440"},"geometry":{"type":"Polygon","coordinates":[[[136,32.5],[136.5,32.5],[136.5,33],[136,33],[136,32.5]]]}},{"type":"Feature","properties":{"zone_name":"441"},"geometry":{"type":"Polygon","coordinates":[[[136.5,32.5],[137,32.5],[137,33],[136.5,33],[136.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"442"},"geometry":{"type":"Polygon","coordinates":[[[137,32.5],[137.5,32.5],[137.5,33],[137,33],[137,32.5]]]}},{"type":"Feature","properties":{"zone_name":"443"},"geometry":{"type":"Polygon","coordinates":[[[137.5,32.5],[138,32.5],[138,33],[137.5,33],[137.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"444"},"geometry":{"type":"Polygon","coordinates":[[[131.5,32],[132,32],[132,32.5],[131.5,32.5],[131.5,32]]]}},{"type":"Feature","properties":{"zone_name":"445"},"geometry":{"type":"Polygon","coordinates":[[[132,32],[132.5,32],[132.5,32.5],[132,32.5],[132,32]]]}},{"type":"Feature","properties":{"zone_name":"446"},"geometry":{"type":"Polygon","coordinates":[[[132.5,32],[133,32],[133,32.5],[132.5,32.5],[132.5,32]]]}},{"type":"Feature","properties":{"zone_name":"447"},"geometry":{"type":"Polygon","coordinates":[[[133,32],[133.5,32],[133.5,32.5],[133,32.5],[133,32]]]}},{"type":"Feature","properties":{"zone_name":"448"},"geometry":{"type":"Polygon","coordinates":[[[133.5,32],[134,32],[134,32.5],[133.5,32.5],[133.5,32]]]}},{"type":"Feature","properties":{"zone_name":"449"},"geometry":{"type":"Polygon","coordinates":[[[134,32],[134.5,32],[134.5,32.5],[134,32.5],[134,32]]]}},{"type":"Feature","properties":{"zone_name":"450"},"geometry":{"type":"Polygon","coordinates":[[[134.5,32],[135,32],[135,32.5],[134.5,32.5],[134.5,32]]]}},{"type":"Feature","properties":{"zone_name":"451"},"geometry":{"type":"Polygon","coordinates":[[[135,32],[135.5,32],[135.5,32.5],[135,32.5],[135,32]]]}},{"type":"Feature","properties":{"zone_name":"452"},"geometry":{"type":"Polygon","coordinates":[[[135.5,32],[136,32],[136,32.5],[135.5,32.5],[135.5,32]]]}},{"type":"Feature","properties":{"zone_name":"453"},"geometry":{"type":"Polygon","coordinates":[[[136,32],[136.5,32],[136.5,32.5],[136,32.5],[136,32]]]}},{"type":"Feature","properties":{"zone_name":"454"},"geometry":{"type":"Polygon","coordinates":[[[136.5,32],[137,32],[137,32.5],[136.5,32.5],[136.5,32]]]}},{"type":"Feature","properties":{"zone_name":"455"},"geometry":{"type":"Polygon","coordinates":[[[137,32],[137.5,32],[137.5,32.5],[137,32.5],[137,32]]]}},{"type":"Feature","properties":{"zone_name":"456"},"geometry":{"type":"Polygon","coordinates":[[[137.5,32],[138,32],[138,32.5],[137.5,32.5],[137.5,32]]]}},{"type":"Feature","properties":{"zone_name":"457"},"geometry":{"type":"Polygon","coordinates":[[[122,31.5],[122.5,31.5],[122.5,32],[122,32],[122,31.5]]]}},{"type":"Feature","properties":{"zone_name":"458"},"geometry":{"type":"Polygon","coordinates":[[[122.5,31.5],[123,31.5],[123,32],[122.5,32],[122.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"459"},"geometry":{"type":"Polygon","coordinates":[[[123,31.5],[123.5,31.5],[123.5,32],[123,32],[123,31.5]]]}},{"type":"Feature","properties":{"zone_name":"460"},"geometry":{"type":"Polygon","coordinates":[[[123.5,31.5],[124,31.5],[124,32],[123.5,32],[123.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"461"},"geometry":{"type":"Polygon","coordinates":[[[124,31.5],[124.5,31.5],[124.5,32],[124,32],[124,31.5]]]}},{"type":"Feature","properties":{"zone_name":"462"},"geometry":{"type":"Polygon","coordinates":[[[124.5,31.5],[125,31.5],[125,32],[124.5,32],[124.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"463"},"geometry":{"type":"Polygon","coordinates":[[[125,31.5],[125.5,31.5],[125.5,32],[125,32],[125,31.5]]]}},{"type":"Feature","properties":{"zone_name":"464"},"geometry":{"type":"Polygon","coordinates":[[[125.5,31.5],[126,31.5],[126,32],[125.5,32],[125.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"465"},"geometry":{"type":"Polygon","coordinates":[[[126,31.5],[126.5,31.5],[126.5,32],[126,32],[126,31.5]]]}},{"type":"Feature","properties":{"zone_name":"466"},"geometry":{"type":"Polygon","coordinates":[[[126.5,31.5],[127,31.5],[127,32],[126.5,32],[126.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"467"},"geometry":{"type":"Polygon","coordinates":[[[127,31.5],[127.5,31.5],[127.5,32],[127,32],[127,31.5]]]}},{"type":"Feature","properties":{"zone_name":"468"},"geometry":{"type":"Polygon","coordinates":[[[127.5,31.5],[128,31.5],[128,32],[127.5,32],[127.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"469"},"geometry":{"type":"Polygon","coordinates":[[[128,31.5],[128.5,31.5],[128.5,32],[128,32],[128,31.5]]]}},{"type":"Feature","properties":{"zone_name":"470"},"geometry":{"type":"Polygon","coordinates":[[[128.5,31.5],[129,31.5],[129,32],[128.5,32],[128.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"471"},"geometry":{"type":"Polygon","coordinates":[[[129,31.5],[129.5,31.5],[129.5,32],[129,32],[129,31.5]]]}},{"type":"Feature","properties":{"zone_name":"472"},"geometry":{"type":"Polygon","coordinates":[[[129.5,31.5],[130,31.5],[130,32],[129.5,32],[129.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"474"},"geometry":{"type":"Polygon","coordinates":[[[131.5,31.5],[132,31.5],[132,32],[131.5,32],[131.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"475"},"geometry":{"type":"Polygon","coordinates":[[[132,31.5],[132.5,31.5],[132.5,32],[132,32],[132,31.5]]]}},{"type":"Feature","properties":{"zone_name":"476"},"geometry":{"type":"Polygon","coordinates":[[[132.5,31.5],[133,31.5],[133,32],[132.5,32],[132.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"477"},"geometry":{"type":"Polygon","coordinates":[[[133,31.5],[133.5,31.5],[133.5,32],[133,32],[133,31.5]]]}},{"type":"Feature","properties":{"zone_name":"478"},"geometry":{"type":"Polygon","coordinates":[[[133.5,31.5],[134,31.5],[134,32],[133.5,32],[133.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"479"},"geometry":{"type":"Polygon","coordinates":[[[134,31.5],[134.5,31.5],[134.5,32],[134,32],[134,31.5]]]}},{"type":"Feature","properties":{"zone_name":"480"},"geometry":{"type":"Polygon","coordinates":[[[134.5,31.5],[135,31.5],[135,32],[134.5,32],[134.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"481"},"geometry":{"type":"Polygon","coordinates":[[[135,31.5],[135.5,31.5],[135.5,32],[135,32],[135,31.5]]]}},{"type":"Feature","properties":{"zone_name":"482"},"geometry":{"type":"Polygon","coordinates":[[[135.5,31.5],[136,31.5],[136,32],[135.5,32],[135.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"483"},"geometry":{"type":"Polygon","coordinates":[[[136,31.5],[136.5,31.5],[136.5,32],[136,32],[136,31.5]]]}},{"type":"Feature","properties":{"zone_name":"484"},"geometry":{"type":"Polygon","coordinates":[[[136.5,31.5],[137,31.5],[137,32],[136.5,32],[136.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"485"},"geometry":{"type":"Polygon","coordinates":[[[137,31.5],[137.5,31.5],[137.5,32],[137,32],[137,31.5]]]}},{"type":"Feature","properties":{"zone_name":"486"},"geometry":{"type":"Polygon","coordinates":[[[137.5,31.5],[138,31.5],[138,32],[137.5,32],[137.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"487"},"geometry":{"type":"Polygon","coordinates":[[[122,31],[122.5,31],[122.5,31.5],[122,31.5],[122,31]]]}},{"type":"Feature","properties":{"zone_name":"488"},"geometry":{"type":"Polygon","coordinates":[[[122.5,31],[123,31],[123,31.5],[122.5,31.5],[122.5,31]]]}},{"type":"Feature","properties":{"zone_name":"489"},"geometry":{"type":"Polygon","coordinates":[[[123,31],[123.5,31],[123.5,31.5],[123,31.5],[123,31]]]}},{"type":"Feature","properties":{"zone_name":"490"},"geometry":{"type":"Polygon","coordinates":[[[123.5,31],[124,31],[124,31.5],[123.5,31.5],[123.5,31]]]}},{"type":"Feature","properties":{"zone_name":"491"},"geometry":{"type":"Polygon","coordinates":[[[124,31],[124.5,31],[124.5,31.5],[124,31.5],[124,31]]]}},{"type":"Feature","properties":{"zone_name":"492"},"geometry":{"type":"Polygon","coordinates":[[[124.5,31],[125,31],[125,31.5],[124.5,31.5],[124.5,31]]]}},{"type":"Feature","properties":{"zone_name":"493"},"geometry":{"type":"Polygon","coordinates":[[[125,31],[125.5,31],[125.5,31.5],[125,31.5],[125,31]]]}},{"type":"Feature","properties":{"zone_name":"494"},"geometry":{"type":"Polygon","coordinates":[[[125.5,31],[126,31],[126,31.5],[125.5,31.5],[125.5,31]]]}},{"type":"Feature","properties":{"zone_name":"495"},"geometry":{"type":"Polygon","coordinates":[[[126,31],[126.5,31],[126.5,31.5],[126,31.5],[126,31]]]}},{"type":"Feature","properties":{"zone_name":"496"},"geometry":{"type":"Polygon","coordinates":[[[126.5,31],[127,31],[127,31.5],[126.5,31.5],[126.5,31]]]}},{"type":"Feature","properties":{"zone_name":"497"},"geometry":{"type":"Polygon","coordinates":[[[127,31],[127.5,31],[127.5,31.5],[127,31.5],[127,31]]]}},{"type":"Feature","properties":{"zone_name":"498"},"geometry":{"type":"Polygon","coordinates":[[[127.5,31],[128,31],[128,31.5],[127.5,31.5],[127.5,31]]]}},{"type":"Feature","properties":{"zone_name":"499"},"geometry":{"type":"Polygon","coordinates":[[[128,31],[128.5,31],[128.5,31.5],[128,31.5],[128,31]]]}},{"type":"Feature","properties":{"zone_name":"500"},"geometry":{"type":"Polygon","coordinates":[[[128.5,31],[129,31],[129,31.5],[128.5,31.5],[128.5,31]]]}},{"type":"Feature","properties":{"zone_name":"501"},"geometry":{"type":"Polygon","coordinates":[[[129,31],[129.5,31],[129.5,31.5],[129,31.5],[129,31]]]}},{"type":"Feature","properties":{"zone_name":"502"},"geometry":{"type":"Polygon","coordinates":[[[129.5,31],[130,31],[130,31.5],[129.5,31.5],[129.5,31]]]}},{"type":"Feature","properties":{"zone_name":"503"},"geometry":{"type":"Polygon","coordinates":[[[130,31],[130.5,31],[130.5,31.5],[130,31.5],[130,31]]]}},{"type":"Feature","properties":{"zone_name":"504"},"geometry":{"type":"Polygon","coordinates":[[[130.5,31],[131,31],[131,31.5],[130.5,31.5],[130.5,31]]]}},{"type":"Feature","properties":{"zone_name":"505"},"geometry":{"type":"Polygon","coordinates":[[[131,31],[131.5,31],[131.5,31.5],[131,31.5],[131,31]]]}},{"type":"Feature","properties":{"zone_name":"506"},"geometry":{"type":"Polygon","coordinates":[[[131.5,31],[132,31],[132,31.5],[131.5,31.5],[131.5,31]]]}},{"type":"Feature","properties":{"zone_name":"507"},"geometry":{"type":"Polygon","coordinates":[[[132,31],[132.5,31],[132.5,31.5],[132,31.5],[132,31]]]}},{"type":"Feature","properties":{"zone_name":"508"},"geometry":{"type":"Polygon","coordinates":[[[132.5,31],[133,31],[133,31.5],[132.5,31.5],[132.5,31]]]}},{"type":"Feature","properties":{"zone_name":"509"},"geometry":{"type":"Polygon","coordinates":[[[133,31],[133.5,31],[133.5,31.5],[133,31.5],[133,31]]]}},{"type":"Feature","properties":{"zone_name":"510"},"geometry":{"type":"Polygon","coordinates":[[[133.5,31],[134,31],[134,31.5],[133.5,31.5],[133.5,31]]]}},{"type":"Feature","properties":{"zone_name":"511"},"geometry":{"type":"Polygon","coordinates":[[[134,31],[134.5,31],[134.5,31.5],[134,31.5],[134,31]]]}},{"type":"Feature","properties":{"zone_name":"512"},"geometry":{"type":"Polygon","coordinates":[[[134.5,31],[135,31],[135,31.5],[134.5,31.5],[134.5,31]]]}},{"type":"Feature","properties":{"zone_name":"513"},"geometry":{"type":"Polygon","coordinates":[[[135,31],[135.5,31],[135.5,31.5],[135,31.5],[135,31]]]}},{"type":"Feature","properties":{"zone_name":"514"},"geometry":{"type":"Polygon","coordinates":[[[135.5,31],[136,31],[136,31.5],[135.5,31.5],[135.5,31]]]}},{"type":"Feature","properties":{"zone_name":"515"},"geometry":{"type":"Polygon","coordinates":[[[136,31],[136.5,31],[136.5,31.5],[136,31.5],[136,31]]]}},{"type":"Feature","properties":{"zone_name":"516"},"geometry":{"type":"Polygon","coordinates":[[[136.5,31],[137,31],[137,31.5],[136.5,31.5],[136.5,31]]]}},{"type":"Feature","properties":{"zone_name":"517"},"geometry":{"type":"Polygon","coordinates":[[[137,31],[137.5,31],[137.5,31.5],[137,31.5],[137,31]]]}},{"type":"Feature","properties":{"zone_name":"518"},"geometry":{"type":"Polygon","coordinates":[[[137.5,31],[138,31],[138,31.5],[137.5,31.5],[137.5,31]]]}},{"type":"Feature","properties":{"zone_name":"519"},"geometry":{"type":"Polygon","coordinates":[[[122,30.5],[122.5,30.5],[122.5,31],[122,31],[122,30.5]]]}},{"type":"Feature","properties":{"zone_name":"520"},"geometry":{"type":"Polygon","coordinates":[[[122.5,30.5],[123,30.5],[123,31],[122.5,31],[122.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"521"},"geometry":{"type":"Polygon","coordinates":[[[123,30.5],[123.5,30.5],[123.5,31],[123,31],[123,30.5]]]}},{"type":"Feature","properties":{"zone_name":"522"},"geometry":{"type":"Polygon","coordinates":[[[123.5,30.5],[124,30.5],[124,31],[123.5,31],[123.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"523"},"geometry":{"type":"Polygon","coordinates":[[[124,30.5],[124.5,30.5],[124.5,31],[124,31],[124,30.5]]]}},{"type":"Feature","properties":{"zone_name":"524"},"geometry":{"type":"Polygon","coordinates":[[[124.5,30.5],[125,30.5],[125,31],[124.5,31],[124.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"525"},"geometry":{"type":"Polygon","coordinates":[[[125,30.5],[125.5,30.5],[125.5,31],[125,31],[125,30.5]]]}},{"type":"Feature","properties":{"zone_name":"526"},"geometry":{"type":"Polygon","coordinates":[[[125.5,30.5],[126,30.5],[126,31],[125.5,31],[125.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"527"},"geometry":{"type":"Polygon","coordinates":[[[126,30.5],[126.5,30.5],[126.5,31],[126,31],[126,30.5]]]}},{"type":"Feature","properties":{"zone_name":"528"},"geometry":{"type":"Polygon","coordinates":[[[126.5,30.5],[127,30.5],[127,31],[126.5,31],[126.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"529"},"geometry":{"type":"Polygon","coordinates":[[[127,30.5],[127.5,30.5],[127.5,31],[127,31],[127,30.5]]]}},{"type":"Feature","properties":{"zone_name":"530"},"geometry":{"type":"Polygon","coordinates":[[[127.5,30.5],[128,30.5],[128,31],[127.5,31],[127.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"531"},"geometry":{"type":"Polygon","coordinates":[[[128,30.5],[128.5,30.5],[128.5,31],[128,31],[128,30.5]]]}},{"type":"Feature","properties":{"zone_name":"532"},"geometry":{"type":"Polygon","coordinates":[[[128.5,30.5],[129,30.5],[129,31],[128.5,31],[128.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"533"},"geometry":{"type":"Polygon","coordinates":[[[129,30.5],[129.5,30.5],[129.5,31],[129,31],[129,30.5]]]}},{"type":"Feature","properties":{"zone_name":"534"},"geometry":{"type":"Polygon","coordinates":[[[129.5,30.5],[130,30.5],[130,31],[129.5,31],[129.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"535"},"geometry":{"type":"Polygon","coordinates":[[[130,30.5],[130.5,30.5],[130.5,31],[130,31],[130,30.5]]]}},{"type":"Feature","properties":{"zone_name":"536"},"geometry":{"type":"Polygon","coordinates":[[[130.5,30.5],[131,30.5],[131,31],[130.5,31],[130.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"537"},"geometry":{"type":"Polygon","coordinates":[[[131,30.5],[131.5,30.5],[131.5,31],[131,31],[131,30.5]]]}},{"type":"Feature","properties":{"zone_name":"538"},"geometry":{"type":"Polygon","coordinates":[[[131.5,30.5],[132,30.5],[132,31],[131.5,31],[131.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"539"},"geometry":{"type":"Polygon","coordinates":[[[132,30.5],[132.5,30.5],[132.5,31],[132,31],[132,30.5]]]}},{"type":"Feature","properties":{"zone_name":"540"},"geometry":{"type":"Polygon","coordinates":[[[132.5,30.5],[133,30.5],[133,31],[132.5,31],[132.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"541"},"geometry":{"type":"Polygon","coordinates":[[[133,30.5],[133.5,30.5],[133.5,31],[133,31],[133,30.5]]]}},{"type":"Feature","properties":{"zone_name":"542"},"geometry":{"type":"Polygon","coordinates":[[[133.5,30.5],[134,30.5],[134,31],[133.5,31],[133.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"543"},"geometry":{"type":"Polygon","coordinates":[[[134,30.5],[134.5,30.5],[134.5,31],[134,31],[134,30.5]]]}},{"type":"Feature","properties":{"zone_name":"544"},"geometry":{"type":"Polygon","coordinates":[[[134.5,30.5],[135,30.5],[135,31],[134.5,31],[134.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"545"},"geometry":{"type":"Polygon","coordinates":[[[135,30.5],[135.5,30.5],[135.5,31],[135,31],[135,30.5]]]}},{"type":"Feature","properties":{"zone_name":"546"},"geometry":{"type":"Polygon","coordinates":[[[135.5,30.5],[136,30.5],[136,31],[135.5,31],[135.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"547"},"geometry":{"type":"Polygon","coordinates":[[[136,30.5],[136.5,30.5],[136.5,31],[136,31],[136,30.5]]]}},{"type":"Feature","properties":{"zone_name":"548"},"geometry":{"type":"Polygon","coordinates":[[[136.5,30.5],[137,30.5],[137,31],[136.5,31],[136.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"549"},"geometry":{"type":"Polygon","coordinates":[[[137,30.5],[137.5,30.5],[137.5,31],[137,31],[137,30.5]]]}},{"type":"Feature","properties":{"zone_name":"550"},"geometry":{"type":"Polygon","coordinates":[[[137.5,30.5],[138,30.5],[138,31],[137.5,31],[137.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"551"},"geometry":{"type":"Polygon","coordinates":[[[122,30],[122.5,30],[122.5,30.5],[122,30.5],[122,30]]]}},{"type":"Feature","properties":{"zone_name":"552"},"geometry":{"type":"Polygon","coordinates":[[[122.5,30],[123,30],[123,30.5],[122.5,30.5],[122.5,30]]]}},{"type":"Feature","properties":{"zone_name":"553"},"geometry":{"type":"Polygon","coordinates":[[[123,30],[123.5,30],[123.5,30.5],[123,30.5],[123,30]]]}},{"type":"Feature","properties":{"zone_name":"554"},"geometry":{"type":"Polygon","coordinates":[[[123.5,30],[124,30],[124,30.5],[123.5,30.5],[123.5,30]]]}},{"type":"Feature","properties":{"zone_name":"555"},"geometry":{"type":"Polygon","coordinates":[[[124,30],[124.5,30],[124.5,30.5],[124,30.5],[124,30]]]}},{"type":"Feature","properties":{"zone_name":"556"},"geometry":{"type":"Polygon","coordinates":[[[124.5,30],[125,30],[125,30.5],[124.5,30.5],[124.5,30]]]}},{"type":"Feature","properties":{"zone_name":"557"},"geometry":{"type":"Polygon","coordinates":[[[125,30],[125.5,30],[125.5,30.5],[125,30.5],[125,30]]]}},{"type":"Feature","properties":{"zone_name":"558"},"geometry":{"type":"Polygon","coordinates":[[[125.5,30],[126,30],[126,30.5],[125.5,30.5],[125.5,30]]]}},{"type":"Feature","properties":{"zone_name":"559"},"geometry":{"type":"Polygon","coordinates":[[[126,30],[126.5,30],[126.5,30.5],[126,30.5],[126,30]]]}},{"type":"Feature","properties":{"zone_name":"560"},"geometry":{"type":"Polygon","coordinates":[[[126.5,30],[127,30],[127,30.5],[126.5,30.5],[126.5,30]]]}},{"type":"Feature","properties":{"zone_name":"561"},"geometry":{"type":"Polygon","coordinates":[[[127,30],[127.5,30],[127.5,30.5],[127,30.5],[127,30]]]}},{"type":"Feature","properties":{"zone_name":"562"},"geometry":{"type":"Polygon","coordinates":[[[127.5,30],[128,30],[128,30.5],[127.5,30.5],[127.5,30]]]}},{"type":"Feature","properties":{"zone_name":"563"},"geometry":{"type":"Polygon","coordinates":[[[128,30],[128.5,30],[128.5,30.5],[128,30.5],[128,30]]]}},{"type":"Feature","properties":{"zone_name":"564"},"geometry":{"type":"Polygon","coordinates":[[[128.5,30],[129,30],[129,30.5],[128.5,30.5],[128.5,30]]]}},{"type":"Feature","properties":{"zone_name":"565"},"geometry":{"type":"Polygon","coordinates":[[[129,30],[129.5,30],[129.5,30.5],[129,30.5],[129,30]]]}},{"type":"Feature","properties":{"zone_name":"566"},"geometry":{"type":"Polygon","coordinates":[[[129.5,30],[130,30],[130,30.5],[129.5,30.5],[129.5,30]]]}},{"type":"Feature","properties":{"zone_name":"567"},"geometry":{"type":"Polygon","coordinates":[[[130,30],[130.5,30],[130.5,30.5],[130,30.5],[130,30]]]}},{"type":"Feature","properties":{"zone_name":"568"},"geometry":{"type":"Polygon","coordinates":[[[130.5,30],[131,30],[131,30.5],[130.5,30.5],[130.5,30]]]}},{"type":"Feature","properties":{"zone_name":"569"},"geometry":{"type":"Polygon","coordinates":[[[131,30],[131.5,30],[131.5,30.5],[131,30.5],[131,30]]]}},{"type":"Feature","properties":{"zone_name":"570"},"geometry":{"type":"Polygon","coordinates":[[[131.5,30],[132,30],[132,30.5],[131.5,30.5],[131.5,30]]]}},{"type":"Feature","properties":{"zone_name":"571"},"geometry":{"type":"Polygon","coordinates":[[[132,30],[132.5,30],[132.5,30.5],[132,30.5],[132,30]]]}},{"type":"Feature","properties":{"zone_name":"572"},"geometry":{"type":"Polygon","coordinates":[[[132.5,30],[133,30],[133,30.5],[132.5,30.5],[132.5,30]]]}},{"type":"Feature","properties":{"zone_name":"573"},"geometry":{"type":"Polygon","coordinates":[[[133,30],[133.5,30],[133.5,30.5],[133,30.5],[133,30]]]}},{"type":"Feature","properties":{"zone_name":"574"},"geometry":{"type":"Polygon","coordinates":[[[133.5,30],[134,30],[134,30.5],[133.5,30.5],[133.5,30]]]}},{"type":"Feature","properties":{"zone_name":"575"},"geometry":{"type":"Polygon","coordinates":[[[134,30],[134.5,30],[134.5,30.5],[134,30.5],[134,30]]]}},{"type":"Feature","properties":{"zone_name":"576"},"geometry":{"type":"Polygon","coordinates":[[[134.5,30],[135,30],[135,30.5],[134.5,30.5],[134.5,30]]]}},{"type":"Feature","properties":{"zone_name":"577"},"geometry":{"type":"Polygon","coordinates":[[[135,30],[135.5,30],[135.5,30.5],[135,30.5],[135,30]]]}},{"type":"Feature","properties":{"zone_name":"578"},"geometry":{"type":"Polygon","coordinates":[[[135.5,30],[136,30],[136,30.5],[135.5,30.5],[135.5,30]]]}},{"type":"Feature","properties":{"zone_name":"579"},"geometry":{"type":"Polygon","coordinates":[[[136,30],[136.5,30],[136.5,30.5],[136,30.5],[136,30]]]}},{"type":"Feature","properties":{"zone_name":"580"},"geometry":{"type":"Polygon","coordinates":[[[136.5,30],[137,30],[137,30.5],[136.5,30.5],[136.5,30]]]}},{"type":"Feature","properties":{"zone_name":"581"},"geometry":{"type":"Polygon","coordinates":[[[137,30],[137.5,30],[137.5,30.5],[137,30.5],[137,30]]]}},{"type":"Feature","properties":{"zone_name":"582"},"geometry":{"type":"Polygon","coordinates":[[[137.5,30],[138,30],[138,30.5],[137.5,30.5],[137.5,30]]]}},{"type":"Feature","properties":{"zone_name":"583"},"geometry":{"type":"Polygon","coordinates":[[[135,29.5],[135.5,29.5],[135.5,30],[135,30],[135,29.5]]]}},{"type":"Feature","properties":{"zone_name":"584"},"geometry":{"type":"Polygon","coordinates":[[[135.5,29.5],[136,29.5],[136,30],[135.5,30],[135.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"585"},"geometry":{"type":"Polygon","coordinates":[[[136,29.5],[136.5,29.5],[136.5,30],[136,30],[136,29.5]]]}},{"type":"Feature","properties":{"zone_name":"586"},"geometry":{"type":"Polygon","coordinates":[[[136.5,29.5],[137,29.5],[137,30],[136.5,30],[136.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"587"},"geometry":{"type":"Polygon","coordinates":[[[137,29.5],[137.5,29.5],[137.5,30],[137,30],[137,29.5]]]}},{"type":"Feature","properties":{"zone_name":"588"},"geometry":{"type":"Polygon","coordinates":[[[137.5,29.5],[138,29.5],[138,30],[137.5,30],[137.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"589"},"geometry":{"type":"Polygon","coordinates":[[[135,29],[135.5,29],[135.5,29.5],[135,29.5],[135,29]]]}},{"type":"Feature","properties":{"zone_name":"590"},"geometry":{"type":"Polygon","coordinates":[[[135.5,29],[136,29],[136,29.5],[135.5,29.5],[135.5,29]]]}},{"type":"Feature","properties":{"zone_name":"591"},"geometry":{"type":"Polygon","coordinates":[[[136,29],[136.5,29],[136.5,29.5],[136,29.5],[136,29]]]}},{"type":"Feature","properties":{"zone_name":"592"},"geometry":{"type":"Polygon","coordinates":[[[136.5,29],[137,29],[137,29.5],[136.5,29.5],[136.5,29]]]}},{"type":"Feature","properties":{"zone_name":"593"},"geometry":{"type":"Polygon","coordinates":[[[137,29],[137.5,29],[137.5,29.5],[137,29.5],[137,29]]]}},{"type":"Feature","properties":{"zone_name":"594"},"geometry":{"type":"Polygon","coordinates":[[[137.5,29],[138,29],[138,29.5],[137.5,29.5],[137.5,29]]]}},{"type":"Feature","properties":{"zone_name":"595"},"geometry":{"type":"Polygon","coordinates":[[[135,28.5],[135.5,28.5],[135.5,29],[135,29],[135,28.5]]]}},{"type":"Feature","properties":{"zone_name":"596"},"geometry":{"type":"Polygon","coordinates":[[[135.5,28.5],[136,28.5],[136,29],[135.5,29],[135.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"597"},"geometry":{"type":"Polygon","coordinates":[[[136,28.5],[136.5,28.5],[136.5,29],[136,29],[136,28.5]]]}},{"type":"Feature","properties":{"zone_name":"598"},"geometry":{"type":"Polygon","coordinates":[[[136.5,28.5],[137,28.5],[137,29],[136.5,29],[136.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"599"},"geometry":{"type":"Polygon","coordinates":[[[137,28.5],[137.5,28.5],[137.5,29],[137,29],[137,28.5]]]}},{"type":"Feature","properties":{"zone_name":"600"},"geometry":{"type":"Polygon","coordinates":[[[137.5,28.5],[138,28.5],[138,29],[137.5,29],[137.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"601"},"geometry":{"type":"Polygon","coordinates":[[[121,30.5],[121.5,30.5],[121.5,31],[121,31],[121,30.5]]]}},{"type":"Feature","properties":{"zone_name":"602"},"geometry":{"type":"Polygon","coordinates":[[[121.5,30.5],[122,30.5],[122,31],[121.5,31],[121.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"603"},"geometry":{"type":"Polygon","coordinates":[[[121,30],[121.5,30],[121.5,30.5],[121,30.5],[121,30]]]}},{"type":"Feature","properties":{"zone_name":"604"},"geometry":{"type":"Polygon","coordinates":[[[121.5,30],[122,30],[122,30.5],[121.5,30.5],[121.5,30]]]}},{"type":"Feature","properties":{"zone_name":"605"},"geometry":{"type":"Polygon","coordinates":[[[122,29.5],[122.5,29.5],[122.5,30],[122,30],[122,29.5]]]}},{"type":"Feature","properties":{"zone_name":"606"},"geometry":{"type":"Polygon","coordinates":[[[122.5,29.5],[123,29.5],[123,30],[122.5,30],[122.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"607"},"geometry":{"type":"Polygon","coordinates":[[[123,29.5],[123.5,29.5],[123.5,30],[123,30],[123,29.5]]]}},{"type":"Feature","properties":{"zone_name":"608"},"geometry":{"type":"Polygon","coordinates":[[[123.5,29.5],[124,29.5],[124,30],[123.5,30],[123.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"609"},"geometry":{"type":"Polygon","coordinates":[[[124,29.5],[124.5,29.5],[124.5,30],[124,30],[124,29.5]]]}},{"type":"Feature","properties":{"zone_name":"610"},"geometry":{"type":"Polygon","coordinates":[[[124.5,29.5],[125,29.5],[125,30],[124.5,30],[124.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"615"},"geometry":{"type":"Polygon","coordinates":[[[122,29],[122.5,29],[122.5,29.5],[122,29.5],[122,29]]]}},{"type":"Feature","properties":{"zone_name":"616"},"geometry":{"type":"Polygon","coordinates":[[[122.5,29],[123,29],[123,29.5],[122.5,29.5],[122.5,29]]]}},{"type":"Feature","properties":{"zone_name":"617"},"geometry":{"type":"Polygon","coordinates":[[[123,29],[123.5,29],[123.5,29.5],[123,29.5],[123,29]]]}},{"type":"Feature","properties":{"zone_name":"618"},"geometry":{"type":"Polygon","coordinates":[[[123.5,29],[124,29],[124,29.5],[123.5,29.5],[123.5,29]]]}},{"type":"Feature","properties":{"zone_name":"619"},"geometry":{"type":"Polygon","coordinates":[[[124,29],[124.5,29],[124.5,29.5],[124,29.5],[124,29]]]}},{"type":"Feature","properties":{"zone_name":"620"},"geometry":{"type":"Polygon","coordinates":[[[124.5,29],[125,29],[125,29.5],[124.5,29.5],[124.5,29]]]}},{"type":"Feature","properties":{"zone_name":"624"},"geometry":{"type":"Polygon","coordinates":[[[121.5,28.5],[122,28.5],[122,29],[121.5,29],[121.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"625"},"geometry":{"type":"Polygon","coordinates":[[[122,28.5],[122.5,28.5],[122.5,29],[122,29],[122,28.5]]]}},{"type":"Feature","properties":{"zone_name":"626"},"geometry":{"type":"Polygon","coordinates":[[[122.5,28.5],[123,28.5],[123,29],[122.5,29],[122.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"627"},"geometry":{"type":"Polygon","coordinates":[[[123,28.5],[123.5,28.5],[123.5,29],[123,29],[123,28.5]]]}},{"type":"Feature","properties":{"zone_name":"628"},"geometry":{"type":"Polygon","coordinates":[[[123.5,28.5],[124,28.5],[124,29],[123.5,29],[123.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"629"},"geometry":{"type":"Polygon","coordinates":[[[124,28.5],[124.5,28.5],[124.5,29],[124,29],[124,28.5]]]}},{"type":"Feature","properties":{"zone_name":"630"},"geometry":{"type":"Polygon","coordinates":[[[124.5,28.5],[125,28.5],[125,29],[124.5,29],[124.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"633"},"geometry":{"type":"Polygon","coordinates":[[[121,28],[121.5,28],[121.5,28.5],[121,28.5],[121,28]]]}},{"type":"Feature","properties":{"zone_name":"634"},"geometry":{"type":"Polygon","coordinates":[[[121.5,28],[122,28],[122,28.5],[121.5,28.5],[121.5,28]]]}},{"type":"Feature","properties":{"zone_name":"635"},"geometry":{"type":"Polygon","coordinates":[[[122,28],[122.5,28],[122.5,28.5],[122,28.5],[122,28]]]}},{"type":"Feature","properties":{"zone_name":"636"},"geometry":{"type":"Polygon","coordinates":[[[122.5,28],[123,28],[123,28.5],[122.5,28.5],[122.5,28]]]}},{"type":"Feature","properties":{"zone_name":"637"},"geometry":{"type":"Polygon","coordinates":[[[123,28],[123.5,28],[123.5,28.5],[123,28.5],[123,28]]]}},{"type":"Feature","properties":{"zone_name":"638"},"geometry":{"type":"Polygon","coordinates":[[[123.5,28],[124,28],[124,28.5],[123.5,28.5],[123.5,28]]]}},{"type":"Feature","properties":{"zone_name":"639"},"geometry":{"type":"Polygon","coordinates":[[[124,28],[124.5,28],[124.5,28.5],[124,28.5],[124,28]]]}},{"type":"Feature","properties":{"zone_name":"640"},"geometry":{"type":"Polygon","coordinates":[[[124.5,28],[125,28],[125,28.5],[124.5,28.5],[124.5,28]]]}},{"type":"Feature","properties":{"zone_name":"642"},"geometry":{"type":"Polygon","coordinates":[[[120.5,27.5],[121,27.5],[121,28],[120.5,28],[120.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"643"},"geometry":{"type":"Polygon","coordinates":[[[121,27.5],[121.5,27.5],[121.5,28],[121,28],[121,27.5]]]}},{"type":"Feature","properties":{"zone_name":"644"},"geometry":{"type":"Polygon","coordinates":[[[121.5,27.5],[122,27.5],[122,28],[121.5,28],[121.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"645"},"geometry":{"type":"Polygon","coordinates":[[[122,27.5],[122.5,27.5],[122.5,28],[122,28],[122,27.5]]]}},{"type":"Feature","properties":{"zone_name":"646"},"geometry":{"type":"Polygon","coordinates":[[[122.5,27.5],[123,27.5],[123,28],[122.5,28],[122.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"647"},"geometry":{"type":"Polygon","coordinates":[[[123,27.5],[123.5,27.5],[123.5,28],[123,28],[123,27.5]]]}},{"type":"Feature","properties":{"zone_name":"648"},"geometry":{"type":"Polygon","coordinates":[[[123.5,27.5],[124,27.5],[124,28],[123.5,28],[123.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"649"},"geometry":{"type":"Polygon","coordinates":[[[124,27.5],[124.5,27.5],[124.5,28],[124,28],[124,27.5]]]}},{"type":"Feature","properties":{"zone_name":"650"},"geometry":{"type":"Polygon","coordinates":[[[124.5,27.5],[125,27.5],[125,28],[124.5,28],[124.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"651"},"geometry":{"type":"Polygon","coordinates":[[[120,27],[120.5,27],[120.5,27.5],[120,27.5],[120,27]]]}},{"type":"Feature","properties":{"zone_name":"652"},"geometry":{"type":"Polygon","coordinates":[[[120.5,27],[121,27],[121,27.5],[120.5,27.5],[120.5,27]]]}},{"type":"Feature","properties":{"zone_name":"653"},"geometry":{"type":"Polygon","coordinates":[[[121,27],[121.5,27],[121.5,27.5],[121,27.5],[121,27]]]}},{"type":"Feature","properties":{"zone_name":"654"},"geometry":{"type":"Polygon","coordinates":[[[121.5,27],[122,27],[122,27.5],[121.5,27.5],[121.5,27]]]}},{"type":"Feature","properties":{"zone_name":"655"},"geometry":{"type":"Polygon","coordinates":[[[122,27],[122.5,27],[122.5,27.5],[122,27.5],[122,27]]]}},{"type":"Feature","properties":{"zone_name":"656"},"geometry":{"type":"Polygon","coordinates":[[[122.5,27],[123,27],[123,27.5],[122.5,27.5],[122.5,27]]]}},{"type":"Feature","properties":{"zone_name":"657"},"geometry":{"type":"Polygon","coordinates":[[[123,27],[123.5,27],[123.5,27.5],[123,27.5],[123,27]]]}},{"type":"Feature","properties":{"zone_name":"658"},"geometry":{"type":"Polygon","coordinates":[[[123.5,27],[124,27],[124,27.5],[123.5,27.5],[123.5,27]]]}},{"type":"Feature","properties":{"zone_name":"659"},"geometry":{"type":"Polygon","coordinates":[[[124,27],[124.5,27],[124.5,27.5],[124,27.5],[124,27]]]}},{"type":"Feature","properties":{"zone_name":"660"},"geometry":{"type":"Polygon","coordinates":[[[124.5,27],[125,27],[125,27.5],[124.5,27.5],[124.5,27]]]}},{"type":"Feature","properties":{"zone_name":"661"},"geometry":{"type":"Polygon","coordinates":[[[120,26.5],[120.5,26.5],[120.5,27],[120,27],[120,26.5]]]}},{"type":"Feature","properties":{"zone_name":"662"},"geometry":{"type":"Polygon","coordinates":[[[120.5,26.5],[121,26.5],[121,27],[120.5,27],[120.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"663"},"geometry":{"type":"Polygon","coordinates":[[[121,26.5],[121.5,26.5],[121.5,27],[121,27],[121,26.5]]]}},{"type":"Feature","properties":{"zone_name":"664"},"geometry":{"type":"Polygon","coordinates":[[[121.5,26.5],[122,26.5],[122,27],[121.5,27],[121.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"665"},"geometry":{"type":"Polygon","coordinates":[[[122,26.5],[122.5,26.5],[122.5,27],[122,27],[122,26.5]]]}},{"type":"Feature","properties":{"zone_name":"666"},"geometry":{"type":"Polygon","coordinates":[[[122.5,26.5],[123,26.5],[123,27],[122.5,27],[122.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"667"},"geometry":{"type":"Polygon","coordinates":[[[123,26.5],[123.5,26.5],[123.5,27],[123,27],[123,26.5]]]}},{"type":"Feature","properties":{"zone_name":"668"},"geometry":{"type":"Polygon","coordinates":[[[123.5,26.5],[124,26.5],[124,27],[123.5,27],[123.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"669"},"geometry":{"type":"Polygon","coordinates":[[[124,26.5],[124.5,26.5],[124.5,27],[124,27],[124,26.5]]]}},{"type":"Feature","properties":{"zone_name":"670"},"geometry":{"type":"Polygon","coordinates":[[[124.5,26.5],[125,26.5],[125,27],[124.5,27],[124.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"671"},"geometry":{"type":"Polygon","coordinates":[[[120,26],[120.5,26],[120.5,26.5],[120,26.5],[120,26]]]}},{"type":"Feature","properties":{"zone_name":"672"},"geometry":{"type":"Polygon","coordinates":[[[120.5,26],[121,26],[121,26.5],[120.5,26.5],[120.5,26]]]}},{"type":"Feature","properties":{"zone_name":"673"},"geometry":{"type":"Polygon","coordinates":[[[121,26],[121.5,26],[121.5,26.5],[121,26.5],[121,26]]]}},{"type":"Feature","properties":{"zone_name":"674"},"geometry":{"type":"Polygon","coordinates":[[[121.5,26],[122,26],[122,26.5],[121.5,26.5],[121.5,26]]]}},{"type":"Feature","properties":{"zone_name":"675"},"geometry":{"type":"Polygon","coordinates":[[[122,26],[122.5,26],[122.5,26.5],[122,26.5],[122,26]]]}},{"type":"Feature","properties":{"zone_name":"676"},"geometry":{"type":"Polygon","coordinates":[[[122.5,26],[123,26],[123,26.5],[122.5,26.5],[122.5,26]]]}},{"type":"Feature","properties":{"zone_name":"677"},"geometry":{"type":"Polygon","coordinates":[[[123,26],[123.5,26],[123.5,26.5],[123,26.5],[123,26]]]}},{"type":"Feature","properties":{"zone_name":"678"},"geometry":{"type":"Polygon","coordinates":[[[123.5,26],[124,26],[124,26.5],[123.5,26.5],[123.5,26]]]}},{"type":"Feature","properties":{"zone_name":"679"},"geometry":{"type":"Polygon","coordinates":[[[124,26],[124.5,26],[124.5,26.5],[124,26.5],[124,26]]]}},{"type":"Feature","properties":{"zone_name":"680"},"geometry":{"type":"Polygon","coordinates":[[[124.5,26],[125,26],[125,26.5],[124.5,26.5],[124.5,26]]]}},{"type":"Feature","properties":{"zone_name":"681"},"geometry":{"type":"Polygon","coordinates":[[[120,25.5],[120.5,25.5],[120.5,26],[120,26],[120,25.5]]]}},{"type":"Feature","properties":{"zone_name":"682"},"geometry":{"type":"Polygon","coordinates":[[[120.5,25.5],[121,25.5],[121,26],[120.5,26],[120.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"683"},"geometry":{"type":"Polygon","coordinates":[[[121,25.5],[121.5,25.5],[121.5,26],[121,26],[121,25.5]]]}},{"type":"Feature","properties":{"zone_name":"684"},"geometry":{"type":"Polygon","coordinates":[[[121.5,25.5],[122,25.5],[122,26],[121.5,26],[121.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"685"},"geometry":{"type":"Polygon","coordinates":[[[122,25.5],[122.5,25.5],[122.5,26],[122,26],[122,25.5]]]}},{"type":"Feature","properties":{"zone_name":"686"},"geometry":{"type":"Polygon","coordinates":[[[122.5,25.5],[123,25.5],[123,26],[122.5,26],[122.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"687"},"geometry":{"type":"Polygon","coordinates":[[[123,25.5],[123.5,25.5],[123.5,26],[123,26],[123,25.5]]]}},{"type":"Feature","properties":{"zone_name":"688"},"geometry":{"type":"Polygon","coordinates":[[[123.5,25.5],[124,25.5],[124,26],[123.5,26],[123.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"689"},"geometry":{"type":"Polygon","coordinates":[[[124,25.5],[124.5,25.5],[124.5,26],[124,26],[124,25.5]]]}},{"type":"Feature","properties":{"zone_name":"690"},"geometry":{"type":"Polygon","coordinates":[[[124.5,25.5],[125,25.5],[125,26],[124.5,26],[124.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"691"},"geometry":{"type":"Polygon","coordinates":[[[120,25],[120.5,25],[120.5,25.5],[120,25.5],[120,25]]]}},{"type":"Feature","properties":{"zone_name":"692"},"geometry":{"type":"Polygon","coordinates":[[[120.5,25],[121,25],[121,25.5],[120.5,25.5],[120.5,25]]]}},{"type":"Feature","properties":{"zone_name":"693"},"geometry":{"type":"Polygon","coordinates":[[[121,25],[121.5,25],[121.5,25.5],[121,25.5],[121,25]]]}},{"type":"Feature","properties":{"zone_name":"694"},"geometry":{"type":"Polygon","coordinates":[[[121.5,25],[122,25],[122,25.5],[121.5,25.5],[121.5,25]]]}},{"type":"Feature","properties":{"zone_name":"695"},"geometry":{"type":"Polygon","coordinates":[[[122,25],[122.5,25],[122.5,25.5],[122,25.5],[122,25]]]}},{"type":"Feature","properties":{"zone_name":"696"},"geometry":{"type":"Polygon","coordinates":[[[122.5,25],[123,25],[123,25.5],[122.5,25.5],[122.5,25]]]}},{"type":"Feature","properties":{"zone_name":"697"},"geometry":{"type":"Polygon","coordinates":[[[123,25],[123.5,25],[123.5,25.5],[123,25.5],[123,25]]]}},{"type":"Feature","properties":{"zone_name":"698"},"geometry":{"type":"Polygon","coordinates":[[[123.5,25],[124,25],[124,25.5],[123.5,25.5],[123.5,25]]]}},{"type":"Feature","properties":{"zone_name":"699"},"geometry":{"type":"Polygon","coordinates":[[[124,25],[124.5,25],[124.5,25.5],[124,25.5],[124,25]]]}},{"type":"Feature","properties":{"zone_name":"700"},"geometry":{"type":"Polygon","coordinates":[[[124.5,25],[125,25],[125,25.5],[124.5,25.5],[124.5,25]]]}},{"type":"Feature","properties":{"zone_name":"701"},"geometry":{"type":"Polygon","coordinates":[[[125,29.5],[125.5,29.5],[125.5,30],[125,30],[125,29.5]]]}},{"type":"Feature","properties":{"zone_name":"702"},"geometry":{"type":"Polygon","coordinates":[[[125.5,29.5],[126,29.5],[126,30],[125.5,30],[125.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"703"},"geometry":{"type":"Polygon","coordinates":[[[126,29.5],[126.5,29.5],[126.5,30],[126,30],[126,29.5]]]}},{"type":"Feature","properties":{"zone_name":"704"},"geometry":{"type":"Polygon","coordinates":[[[126.5,29.5],[127,29.5],[127,30],[126.5,30],[126.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"705"},"geometry":{"type":"Polygon","coordinates":[[[127,29.5],[127.5,29.5],[127.5,30],[127,30],[127,29.5]]]}},{"type":"Feature","properties":{"zone_name":"706"},"geometry":{"type":"Polygon","coordinates":[[[127.5,29.5],[128,29.5],[128,30],[127.5,30],[127.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"707"},"geometry":{"type":"Polygon","coordinates":[[[128,29.5],[128.5,29.5],[128.5,30],[128,30],[128,29.5]]]}},{"type":"Feature","properties":{"zone_name":"708"},"geometry":{"type":"Polygon","coordinates":[[[128.5,29.5],[129,29.5],[129,30],[128.5,30],[128.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"709"},"geometry":{"type":"Polygon","coordinates":[[[129,29.5],[129.5,29.5],[129.5,30],[129,30],[129,29.5]]]}},{"type":"Feature","properties":{"zone_name":"710"},"geometry":{"type":"Polygon","coordinates":[[[129.5,29.5],[130,29.5],[130,30],[129.5,30],[129.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"711"},"geometry":{"type":"Polygon","coordinates":[[[125,29],[125.5,29],[125.5,29.5],[125,29.5],[125,29]]]}},{"type":"Feature","properties":{"zone_name":"712"},"geometry":{"type":"Polygon","coordinates":[[[125.5,29],[126,29],[126,29.5],[125.5,29.5],[125.5,29]]]}},{"type":"Feature","properties":{"zone_name":"713"},"geometry":{"type":"Polygon","coordinates":[[[126,29],[126.5,29],[126.5,29.5],[126,29.5],[126,29]]]}},{"type":"Feature","properties":{"zone_name":"714"},"geometry":{"type":"Polygon","coordinates":[[[126.5,29],[127,29],[127,29.5],[126.5,29.5],[126.5,29]]]}},{"type":"Feature","properties":{"zone_name":"715"},"geometry":{"type":"Polygon","coordinates":[[[127,29],[127.5,29],[127.5,29.5],[127,29.5],[127,29]]]}},{"type":"Feature","properties":{"zone_name":"716"},"geometry":{"type":"Polygon","coordinates":[[[127.5,29],[128,29],[128,29.5],[127.5,29.5],[127.5,29]]]}},{"type":"Feature","properties":{"zone_name":"717"},"geometry":{"type":"Polygon","coordinates":[[[128,29],[128.5,29],[128.5,29.5],[128,29.5],[128,29]]]}},{"type":"Feature","properties":{"zone_name":"718"},"geometry":{"type":"Polygon","coordinates":[[[128.5,29],[129,29],[129,29.5],[128.5,29.5],[128.5,29]]]}},{"type":"Feature","properties":{"zone_name":"719"},"geometry":{"type":"Polygon","coordinates":[[[129,29],[129.5,29],[129.5,29.5],[129,29.5],[129,29]]]}},{"type":"Feature","properties":{"zone_name":"720"},"geometry":{"type":"Polygon","coordinates":[[[129.5,29],[130,29],[130,29.5],[129.5,29.5],[129.5,29]]]}},{"type":"Feature","properties":{"zone_name":"721"},"geometry":{"type":"Polygon","coordinates":[[[125,28.5],[125.5,28.5],[125.5,29],[125,29],[125,28.5]]]}},{"type":"Feature","properties":{"zone_name":"722"},"geometry":{"type":"Polygon","coordinates":[[[125.5,28.5],[126,28.5],[126,29],[125.5,29],[125.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"723"},"geometry":{"type":"Polygon","coordinates":[[[126,28.5],[126.5,28.5],[126.5,29],[126,29],[126,28.5]]]}},{"type":"Feature","properties":{"zone_name":"724"},"geometry":{"type":"Polygon","coordinates":[[[126.5,28.5],[127,28.5],[127,29],[126.5,29],[126.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"725"},"geometry":{"type":"Polygon","coordinates":[[[127,28.5],[127.5,28.5],[127.5,29],[127,29],[127,28.5]]]}},{"type":"Feature","properties":{"zone_name":"726"},"geometry":{"type":"Polygon","coordinates":[[[127.5,28.5],[128,28.5],[128,29],[127.5,29],[127.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"727"},"geometry":{"type":"Polygon","coordinates":[[[128,28.5],[128.5,28.5],[128.5,29],[128,29],[128,28.5]]]}},{"type":"Feature","properties":{"zone_name":"728"},"geometry":{"type":"Polygon","coordinates":[[[128.5,28.5],[129,28.5],[129,29],[128.5,29],[128.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"729"},"geometry":{"type":"Polygon","coordinates":[[[129,28.5],[129.5,28.5],[129.5,29],[129,29],[129,28.5]]]}},{"type":"Feature","properties":{"zone_name":"730"},"geometry":{"type":"Polygon","coordinates":[[[129.5,28.5],[130,28.5],[130,29],[129.5,29],[129.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"731"},"geometry":{"type":"Polygon","coordinates":[[[125,28],[125.5,28],[125.5,28.5],[125,28.5],[125,28]]]}},{"type":"Feature","properties":{"zone_name":"732"},"geometry":{"type":"Polygon","coordinates":[[[125.5,28],[126,28],[126,28.5],[125.5,28.5],[125.5,28]]]}},{"type":"Feature","properties":{"zone_name":"733"},"geometry":{"type":"Polygon","coordinates":[[[126,28],[126.5,28],[126.5,28.5],[126,28.5],[126,28]]]}},{"type":"Feature","properties":{"zone_name":"734"},"geometry":{"type":"Polygon","coordinates":[[[126.5,28],[127,28],[127,28.5],[126.5,28.5],[126.5,28]]]}},{"type":"Feature","properties":{"zone_name":"735"},"geometry":{"type":"Polygon","coordinates":[[[127,28],[127.5,28],[127.5,28.5],[127,28.5],[127,28]]]}},{"type":"Feature","properties":{"zone_name":"736"},"geometry":{"type":"Polygon","coordinates":[[[127.5,28],[128,28],[128,28.5],[127.5,28.5],[127.5,28]]]}},{"type":"Feature","properties":{"zone_name":"737"},"geometry":{"type":"Polygon","coordinates":[[[128,28],[128.5,28],[128.5,28.5],[128,28.5],[128,28]]]}},{"type":"Feature","properties":{"zone_name":"738"},"geometry":{"type":"Polygon","coordinates":[[[128.5,28],[129,28],[129,28.5],[128.5,28.5],[128.5,28]]]}},{"type":"Feature","properties":{"zone_name":"739"},"geometry":{"type":"Polygon","coordinates":[[[129,28],[129.5,28],[129.5,28.5],[129,28.5],[129,28]]]}},{"type":"Feature","properties":{"zone_name":"740"},"geometry":{"type":"Polygon","coordinates":[[[129.5,28],[130,28],[130,28.5],[129.5,28.5],[129.5,28]]]}},{"type":"Feature","properties":{"zone_name":"741"},"geometry":{"type":"Polygon","coordinates":[[[125,27.5],[125.5,27.5],[125.5,28],[125,28],[125,27.5]]]}},{"type":"Feature","properties":{"zone_name":"742"},"geometry":{"type":"Polygon","coordinates":[[[125.5,27.5],[126,27.5],[126,28],[125.5,28],[125.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"743"},"geometry":{"type":"Polygon","coordinates":[[[126,27.5],[126.5,27.5],[126.5,28],[126,28],[126,27.5]]]}},{"type":"Feature","properties":{"zone_name":"744"},"geometry":{"type":"Polygon","coordinates":[[[126.5,27.5],[127,27.5],[127,28],[126.5,28],[126.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"745"},"geometry":{"type":"Polygon","coordinates":[[[127,27.5],[127.5,27.5],[127.5,28],[127,28],[127,27.5]]]}},{"type":"Feature","properties":{"zone_name":"746"},"geometry":{"type":"Polygon","coordinates":[[[127.5,27.5],[128,27.5],[128,28],[127.5,28],[127.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"747"},"geometry":{"type":"Polygon","coordinates":[[[128,27.5],[128.5,27.5],[128.5,28],[128,28],[128,27.5]]]}},{"type":"Feature","properties":{"zone_name":"748"},"geometry":{"type":"Polygon","coordinates":[[[128.5,27.5],[129,27.5],[129,28],[128.5,28],[128.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"749"},"geometry":{"type":"Polygon","coordinates":[[[129,27.5],[129.5,27.5],[129.5,28],[129,28],[129,27.5]]]}},{"type":"Feature","properties":{"zone_name":"750"},"geometry":{"type":"Polygon","coordinates":[[[129.5,27.5],[130,27.5],[130,28],[129.5,28],[129.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"751"},"geometry":{"type":"Polygon","coordinates":[[[125,27],[125.5,27],[125.5,27.5],[125,27.5],[125,27]]]}},{"type":"Feature","properties":{"zone_name":"752"},"geometry":{"type":"Polygon","coordinates":[[[125.5,27],[126,27],[126,27.5],[125.5,27.5],[125.5,27]]]}},{"type":"Feature","properties":{"zone_name":"753"},"geometry":{"type":"Polygon","coordinates":[[[126,27],[126.5,27],[126.5,27.5],[126,27.5],[126,27]]]}},{"type":"Feature","properties":{"zone_name":"754"},"geometry":{"type":"Polygon","coordinates":[[[126.5,27],[127,27],[127,27.5],[126.5,27.5],[126.5,27]]]}},{"type":"Feature","properties":{"zone_name":"755"},"geometry":{"type":"Polygon","coordinates":[[[127,27],[127.5,27],[127.5,27.5],[127,27.5],[127,27]]]}},{"type":"Feature","properties":{"zone_name":"756"},"geometry":{"type":"Polygon","coordinates":[[[127.5,27],[128,27],[128,27.5],[127.5,27.5],[127.5,27]]]}},{"type":"Feature","properties":{"zone_name":"757"},"geometry":{"type":"Polygon","coordinates":[[[128,27],[128.5,27],[128.5,27.5],[128,27.5],[128,27]]]}},{"type":"Feature","properties":{"zone_name":"758"},"geometry":{"type":"Polygon","coordinates":[[[128.5,27],[129,27],[129,27.5],[128.5,27.5],[128.5,27]]]}},{"type":"Feature","properties":{"zone_name":"759"},"geometry":{"type":"Polygon","coordinates":[[[129,27],[129.5,27],[129.5,27.5],[129,27.5],[129,27]]]}},{"type":"Feature","properties":{"zone_name":"760"},"geometry":{"type":"Polygon","coordinates":[[[129.5,27],[130,27],[130,27.5],[129.5,27.5],[129.5,27]]]}},{"type":"Feature","properties":{"zone_name":"761"},"geometry":{"type":"Polygon","coordinates":[[[125,26.5],[125.5,26.5],[125.5,27],[125,27],[125,26.5]]]}},{"type":"Feature","properties":{"zone_name":"762"},"geometry":{"type":"Polygon","coordinates":[[[125.5,26.5],[126,26.5],[126,27],[125.5,27],[125.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"763"},"geometry":{"type":"Polygon","coordinates":[[[126,26.5],[126.5,26.5],[126.5,27],[126,27],[126,26.5]]]}},{"type":"Feature","properties":{"zone_name":"764"},"geometry":{"type":"Polygon","coordinates":[[[126.5,26.5],[127,26.5],[127,27],[126.5,27],[126.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"765"},"geometry":{"type":"Polygon","coordinates":[[[127,26.5],[127.5,26.5],[127.5,27],[127,27],[127,26.5]]]}},{"type":"Feature","properties":{"zone_name":"766"},"geometry":{"type":"Polygon","coordinates":[[[127.5,26.5],[128,26.5],[128,27],[127.5,27],[127.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"767"},"geometry":{"type":"Polygon","coordinates":[[[128,26.5],[128.5,26.5],[128.5,27],[128,27],[128,26.5]]]}},{"type":"Feature","properties":{"zone_name":"768"},"geometry":{"type":"Polygon","coordinates":[[[128.5,26.5],[129,26.5],[129,27],[128.5,27],[128.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"769"},"geometry":{"type":"Polygon","coordinates":[[[129,26.5],[129.5,26.5],[129.5,27],[129,27],[129,26.5]]]}},{"type":"Feature","properties":{"zone_name":"770"},"geometry":{"type":"Polygon","coordinates":[[[129.5,26.5],[130,26.5],[130,27],[129.5,27],[129.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"771"},"geometry":{"type":"Polygon","coordinates":[[[125,26],[125.5,26],[125.5,26.5],[125,26.5],[125,26]]]}},{"type":"Feature","properties":{"zone_name":"772"},"geometry":{"type":"Polygon","coordinates":[[[125.5,26],[126,26],[126,26.5],[125.5,26.5],[125.5,26]]]}},{"type":"Feature","properties":{"zone_name":"773"},"geometry":{"type":"Polygon","coordinates":[[[126,26],[126.5,26],[126.5,26.5],[126,26.5],[126,26]]]}},{"type":"Feature","properties":{"zone_name":"774"},"geometry":{"type":"Polygon","coordinates":[[[126.5,26],[127,26],[127,26.5],[126.5,26.5],[126.5,26]]]}},{"type":"Feature","properties":{"zone_name":"775"},"geometry":{"type":"Polygon","coordinates":[[[127,26],[127.5,26],[127.5,26.5],[127,26.5],[127,26]]]}},{"type":"Feature","properties":{"zone_name":"776"},"geometry":{"type":"Polygon","coordinates":[[[127.5,26],[128,26],[128,26.5],[127.5,26.5],[127.5,26]]]}},{"type":"Feature","properties":{"zone_name":"777"},"geometry":{"type":"Polygon","coordinates":[[[128,26],[128.5,26],[128.5,26.5],[128,26.5],[128,26]]]}},{"type":"Feature","properties":{"zone_name":"778"},"geometry":{"type":"Polygon","coordinates":[[[128.5,26],[129,26],[129,26.5],[128.5,26.5],[128.5,26]]]}},{"type":"Feature","properties":{"zone_name":"779"},"geometry":{"type":"Polygon","coordinates":[[[129,26],[129.5,26],[129.5,26.5],[129,26.5],[129,26]]]}},{"type":"Feature","properties":{"zone_name":"780"},"geometry":{"type":"Polygon","coordinates":[[[129.5,26],[130,26],[130,26.5],[129.5,26.5],[129.5,26]]]}},{"type":"Feature","properties":{"zone_name":"781"},"geometry":{"type":"Polygon","coordinates":[[[125,25.5],[125.5,25.5],[125.5,26],[125,26],[125,25.5]]]}},{"type":"Feature","properties":{"zone_name":"782"},"geometry":{"type":"Polygon","coordinates":[[[125.5,25.5],[126,25.5],[126,26],[125.5,26],[125.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"783"},"geometry":{"type":"Polygon","coordinates":[[[126,25.5],[126.5,25.5],[126.5,26],[126,26],[126,25.5]]]}},{"type":"Feature","properties":{"zone_name":"784"},"geometry":{"type":"Polygon","coordinates":[[[126.5,25.5],[127,25.5],[127,26],[126.5,26],[126.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"785"},"geometry":{"type":"Polygon","coordinates":[[[127,25.5],[127.5,25.5],[127.5,26],[127,26],[127,25.5]]]}},{"type":"Feature","properties":{"zone_name":"786"},"geometry":{"type":"Polygon","coordinates":[[[127.5,25.5],[128,25.5],[128,26],[127.5,26],[127.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"787"},"geometry":{"type":"Polygon","coordinates":[[[128,25.5],[128.5,25.5],[128.5,26],[128,26],[128,25.5]]]}},{"type":"Feature","properties":{"zone_name":"788"},"geometry":{"type":"Polygon","coordinates":[[[128.5,25.5],[129,25.5],[129,26],[128.5,26],[128.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"789"},"geometry":{"type":"Polygon","coordinates":[[[129,25.5],[129.5,25.5],[129.5,26],[129,26],[129,25.5]]]}},{"type":"Feature","properties":{"zone_name":"790"},"geometry":{"type":"Polygon","coordinates":[[[129.5,25.5],[130,25.5],[130,26],[129.5,26],[129.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"791"},"geometry":{"type":"Polygon","coordinates":[[[125,25],[125.5,25],[125.5,25.5],[125,25.5],[125,25]]]}},{"type":"Feature","properties":{"zone_name":"792"},"geometry":{"type":"Polygon","coordinates":[[[125.5,25],[126,25],[126,25.5],[125.5,25.5],[125.5,25]]]}},{"type":"Feature","properties":{"zone_name":"793"},"geometry":{"type":"Polygon","coordinates":[[[126,25],[126.5,25],[126.5,25.5],[126,25.5],[126,25]]]}},{"type":"Feature","properties":{"zone_name":"794"},"geometry":{"type":"Polygon","coordinates":[[[126.5,25],[127,25],[127,25.5],[126.5,25.5],[126.5,25]]]}},{"type":"Feature","properties":{"zone_name":"795"},"geometry":{"type":"Polygon","coordinates":[[[127,25],[127.5,25],[127.5,25.5],[127,25.5],[127,25]]]}},{"type":"Feature","properties":{"zone_name":"796"},"geometry":{"type":"Polygon","coordinates":[[[127.5,25],[128,25],[128,25.5],[127.5,25.5],[127.5,25]]]}},{"type":"Feature","properties":{"zone_name":"797"},"geometry":{"type":"Polygon","coordinates":[[[128,25],[128.5,25],[128.5,25.5],[128,25.5],[128,25]]]}},{"type":"Feature","properties":{"zone_name":"798"},"geometry":{"type":"Polygon","coordinates":[[[128.5,25],[129,25],[129,25.5],[128.5,25.5],[128.5,25]]]}},{"type":"Feature","properties":{"zone_name":"799"},"geometry":{"type":"Polygon","coordinates":[[[129,25],[129.5,25],[129.5,25.5],[129,25.5],[129,25]]]}},{"type":"Feature","properties":{"zone_name":"800"},"geometry":{"type":"Polygon","coordinates":[[[129.5,25],[130,25],[130,25.5],[129.5,25.5],[129.5,25]]]}},{"type":"Feature","properties":{"zone_name":"801"},"geometry":{"type":"Polygon","coordinates":[[[130,29.5],[130.5,29.5],[130.5,30],[130,30],[130,29.5]]]}},{"type":"Feature","properties":{"zone_name":"802"},"geometry":{"type":"Polygon","coordinates":[[[130.5,29.5],[131,29.5],[131,30],[130.5,30],[130.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"803"},"geometry":{"type":"Polygon","coordinates":[[[131,29.5],[131.5,29.5],[131.5,30],[131,30],[131,29.5]]]}},{"type":"Feature","properties":{"zone_name":"804"},"geometry":{"type":"Polygon","coordinates":[[[131.5,29.5],[132,29.5],[132,30],[131.5,30],[131.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"805"},"geometry":{"type":"Polygon","coordinates":[[[132,29.5],[132.5,29.5],[132.5,30],[132,30],[132,29.5]]]}},{"type":"Feature","properties":{"zone_name":"806"},"geometry":{"type":"Polygon","coordinates":[[[132.5,29.5],[133,29.5],[133,30],[132.5,30],[132.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"807"},"geometry":{"type":"Polygon","coordinates":[[[133,29.5],[133.5,29.5],[133.5,30],[133,30],[133,29.5]]]}},{"type":"Feature","properties":{"zone_name":"808"},"geometry":{"type":"Polygon","coordinates":[[[133.5,29.5],[134,29.5],[134,30],[133.5,30],[133.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"809"},"geometry":{"type":"Polygon","coordinates":[[[134,29.5],[134.5,29.5],[134.5,30],[134,30],[134,29.5]]]}},{"type":"Feature","properties":{"zone_name":"810"},"geometry":{"type":"Polygon","coordinates":[[[134.5,29.5],[135,29.5],[135,30],[134.5,30],[134.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"811"},"geometry":{"type":"Polygon","coordinates":[[[130,29],[130.5,29],[130.5,29.5],[130,29.5],[130,29]]]}},{"type":"Feature","properties":{"zone_name":"812"},"geometry":{"type":"Polygon","coordinates":[[[130.5,29],[131,29],[131,29.5],[130.5,29.5],[130.5,29]]]}},{"type":"Feature","properties":{"zone_name":"813"},"geometry":{"type":"Polygon","coordinates":[[[131,29],[131.5,29],[131.5,29.5],[131,29.5],[131,29]]]}},{"type":"Feature","properties":{"zone_name":"814"},"geometry":{"type":"Polygon","coordinates":[[[131.5,29],[132,29],[132,29.5],[131.5,29.5],[131.5,29]]]}},{"type":"Feature","properties":{"zone_name":"815"},"geometry":{"type":"Polygon","coordinates":[[[132,29],[132.5,29],[132.5,29.5],[132,29.5],[132,29]]]}},{"type":"Feature","properties":{"zone_name":"816"},"geometry":{"type":"Polygon","coordinates":[[[132.5,29],[133,29],[133,29.5],[132.5,29.5],[132.5,29]]]}},{"type":"Feature","properties":{"zone_name":"817"},"geometry":{"type":"Polygon","coordinates":[[[133,29],[133.5,29],[133.5,29.5],[133,29.5],[133,29]]]}},{"type":"Feature","properties":{"zone_name":"818"},"geometry":{"type":"Polygon","coordinates":[[[133.5,29],[134,29],[134,29.5],[133.5,29.5],[133.5,29]]]}},{"type":"Feature","properties":{"zone_name":"819"},"geometry":{"type":"Polygon","coordinates":[[[134,29],[134.5,29],[134.5,29.5],[134,29.5],[134,29]]]}},{"type":"Feature","properties":{"zone_name":"820"},"geometry":{"type":"Polygon","coordinates":[[[134.5,29],[135,29],[135,29.5],[134.5,29.5],[134.5,29]]]}},{"type":"Feature","properties":{"zone_name":"821"},"geometry":{"type":"Polygon","coordinates":[[[130,28.5],[130.5,28.5],[130.5,29],[130,29],[130,28.5]]]}},{"type":"Feature","properties":{"zone_name":"822"},"geometry":{"type":"Polygon","coordinates":[[[130.5,28.5],[131,28.5],[131,29],[130.5,29],[130.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"823"},"geometry":{"type":"Polygon","coordinates":[[[131,28.5],[131.5,28.5],[131.5,29],[131,29],[131,28.5]]]}},{"type":"Feature","properties":{"zone_name":"824"},"geometry":{"type":"Polygon","coordinates":[[[131.5,28.5],[132,28.5],[132,29],[131.5,29],[131.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"825"},"geometry":{"type":"Polygon","coordinates":[[[132,28.5],[132.5,28.5],[132.5,29],[132,29],[132,28.5]]]}},{"type":"Feature","properties":{"zone_name":"826"},"geometry":{"type":"Polygon","coordinates":[[[132.5,28.5],[133,28.5],[133,29],[132.5,29],[132.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"827"},"geometry":{"type":"Polygon","coordinates":[[[133,28.5],[133.5,28.5],[133.5,29],[133,29],[133,28.5]]]}},{"type":"Feature","properties":{"zone_name":"828"},"geometry":{"type":"Polygon","coordinates":[[[133.5,28.5],[134,28.5],[134,29],[133.5,29],[133.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"829"},"geometry":{"type":"Polygon","coordinates":[[[134,28.5],[134.5,28.5],[134.5,29],[134,29],[134,28.5]]]}},{"type":"Feature","properties":{"zone_name":"830"},"geometry":{"type":"Polygon","coordinates":[[[134.5,28.5],[135,28.5],[135,29],[134.5,29],[134.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"831"},"geometry":{"type":"Polygon","coordinates":[[[130,28],[130.5,28],[130.5,28.5],[130,28.5],[130,28]]]}},{"type":"Feature","properties":{"zone_name":"832"},"geometry":{"type":"Polygon","coordinates":[[[130.5,28],[131,28],[131,28.5],[130.5,28.5],[130.5,28]]]}},{"type":"Feature","properties":{"zone_name":"833"},"geometry":{"type":"Polygon","coordinates":[[[131,28],[131.5,28],[131.5,28.5],[131,28.5],[131,28]]]}},{"type":"Feature","properties":{"zone_name":"834"},"geometry":{"type":"Polygon","coordinates":[[[131.5,28],[132,28],[132,28.5],[131.5,28.5],[131.5,28]]]}},{"type":"Feature","properties":{"zone_name":"835"},"geometry":{"type":"Polygon","coordinates":[[[132,28],[132.5,28],[132.5,28.5],[132,28.5],[132,28]]]}},{"type":"Feature","properties":{"zone_name":"836"},"geometry":{"type":"Polygon","coordinates":[[[132.5,28],[133,28],[133,28.5],[132.5,28.5],[132.5,28]]]}},{"type":"Feature","properties":{"zone_name":"837"},"geometry":{"type":"Polygon","coordinates":[[[133,28],[133.5,28],[133.5,28.5],[133,28.5],[133,28]]]}},{"type":"Feature","properties":{"zone_name":"838"},"geometry":{"type":"Polygon","coordinates":[[[133.5,28],[134,28],[134,28.5],[133.5,28.5],[133.5,28]]]}},{"type":"Feature","properties":{"zone_name":"839"},"geometry":{"type":"Polygon","coordinates":[[[134,28],[134.5,28],[134.5,28.5],[134,28.5],[134,28]]]}},{"type":"Feature","properties":{"zone_name":"840"},"geometry":{"type":"Polygon","coordinates":[[[134.5,28],[135,28],[135,28.5],[134.5,28.5],[134.5,28]]]}},{"type":"Feature","properties":{"zone_name":"841"},"geometry":{"type":"Polygon","coordinates":[[[130,27.5],[130.5,27.5],[130.5,28],[130,28],[130,27.5]]]}},{"type":"Feature","properties":{"zone_name":"842"},"geometry":{"type":"Polygon","coordinates":[[[130.5,27.5],[131,27.5],[131,28],[130.5,28],[130.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"843"},"geometry":{"type":"Polygon","coordinates":[[[131,27.5],[131.5,27.5],[131.5,28],[131,28],[131,27.5]]]}},{"type":"Feature","properties":{"zone_name":"844"},"geometry":{"type":"Polygon","coordinates":[[[131.5,27.5],[132,27.5],[132,28],[131.5,28],[131.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"845"},"geometry":{"type":"Polygon","coordinates":[[[132,27.5],[132.5,27.5],[132.5,28],[132,28],[132,27.5]]]}},{"type":"Feature","properties":{"zone_name":"846"},"geometry":{"type":"Polygon","coordinates":[[[132.5,27.5],[133,27.5],[133,28],[132.5,28],[132.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"847"},"geometry":{"type":"Polygon","coordinates":[[[133,27.5],[133.5,27.5],[133.5,28],[133,28],[133,27.5]]]}},{"type":"Feature","properties":{"zone_name":"848"},"geometry":{"type":"Polygon","coordinates":[[[133.5,27.5],[134,27.5],[134,28],[133.5,28],[133.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"849"},"geometry":{"type":"Polygon","coordinates":[[[134,27.5],[134.5,27.5],[134.5,28],[134,28],[134,27.5]]]}},{"type":"Feature","properties":{"zone_name":"850"},"geometry":{"type":"Polygon","coordinates":[[[134.5,27.5],[135,27.5],[135,28],[134.5,28],[134.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"851"},"geometry":{"type":"Polygon","coordinates":[[[130,27],[130.5,27],[130.5,27.5],[130,27.5],[130,27]]]}},{"type":"Feature","properties":{"zone_name":"852"},"geometry":{"type":"Polygon","coordinates":[[[130.5,27],[131,27],[131,27.5],[130.5,27.5],[130.5,27]]]}},{"type":"Feature","properties":{"zone_name":"853"},"geometry":{"type":"Polygon","coordinates":[[[131,27],[131.5,27],[131.5,27.5],[131,27.5],[131,27]]]}},{"type":"Feature","properties":{"zone_name":"854"},"geometry":{"type":"Polygon","coordinates":[[[131.5,27],[132,27],[132,27.5],[131.5,27.5],[131.5,27]]]}},{"type":"Feature","properties":{"zone_name":"855"},"geometry":{"type":"Polygon","coordinates":[[[132,27],[132.5,27],[132.5,27.5],[132,27.5],[132,27]]]}},{"type":"Feature","properties":{"zone_name":"856"},"geometry":{"type":"Polygon","coordinates":[[[132.5,27],[133,27],[133,27.5],[132.5,27.5],[132.5,27]]]}},{"type":"Feature","properties":{"zone_name":"857"},"geometry":{"type":"Polygon","coordinates":[[[133,27],[133.5,27],[133.5,27.5],[133,27.5],[133,27]]]}},{"type":"Feature","properties":{"zone_name":"858"},"geometry":{"type":"Polygon","coordinates":[[[133.5,27],[134,27],[134,27.5],[133.5,27.5],[133.5,27]]]}},{"type":"Feature","properties":{"zone_name":"859"},"geometry":{"type":"Polygon","coordinates":[[[134,27],[134.5,27],[134.5,27.5],[134,27.5],[134,27]]]}},{"type":"Feature","properties":{"zone_name":"860"},"geometry":{"type":"Polygon","coordinates":[[[134.5,27],[135,27],[135,27.5],[134.5,27.5],[134.5,27]]]}},{"type":"Feature","properties":{"zone_name":"861"},"geometry":{"type":"Polygon","coordinates":[[[130,26.5],[130.5,26.5],[130.5,27],[130,27],[130,26.5]]]}},{"type":"Feature","properties":{"zone_name":"862"},"geometry":{"type":"Polygon","coordinates":[[[130.5,26.5],[131,26.5],[131,27],[130.5,27],[130.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"863"},"geometry":{"type":"Polygon","coordinates":[[[131,26.5],[131.5,26.5],[131.5,27],[131,27],[131,26.5]]]}},{"type":"Feature","properties":{"zone_name":"864"},"geometry":{"type":"Polygon","coordinates":[[[131.5,26.5],[132,26.5],[132,27],[131.5,27],[131.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"865"},"geometry":{"type":"Polygon","coordinates":[[[132,26.5],[132.5,26.5],[132.5,27],[132,27],[132,26.5]]]}},{"type":"Feature","properties":{"zone_name":"866"},"geometry":{"type":"Polygon","coordinates":[[[132.5,26.5],[133,26.5],[133,27],[132.5,27],[132.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"867"},"geometry":{"type":"Polygon","coordinates":[[[133,26.5],[133.5,26.5],[133.5,27],[133,27],[133,26.5]]]}},{"type":"Feature","properties":{"zone_name":"868"},"geometry":{"type":"Polygon","coordinates":[[[133.5,26.5],[134,26.5],[134,27],[133.5,27],[133.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"869"},"geometry":{"type":"Polygon","coordinates":[[[134,26.5],[134.5,26.5],[134.5,27],[134,27],[134,26.5]]]}},{"type":"Feature","properties":{"zone_name":"870"},"geometry":{"type":"Polygon","coordinates":[[[134.5,26.5],[135,26.5],[135,27],[134.5,27],[134.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"871"},"geometry":{"type":"Polygon","coordinates":[[[130,26],[130.5,26],[130.5,26.5],[130,26.5],[130,26]]]}},{"type":"Feature","properties":{"zone_name":"872"},"geometry":{"type":"Polygon","coordinates":[[[130.5,26],[131,26],[131,26.5],[130.5,26.5],[130.5,26]]]}},{"type":"Feature","properties":{"zone_name":"873"},"geometry":{"type":"Polygon","coordinates":[[[131,26],[131.5,26],[131.5,26.5],[131,26.5],[131,26]]]}},{"type":"Feature","properties":{"zone_name":"874"},"geometry":{"type":"Polygon","coordinates":[[[131.5,26],[132,26],[132,26.5],[131.5,26.5],[131.5,26]]]}},{"type":"Feature","properties":{"zone_name":"875"},"geometry":{"type":"Polygon","coordinates":[[[132,26],[132.5,26],[132.5,26.5],[132,26.5],[132,26]]]}},{"type":"Feature","properties":{"zone_name":"876"},"geometry":{"type":"Polygon","coordinates":[[[132.5,26],[133,26],[133,26.5],[132.5,26.5],[132.5,26]]]}},{"type":"Feature","properties":{"zone_name":"877"},"geometry":{"type":"Polygon","coordinates":[[[133,26],[133.5,26],[133.5,26.5],[133,26.5],[133,26]]]}},{"type":"Feature","properties":{"zone_name":"878"},"geometry":{"type":"Polygon","coordinates":[[[133.5,26],[134,26],[134,26.5],[133.5,26.5],[133.5,26]]]}},{"type":"Feature","properties":{"zone_name":"879"},"geometry":{"type":"Polygon","coordinates":[[[134,26],[134.5,26],[134.5,26.5],[134,26.5],[134,26]]]}},{"type":"Feature","properties":{"zone_name":"880"},"geometry":{"type":"Polygon","coordinates":[[[134.5,26],[135,26],[135,26.5],[134.5,26.5],[134.5,26]]]}},{"type":"Feature","properties":{"zone_name":"881"},"geometry":{"type":"Polygon","coordinates":[[[130,25.5],[130.5,25.5],[130.5,26],[130,26],[130,25.5]]]}},{"type":"Feature","properties":{"zone_name":"882"},"geometry":{"type":"Polygon","coordinates":[[[130.5,25.5],[131,25.5],[131,26],[130.5,26],[130.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"883"},"geometry":{"type":"Polygon","coordinates":[[[131,25.5],[131.5,25.5],[131.5,26],[131,26],[131,25.5]]]}},{"type":"Feature","properties":{"zone_name":"884"},"geometry":{"type":"Polygon","coordinates":[[[131.5,25.5],[132,25.5],[132,26],[131.5,26],[131.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"885"},"geometry":{"type":"Polygon","coordinates":[[[132,25.5],[132.5,25.5],[132.5,26],[132,26],[132,25.5]]]}},{"type":"Feature","properties":{"zone_name":"886"},"geometry":{"type":"Polygon","coordinates":[[[132.5,25.5],[133,25.5],[133,26],[132.5,26],[132.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"887"},"geometry":{"type":"Polygon","coordinates":[[[133,25.5],[133.5,25.5],[133.5,26],[133,26],[133,25.5]]]}},{"type":"Feature","properties":{"zone_name":"888"},"geometry":{"type":"Polygon","coordinates":[[[133.5,25.5],[134,25.5],[134,26],[133.5,26],[133.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"889"},"geometry":{"type":"Polygon","coordinates":[[[134,25.5],[134.5,25.5],[134.5,26],[134,26],[134,25.5]]]}},{"type":"Feature","properties":{"zone_name":"890"},"geometry":{"type":"Polygon","coordinates":[[[134.5,25.5],[135,25.5],[135,26],[134.5,26],[134.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"891"},"geometry":{"type":"Polygon","coordinates":[[[130,25],[130.5,25],[130.5,25.5],[130,25.5],[130,25]]]}},{"type":"Feature","properties":{"zone_name":"892"},"geometry":{"type":"Polygon","coordinates":[[[130.5,25],[131,25],[131,25.5],[130.5,25.5],[130.5,25]]]}},{"type":"Feature","properties":{"zone_name":"893"},"geometry":{"type":"Polygon","coordinates":[[[131,25],[131.5,25],[131.5,25.5],[131,25.5],[131,25]]]}},{"type":"Feature","properties":{"zone_name":"894"},"geometry":{"type":"Polygon","coordinates":[[[131.5,25],[132,25],[132,25.5],[131.5,25.5],[131.5,25]]]}},{"type":"Feature","properties":{"zone_name":"895"},"geometry":{"type":"Polygon","coordinates":[[[132,25],[132.5,25],[132.5,25.5],[132,25.5],[132,25]]]}},{"type":"Feature","properties":{"zone_name":"896"},"geometry":{"type":"Polygon","coordinates":[[[132.5,25],[133,25],[133,25.5],[132.5,25.5],[132.5,25]]]}},{"type":"Feature","properties":{"zone_name":"897"},"geometry":{"type":"Polygon","coordinates":[[[133,25],[133.5,25],[133.5,25.5],[133,25.5],[133,25]]]}},{"type":"Feature","properties":{"zone_name":"898"},"geometry":{"type":"Polygon","coordinates":[[[133.5,25],[134,25],[134,25.5],[133.5,25.5],[133.5,25]]]}},{"type":"Feature","properties":{"zone_name":"899"},"geometry":{"type":"Polygon","coordinates":[[[134,25],[134.5,25],[134.5,25.5],[134,25.5],[134,25]]]}},{"type":"Feature","properties":{"zone_name":"900"},"geometry":{"type":"Polygon","coordinates":[[[134.5,25],[135,25],[135,25.5],[134.5,25.5],[134.5,25]]]}},{"type":"Feature","properties":{"zone_name":"901"},"geometry":{"type":"Polygon","coordinates":[[[134,35.5],[134.5,35.5],[134.5,36],[134,36],[134,35.5]]]}},{"type":"Feature","properties":{"zone_name":"902"},"geometry":{"type":"Polygon","coordinates":[[[134.5,35.5],[135,35.5],[135,36],[134.5,36],[134.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"903"},"geometry":{"type":"Polygon","coordinates":[[[135,35.5],[135.5,35.5],[135.5,36],[135,36],[135,35.5]]]}},{"type":"Feature","properties":{"zone_name":"904"},"geometry":{"type":"Polygon","coordinates":[[[135.5,35.5],[136,35.5],[136,36],[135.5,36],[135.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"905"},"geometry":{"type":"Polygon","coordinates":[[[134,36],[134.5,36],[134.5,36.5],[134,36.5],[134,36]]]}},{"type":"Feature","properties":{"zone_name":"906"},"geometry":{"type":"Polygon","coordinates":[[[134.5,36],[135,36],[135,36.5],[134.5,36.5],[134.5,36]]]}},{"type":"Feature","properties":{"zone_name":"907"},"geometry":{"type":"Polygon","coordinates":[[[135,36],[135.5,36],[135.5,36.5],[135,36.5],[135,36]]]}},{"type":"Feature","properties":{"zone_name":"908"},"geometry":{"type":"Polygon","coordinates":[[[135.5,36],[136,36],[136,36.5],[135.5,36.5],[135.5,36]]]}},{"type":"Feature","properties":{"zone_name":"909"},"geometry":{"type":"Polygon","coordinates":[[[136,36],[136.5,36],[136.5,36.5],[136,36.5],[136,36]]]}},{"type":"Feature","properties":{"zone_name":"910"},"geometry":{"type":"Polygon","coordinates":[[[134,36.5],[134.5,36.5],[134.5,37],[134,37],[134,36.5]]]}},{"type":"Feature","properties":{"zone_name":"911"},"geometry":{"type":"Polygon","coordinates":[[[134.5,36.5],[135,36.5],[135,37],[134.5,37],[134.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"912"},"geometry":{"type":"Polygon","coordinates":[[[135,36.5],[135.5,36.5],[135.5,37],[135,37],[135,36.5]]]}},{"type":"Feature","properties":{"zone_name":"913"},"geometry":{"type":"Polygon","coordinates":[[[135.5,36.5],[136,36.5],[136,37],[135.5,37],[135.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"914"},"geometry":{"type":"Polygon","coordinates":[[[136,36.5],[136.5,36.5],[136.5,37],[136,37],[136,36.5]]]}},{"type":"Feature","properties":{"zone_name":"915"},"geometry":{"type":"Polygon","coordinates":[[[136.5,36.5],[137,36.5],[137,37],[136.5,37],[136.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"916"},"geometry":{"type":"Polygon","coordinates":[[[134,37],[134.5,37],[134.5,37.5],[134,37.5],[134,37]]]}},{"type":"Feature","properties":{"zone_name":"917"},"geometry":{"type":"Polygon","coordinates":[[[134.5,37],[135,37],[135,37.5],[134.5,37.5],[134.5,37]]]}},{"type":"Feature","properties":{"zone_name":"918"},"geometry":{"type":"Polygon","coordinates":[[[135,37],[135.5,37],[135.5,37.5],[135,37.5],[135,37]]]}},{"type":"Feature","properties":{"zone_name":"919"},"geometry":{"type":"Polygon","coordinates":[[[135.5,37],[136,37],[136,37.5],[135.5,37.5],[135.5,37]]]}},{"type":"Feature","properties":{"zone_name":"920"},"geometry":{"type":"Polygon","coordinates":[[[136,37],[136.5,37],[136.5,37.5],[136,37.5],[136,37]]]}},{"type":"Feature","properties":{"zone_name":"921"},"geometry":{"type":"Polygon","coordinates":[[[136.5,37],[137,37],[137,37.5],[136.5,37.5],[136.5,37]]]}},{"type":"Feature","properties":{"zone_name":"922"},"geometry":{"type":"Polygon","coordinates":[[[137,37],[137.5,37],[137.5,37.5],[137,37.5],[137,37]]]}},{"type":"Feature","properties":{"zone_name":"923"},"geometry":{"type":"Polygon","coordinates":[[[137.5,37],[138,37],[138,37.5],[137.5,37.5],[137.5,37]]]}},{"type":"Feature","properties":{"zone_name":"924"},"geometry":{"type":"Polygon","coordinates":[[[134,37.5],[134.5,37.5],[134.5,38],[134,38],[134,37.5]]]}},{"type":"Feature","properties":{"zone_name":"925"},"geometry":{"type":"Polygon","coordinates":[[[134.5,37.5],[135,37.5],[135,38],[134.5,38],[134.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"926"},"geometry":{"type":"Polygon","coordinates":[[[135,37.5],[135.5,37.5],[135.5,38],[135,38],[135,37.5]]]}},{"type":"Feature","properties":{"zone_name":"927"},"geometry":{"type":"Polygon","coordinates":[[[135.5,37.5],[136,37.5],[136,38],[135.5,38],[135.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"928"},"geometry":{"type":"Polygon","coordinates":[[[136,37.5],[136.5,37.5],[136.5,38],[136,38],[136,37.5]]]}},{"type":"Feature","properties":{"zone_name":"929"},"geometry":{"type":"Polygon","coordinates":[[[136.5,37.5],[137,37.5],[137,38],[136.5,38],[136.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"930"},"geometry":{"type":"Polygon","coordinates":[[[137,37.5],[137.5,37.5],[137.5,38],[137,38],[137,37.5]]]}},{"type":"Feature","properties":{"zone_name":"931"},"geometry":{"type":"Polygon","coordinates":[[[137.5,37.5],[138,37.5],[138,38],[137.5,38],[137.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"932"},"geometry":{"type":"Polygon","coordinates":[[[134,38],[134.5,38],[134.5,38.5],[134,38.5],[134,38]]]}},{"type":"Feature","properties":{"zone_name":"933"},"geometry":{"type":"Polygon","coordinates":[[[134.5,38],[135,38],[135,38.5],[134.5,38.5],[134.5,38]]]}},{"type":"Feature","properties":{"zone_name":"934"},"geometry":{"type":"Polygon","coordinates":[[[135,38],[135.5,38],[135.5,38.5],[135,38.5],[135,38]]]}},{"type":"Feature","properties":{"zone_name":"935"},"geometry":{"type":"Polygon","coordinates":[[[135.5,38],[136,38],[136,38.5],[135.5,38.5],[135.5,38]]]}},{"type":"Feature","properties":{"zone_name":"936"},"geometry":{"type":"Polygon","coordinates":[[[136,38],[136.5,38],[136.5,38.5],[136,38.5],[136,38]]]}},{"type":"Feature","properties":{"zone_name":"937"},"geometry":{"type":"Polygon","coordinates":[[[136.5,38],[137,38],[137,38.5],[136.5,38.5],[136.5,38]]]}},{"type":"Feature","properties":{"zone_name":"938"},"geometry":{"type":"Polygon","coordinates":[[[137,38],[137.5,38],[137.5,38.5],[137,38.5],[137,38]]]}},{"type":"Feature","properties":{"zone_name":"939"},"geometry":{"type":"Polygon","coordinates":[[[137.5,38],[138,38],[138,38.5],[137.5,38.5],[137.5,38]]]}},{"type":"Feature","properties":{"zone_name":"940"},"geometry":{"type":"Polygon","coordinates":[[[134,38.5],[134.5,38.5],[134.5,39],[134,39],[134,38.5]]]}},{"type":"Feature","properties":{"zone_name":"941"},"geometry":{"type":"Polygon","coordinates":[[[134.5,38.5],[135,38.5],[135,39],[134.5,39],[134.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"942"},"geometry":{"type":"Polygon","coordinates":[[[135,38.5],[135.5,38.5],[135.5,39],[135,39],[135,38.5]]]}},{"type":"Feature","properties":{"zone_name":"943"},"geometry":{"type":"Polygon","coordinates":[[[135.5,38.5],[136,38.5],[136,39],[135.5,39],[135.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"944"},"geometry":{"type":"Polygon","coordinates":[[[136,38.5],[136.5,38.5],[136.5,39],[136,39],[136,38.5]]]}},{"type":"Feature","properties":{"zone_name":"945"},"geometry":{"type":"Polygon","coordinates":[[[136.5,38.5],[137,38.5],[137,39],[136.5,39],[136.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"946"},"geometry":{"type":"Polygon","coordinates":[[[137,38.5],[137.5,38.5],[137.5,39],[137,39],[137,38.5]]]}},{"type":"Feature","properties":{"zone_name":"947"},"geometry":{"type":"Polygon","coordinates":[[[137.5,38.5],[138,38.5],[138,39],[137.5,39],[137.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"948"},"geometry":{"type":"Polygon","coordinates":[[[134,39],[134.5,39],[134.5,39.5],[134,39.5],[134,39]]]}},{"type":"Feature","properties":{"zone_name":"949"},"geometry":{"type":"Polygon","coordinates":[[[134.5,39],[135,39],[135,39.5],[134.5,39.5],[134.5,39]]]}},{"type":"Feature","properties":{"zone_name":"950"},"geometry":{"type":"Polygon","coordinates":[[[135,39],[135.5,39],[135.5,39.5],[135,39.5],[135,39]]]}},{"type":"Feature","properties":{"zone_name":"951"},"geometry":{"type":"Polygon","coordinates":[[[135.5,39],[136,39],[136,39.5],[135.5,39.5],[135.5,39]]]}},{"type":"Feature","properties":{"zone_name":"952"},"geometry":{"type":"Polygon","coordinates":[[[136,39],[136.5,39],[136.5,39.5],[136,39.5],[136,39]]]}},{"type":"Feature","properties":{"zone_name":"953"},"geometry":{"type":"Polygon","coordinates":[[[136.5,39],[137,39],[137,39.5],[136.5,39.5],[136.5,39]]]}},{"type":"Feature","properties":{"zone_name":"954"},"geometry":{"type":"Polygon","coordinates":[[[137,39],[137.5,39],[137.5,39.5],[137,39.5],[137,39]]]}},{"type":"Feature","properties":{"zone_name":"955"},"geometry":{"type":"Polygon","coordinates":[[[137.5,39],[138,39],[138,39.5],[137.5,39.5],[137.5,39]]]}},{"type":"Feature","properties":{"zone_name":"956"},"geometry":{"type":"Polygon","coordinates":[[[134,39.5],[134.5,39.5],[134.5,40],[134,40],[134,39.5]]]}},{"type":"Feature","properties":{"zone_name":"957"},"geometry":{"type":"Polygon","coordinates":[[[134.5,39.5],[135,39.5],[135,40],[134.5,40],[134.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"958"},"geometry":{"type":"Polygon","coordinates":[[[135,39.5],[135.5,39.5],[135.5,40],[135,40],[135,39.5]]]}},{"type":"Feature","properties":{"zone_name":"959"},"geometry":{"type":"Polygon","coordinates":[[[135.5,39.5],[136,39.5],[136,40],[135.5,40],[135.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"960"},"geometry":{"type":"Polygon","coordinates":[[[136,39.5],[136.5,39.5],[136.5,40],[136,40],[136,39.5]]]}},{"type":"Feature","properties":{"zone_name":"961"},"geometry":{"type":"Polygon","coordinates":[[[136.5,39.5],[137,39.5],[137,40],[136.5,40],[136.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"962"},"geometry":{"type":"Polygon","coordinates":[[[137,39.5],[137.5,39.5],[137.5,40],[137,40],[137,39.5]]]}},{"type":"Feature","properties":{"zone_name":"963"},"geometry":{"type":"Polygon","coordinates":[[[137.5,39.5],[138,39.5],[138,40],[137.5,40],[137.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"964"},"geometry":{"type":"Polygon","coordinates":[[[134,40],[134.5,40],[134.5,40.5],[134,40.5],[134,40]]]}},{"type":"Feature","properties":{"zone_name":"965"},"geometry":{"type":"Polygon","coordinates":[[[134.5,40],[135,40],[135,40.5],[134.5,40.5],[134.5,40]]]}},{"type":"Feature","properties":{"zone_name":"966"},"geometry":{"type":"Polygon","coordinates":[[[135,40],[135.5,40],[135.5,40.5],[135,40.5],[135,40]]]}},{"type":"Feature","properties":{"zone_name":"967"},"geometry":{"type":"Polygon","coordinates":[[[135.5,40],[136,40],[136,40.5],[135.5,40.5],[135.5,40]]]}},{"type":"Feature","properties":{"zone_name":"968"},"geometry":{"type":"Polygon","coordinates":[[[136,40],[136.5,40],[136.5,40.5],[136,40.5],[136,40]]]}},{"type":"Feature","properties":{"zone_name":"969"},"geometry":{"type":"Polygon","coordinates":[[[136.5,40],[137,40],[137,40.5],[136.5,40.5],[136.5,40]]]}},{"type":"Feature","properties":{"zone_name":"970"},"geometry":{"type":"Polygon","coordinates":[[[137,40],[137.5,40],[137.5,40.5],[137,40.5],[137,40]]]}},{"type":"Feature","properties":{"zone_name":"971"},"geometry":{"type":"Polygon","coordinates":[[[137.5,40],[138,40],[138,40.5],[137.5,40.5],[137.5,40]]]}},{"type":"Feature","properties":{"zone_name":"972"},"geometry":{"type":"Polygon","coordinates":[[[134,40.5],[134.5,40.5],[134.5,41],[134,41],[134,40.5]]]}},{"type":"Feature","properties":{"zone_name":"973"},"geometry":{"type":"Polygon","coordinates":[[[134.5,40.5],[135,40.5],[135,41],[134.5,41],[134.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"974"},"geometry":{"type":"Polygon","coordinates":[[[135,40.5],[135.5,40.5],[135.5,41],[135,41],[135,40.5]]]}},{"type":"Feature","properties":{"zone_name":"975"},"geometry":{"type":"Polygon","coordinates":[[[135.5,40.5],[136,40.5],[136,41],[135.5,41],[135.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"976"},"geometry":{"type":"Polygon","coordinates":[[[136,40.5],[136.5,40.5],[136.5,41],[136,41],[136,40.5]]]}},{"type":"Feature","properties":{"zone_name":"977"},"geometry":{"type":"Polygon","coordinates":[[[136.5,40.5],[137,40.5],[137,41],[136.5,41],[136.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"978"},"geometry":{"type":"Polygon","coordinates":[[[137,40.5],[137.5,40.5],[137.5,41],[137,41],[137,40.5]]]}},{"type":"Feature","properties":{"zone_name":"979"},"geometry":{"type":"Polygon","coordinates":[[[137.5,40.5],[138,40.5],[138,41],[137.5,41],[137.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"980"},"geometry":{"type":"Polygon","coordinates":[[[134,41],[134.5,41],[134.5,41.5],[134,41.5],[134,41]]]}},{"type":"Feature","properties":{"zone_name":"981"},"geometry":{"type":"Polygon","coordinates":[[[134.5,41],[135,41],[135,41.5],[134.5,41.5],[134.5,41]]]}},{"type":"Feature","properties":{"zone_name":"982"},"geometry":{"type":"Polygon","coordinates":[[[135,41],[135.5,41],[135.5,41.5],[135,41.5],[135,41]]]}},{"type":"Feature","properties":{"zone_name":"983"},"geometry":{"type":"Polygon","coordinates":[[[135.5,41],[136,41],[136,41.5],[135.5,41.5],[135.5,41]]]}},{"type":"Feature","properties":{"zone_name":"984"},"geometry":{"type":"Polygon","coordinates":[[[136,41],[136.5,41],[136.5,41.5],[136,41.5],[136,41]]]}},{"type":"Feature","properties":{"zone_name":"985"},"geometry":{"type":"Polygon","coordinates":[[[136.5,41],[137,41],[137,41.5],[136.5,41.5],[136.5,41]]]}},{"type":"Feature","properties":{"zone_name":"986"},"geometry":{"type":"Polygon","coordinates":[[[137,41],[137.5,41],[137.5,41.5],[137,41.5],[137,41]]]}},{"type":"Feature","properties":{"zone_name":"987"},"geometry":{"type":"Polygon","coordinates":[[[137.5,41],[138,41],[138,41.5],[137.5,41.5],[137.5,41]]]}},{"type":"Feature","properties":{"zone_name":"988"},"geometry":{"type":"Polygon","coordinates":[[[134,41.5],[134.5,41.5],[134.5,42],[134,42],[134,41.5]]]}},{"type":"Feature","properties":{"zone_name":"989"},"geometry":{"type":"Polygon","coordinates":[[[134.5,41.5],[135,41.5],[135,42],[134.5,42],[134.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"990"},"geometry":{"type":"Polygon","coordinates":[[[135,41.5],[135.5,41.5],[135.5,42],[135,42],[135,41.5]]]}},{"type":"Feature","properties":{"zone_name":"991"},"geometry":{"type":"Polygon","coordinates":[[[135.5,41.5],[136,41.5],[136,42],[135.5,42],[135.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"992"},"geometry":{"type":"Polygon","coordinates":[[[136,41.5],[136.5,41.5],[136.5,42],[136,42],[136,41.5]]]}},{"type":"Feature","properties":{"zone_name":"993"},"geometry":{"type":"Polygon","coordinates":[[[136.5,41.5],[137,41.5],[137,42],[136.5,42],[136.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"994"},"geometry":{"type":"Polygon","coordinates":[[[137,41.5],[137.5,41.5],[137.5,42],[137,42],[137,41.5]]]}},{"type":"Feature","properties":{"zone_name":"995"},"geometry":{"type":"Polygon","coordinates":[[[137.5,41.5],[138,41.5],[138,42],[137.5,42],[137.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"996"},"geometry":{"type":"Polygon","coordinates":[[[134,42],[134.5,42],[134.5,42.5],[134,42.5],[134,42]]]}},{"type":"Feature","properties":{"zone_name":"997"},"geometry":{"type":"Polygon","coordinates":[[[134.5,42],[135,42],[135,42.5],[134.5,42.5],[134.5,42]]]}},{"type":"Feature","properties":{"zone_name":"998"},"geometry":{"type":"Polygon","coordinates":[[[135,42],[135.5,42],[135.5,42.5],[135,42.5],[135,42]]]}},{"type":"Feature","properties":{"zone_name":"999"},"geometry":{"type":"Polygon","coordinates":[[[135.5,42],[136,42],[136,42.5],[135.5,42.5],[135.5,42]]]}},{"type":"Feature","properties":{"zone_name":"1000"},"geometry":{"type":"Polygon","coordinates":[[[136,42],[136.5,42],[136.5,42.5],[136,42.5],[136,42]]]}},{"type":"Feature","properties":{"zone_name":"1001"},"geometry":{"type":"Polygon","coordinates":[[[136.5,42],[137,42],[137,42.5],[136.5,42.5],[136.5,42]]]}},{"type":"Feature","properties":{"zone_name":"1002"},"geometry":{"type":"Polygon","coordinates":[[[137,42],[137.5,42],[137.5,42.5],[137,42.5],[137,42]]]}},{"type":"Feature","properties":{"zone_name":"1003"},"geometry":{"type":"Polygon","coordinates":[[[137.5,42],[138,42],[138,42.5],[137.5,42.5],[137.5,42]]]}},{"type":"Feature","properties":{"zone_name":"1004"},"geometry":{"type":"Polygon","coordinates":[[[134,42.5],[134.5,42.5],[134.5,43],[134,43],[134,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1005"},"geometry":{"type":"Polygon","coordinates":[[[134.5,42.5],[135,42.5],[135,43],[134.5,43],[134.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1006"},"geometry":{"type":"Polygon","coordinates":[[[135,42.5],[135.5,42.5],[135.5,43],[135,43],[135,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1007"},"geometry":{"type":"Polygon","coordinates":[[[135.5,42.5],[136,42.5],[136,43],[135.5,43],[135.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1008"},"geometry":{"type":"Polygon","coordinates":[[[136,42.5],[136.5,42.5],[136.5,43],[136,43],[136,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1009"},"geometry":{"type":"Polygon","coordinates":[[[136.5,42.5],[137,42.5],[137,43],[136.5,43],[136.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1010"},"geometry":{"type":"Polygon","coordinates":[[[137,42.5],[137.5,42.5],[137.5,43],[137,43],[137,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1011"},"geometry":{"type":"Polygon","coordinates":[[[137.5,42.5],[138,42.5],[138,43],[137.5,43],[137.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1237"},"geometry":{"type":"Polygon","coordinates":[[[138,42.5],[138.5,42.5],[138.5,43],[138,43],[138,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1238"},"geometry":{"type":"Polygon","coordinates":[[[138.5,42.5],[139,42.5],[139,43],[138.5,43],[138.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1239"},"geometry":{"type":"Polygon","coordinates":[[[139,42.5],[139.5,42.5],[139.5,43],[139,43],[139,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1240"},"geometry":{"type":"Polygon","coordinates":[[[139.5,42.5],[140,42.5],[140,43],[139.5,43],[139.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"1247"},"geometry":{"type":"Polygon","coordinates":[[[138,42],[138.5,42],[138.5,42.5],[138,42.5],[138,42]]]}},{"type":"Feature","properties":{"zone_name":"1248"},"geometry":{"type":"Polygon","coordinates":[[[138.5,42],[139,42],[139,42.5],[138.5,42.5],[138.5,42]]]}},{"type":"Feature","properties":{"zone_name":"1249"},"geometry":{"type":"Polygon","coordinates":[[[139,42],[139.5,42],[139.5,42.5],[139,42.5],[139,42]]]}},{"type":"Feature","properties":{"zone_name":"1250"},"geometry":{"type":"Polygon","coordinates":[[[139.5,42],[140,42],[140,42.5],[139.5,42.5],[139.5,42]]]}},{"type":"Feature","properties":{"zone_name":"1261"},"geometry":{"type":"Polygon","coordinates":[[[138,41.5],[138.5,41.5],[138.5,42],[138,42],[138,41.5]]]}},{"type":"Feature","properties":{"zone_name":"1262"},"geometry":{"type":"Polygon","coordinates":[[[138.5,41.5],[139,41.5],[139,42],[138.5,42],[138.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"1263"},"geometry":{"type":"Polygon","coordinates":[[[139,41.5],[139.5,41.5],[139.5,42],[139,42],[139,41.5]]]}},{"type":"Feature","properties":{"zone_name":"1264"},"geometry":{"type":"Polygon","coordinates":[[[139.5,41.5],[140,41.5],[140,42],[139.5,42],[139.5,41.5]]]}},{"type":"Feature","properties":{"zone_name":"1275"},"geometry":{"type":"Polygon","coordinates":[[[138,41],[138.5,41],[138.5,41.5],[138,41.5],[138,41]]]}},{"type":"Feature","properties":{"zone_name":"1276"},"geometry":{"type":"Polygon","coordinates":[[[138.5,41],[139,41],[139,41.5],[138.5,41.5],[138.5,41]]]}},{"type":"Feature","properties":{"zone_name":"1277"},"geometry":{"type":"Polygon","coordinates":[[[139,41],[139.5,41],[139.5,41.5],[139,41.5],[139,41]]]}},{"type":"Feature","properties":{"zone_name":"1278"},"geometry":{"type":"Polygon","coordinates":[[[139.5,41],[140,41],[140,41.5],[139.5,41.5],[139.5,41]]]}},{"type":"Feature","properties":{"zone_name":"1289"},"geometry":{"type":"Polygon","coordinates":[[[138,40.5],[138.5,40.5],[138.5,41],[138,41],[138,40.5]]]}},{"type":"Feature","properties":{"zone_name":"1290"},"geometry":{"type":"Polygon","coordinates":[[[138.5,40.5],[139,40.5],[139,41],[138.5,41],[138.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"1291"},"geometry":{"type":"Polygon","coordinates":[[[139,40.5],[139.5,40.5],[139.5,41],[139,41],[139,40.5]]]}},{"type":"Feature","properties":{"zone_name":"1292"},"geometry":{"type":"Polygon","coordinates":[[[139.5,40.5],[140,40.5],[140,41],[139.5,41],[139.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"1303"},"geometry":{"type":"Polygon","coordinates":[[[138,40],[138.5,40],[138.5,40.5],[138,40.5],[138,40]]]}},{"type":"Feature","properties":{"zone_name":"1304"},"geometry":{"type":"Polygon","coordinates":[[[138.5,40],[139,40],[139,40.5],[138.5,40.5],[138.5,40]]]}},{"type":"Feature","properties":{"zone_name":"1305"},"geometry":{"type":"Polygon","coordinates":[[[139,40],[139.5,40],[139.5,40.5],[139,40.5],[139,40]]]}},{"type":"Feature","properties":{"zone_name":"1306"},"geometry":{"type":"Polygon","coordinates":[[[139.5,40],[140,40],[140,40.5],[139.5,40.5],[139.5,40]]]}},{"type":"Feature","properties":{"zone_name":"1314"},"geometry":{"type":"Polygon","coordinates":[[[138,39.5],[138.5,39.5],[138.5,40],[138,40],[138,39.5]]]}},{"type":"Feature","properties":{"zone_name":"1315"},"geometry":{"type":"Polygon","coordinates":[[[138.5,39.5],[139,39.5],[139,40],[138.5,40],[138.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"1316"},"geometry":{"type":"Polygon","coordinates":[[[139,39.5],[139.5,39.5],[139.5,40],[139,40],[139,39.5]]]}},{"type":"Feature","properties":{"zone_name":"1317"},"geometry":{"type":"Polygon","coordinates":[[[139.5,39.5],[140,39.5],[140,40],[139.5,40],[139.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"1325"},"geometry":{"type":"Polygon","coordinates":[[[138,39],[138.5,39],[138.5,39.5],[138,39.5],[138,39]]]}},{"type":"Feature","properties":{"zone_name":"1326"},"geometry":{"type":"Polygon","coordinates":[[[138.5,39],[139,39],[139,39.5],[138.5,39.5],[138.5,39]]]}},{"type":"Feature","properties":{"zone_name":"1327"},"geometry":{"type":"Polygon","coordinates":[[[139,39],[139.5,39],[139.5,39.5],[139,39.5],[139,39]]]}},{"type":"Feature","properties":{"zone_name":"1328"},"geometry":{"type":"Polygon","coordinates":[[[139.5,39],[140,39],[140,39.5],[139.5,39.5],[139.5,39]]]}},{"type":"Feature","properties":{"zone_name":"1336"},"geometry":{"type":"Polygon","coordinates":[[[138,38.5],[138.5,38.5],[138.5,39],[138,39],[138,38.5]]]}},{"type":"Feature","properties":{"zone_name":"1337"},"geometry":{"type":"Polygon","coordinates":[[[138.5,38.5],[139,38.5],[139,39],[138.5,39],[138.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"1338"},"geometry":{"type":"Polygon","coordinates":[[[139,38.5],[139.5,38.5],[139.5,39],[139,39],[139,38.5]]]}},{"type":"Feature","properties":{"zone_name":"1339"},"geometry":{"type":"Polygon","coordinates":[[[139.5,38.5],[140,38.5],[140,39],[139.5,39],[139.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"1347"},"geometry":{"type":"Polygon","coordinates":[[[138,38],[138.5,38],[138.5,38.5],[138,38.5],[138,38]]]}},{"type":"Feature","properties":{"zone_name":"1348"},"geometry":{"type":"Polygon","coordinates":[[[138.5,38],[139,38],[139,38.5],[138.5,38.5],[138.5,38]]]}},{"type":"Feature","properties":{"zone_name":"1349"},"geometry":{"type":"Polygon","coordinates":[[[139,38],[139.5,38],[139.5,38.5],[139,38.5],[139,38]]]}},{"type":"Feature","properties":{"zone_name":"1358"},"geometry":{"type":"Polygon","coordinates":[[[138,37.5],[138.5,37.5],[138.5,38],[138,38],[138,37.5]]]}},{"type":"Feature","properties":{"zone_name":"1359"},"geometry":{"type":"Polygon","coordinates":[[[138.5,37.5],[139,37.5],[139,38],[138.5,38],[138.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"1368"},"geometry":{"type":"Polygon","coordinates":[[[138,37],[138.5,37],[138.5,37.5],[138,37.5],[138,37]]]}},{"type":"Feature","properties":{"zone_name":"1420"},"geometry":{"type":"Polygon","coordinates":[[[139,34.5],[139.5,34.5],[139.5,35],[139,35],[139,34.5]]]}},{"type":"Feature","properties":{"zone_name":"1421"},"geometry":{"type":"Polygon","coordinates":[[[139.5,34.5],[140,34.5],[140,35],[139.5,35],[139.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"1432"},"geometry":{"type":"Polygon","coordinates":[[[138,34],[138.5,34],[138.5,34.5],[138,34.5],[138,34]]]}},{"type":"Feature","properties":{"zone_name":"1433"},"geometry":{"type":"Polygon","coordinates":[[[138.5,34],[139,34],[139,34.5],[138.5,34.5],[138.5,34]]]}},{"type":"Feature","properties":{"zone_name":"1434"},"geometry":{"type":"Polygon","coordinates":[[[139,34],[139.5,34],[139.5,34.5],[139,34.5],[139,34]]]}},{"type":"Feature","properties":{"zone_name":"1435"},"geometry":{"type":"Polygon","coordinates":[[[139.5,34],[140,34],[140,34.5],[139.5,34.5],[139.5,34]]]}},{"type":"Feature","properties":{"zone_name":"1446"},"geometry":{"type":"Polygon","coordinates":[[[138,33.5],[138.5,33.5],[138.5,34],[138,34],[138,33.5]]]}},{"type":"Feature","properties":{"zone_name":"1447"},"geometry":{"type":"Polygon","coordinates":[[[138.5,33.5],[139,33.5],[139,34],[138.5,34],[138.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"1448"},"geometry":{"type":"Polygon","coordinates":[[[139,33.5],[139.5,33.5],[139.5,34],[139,34],[139,33.5]]]}},{"type":"Feature","properties":{"zone_name":"1449"},"geometry":{"type":"Polygon","coordinates":[[[139.5,33.5],[140,33.5],[140,34],[139.5,34],[139.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"1460"},"geometry":{"type":"Polygon","coordinates":[[[138,33],[138.5,33],[138.5,33.5],[138,33.5],[138,33]]]}},{"type":"Feature","properties":{"zone_name":"1461"},"geometry":{"type":"Polygon","coordinates":[[[138.5,33],[139,33],[139,33.5],[138.5,33.5],[138.5,33]]]}},{"type":"Feature","properties":{"zone_name":"1462"},"geometry":{"type":"Polygon","coordinates":[[[139,33],[139.5,33],[139.5,33.5],[139,33.5],[139,33]]]}},{"type":"Feature","properties":{"zone_name":"1463"},"geometry":{"type":"Polygon","coordinates":[[[139.5,33],[140,33],[140,33.5],[139.5,33.5],[139.5,33]]]}},{"type":"Feature","properties":{"zone_name":"1474"},"geometry":{"type":"Polygon","coordinates":[[[138,32.5],[138.5,32.5],[138.5,33],[138,33],[138,32.5]]]}},{"type":"Feature","properties":{"zone_name":"1475"},"geometry":{"type":"Polygon","coordinates":[[[138.5,32.5],[139,32.5],[139,33],[138.5,33],[138.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"1476"},"geometry":{"type":"Polygon","coordinates":[[[139,32.5],[139.5,32.5],[139.5,33],[139,33],[139,32.5]]]}},{"type":"Feature","properties":{"zone_name":"1477"},"geometry":{"type":"Polygon","coordinates":[[[139.5,32.5],[140,32.5],[140,33],[139.5,33],[139.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"1488"},"geometry":{"type":"Polygon","coordinates":[[[138,32],[138.5,32],[138.5,32.5],[138,32.5],[138,32]]]}},{"type":"Feature","properties":{"zone_name":"1489"},"geometry":{"type":"Polygon","coordinates":[[[138.5,32],[139,32],[139,32.5],[138.5,32.5],[138.5,32]]]}},{"type":"Feature","properties":{"zone_name":"1490"},"geometry":{"type":"Polygon","coordinates":[[[139,32],[139.5,32],[139.5,32.5],[139,32.5],[139,32]]]}},{"type":"Feature","properties":{"zone_name":"1491"},"geometry":{"type":"Polygon","coordinates":[[[139.5,32],[140,32],[140,32.5],[139.5,32.5],[139.5,32]]]}},{"type":"Feature","properties":{"zone_name":"1502"},"geometry":{"type":"Polygon","coordinates":[[[138,31.5],[138.5,31.5],[138.5,32],[138,32],[138,31.5]]]}},{"type":"Feature","properties":{"zone_name":"1503"},"geometry":{"type":"Polygon","coordinates":[[[138.5,31.5],[139,31.5],[139,32],[138.5,32],[138.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"1504"},"geometry":{"type":"Polygon","coordinates":[[[139,31.5],[139.5,31.5],[139.5,32],[139,32],[139,31.5]]]}},{"type":"Feature","properties":{"zone_name":"1505"},"geometry":{"type":"Polygon","coordinates":[[[139.5,31.5],[140,31.5],[140,32],[139.5,32],[139.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"1516"},"geometry":{"type":"Polygon","coordinates":[[[138,31],[138.5,31],[138.5,31.5],[138,31.5],[138,31]]]}},{"type":"Feature","properties":{"zone_name":"1517"},"geometry":{"type":"Polygon","coordinates":[[[138.5,31],[139,31],[139,31.5],[138.5,31.5],[138.5,31]]]}},{"type":"Feature","properties":{"zone_name":"1518"},"geometry":{"type":"Polygon","coordinates":[[[139,31],[139.5,31],[139.5,31.5],[139,31.5],[139,31]]]}},{"type":"Feature","properties":{"zone_name":"1519"},"geometry":{"type":"Polygon","coordinates":[[[139.5,31],[140,31],[140,31.5],[139.5,31.5],[139.5,31]]]}},{"type":"Feature","properties":{"zone_name":"1530"},"geometry":{"type":"Polygon","coordinates":[[[138,30.5],[138.5,30.5],[138.5,31],[138,31],[138,30.5]]]}},{"type":"Feature","properties":{"zone_name":"1531"},"geometry":{"type":"Polygon","coordinates":[[[138.5,30.5],[139,30.5],[139,31],[138.5,31],[138.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"1532"},"geometry":{"type":"Polygon","coordinates":[[[139,30.5],[139.5,30.5],[139.5,31],[139,31],[139,30.5]]]}},{"type":"Feature","properties":{"zone_name":"1533"},"geometry":{"type":"Polygon","coordinates":[[[139.5,30.5],[140,30.5],[140,31],[139.5,31],[139.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"1544"},"geometry":{"type":"Polygon","coordinates":[[[138,30],[138.5,30],[138.5,30.5],[138,30.5],[138,30]]]}},{"type":"Feature","properties":{"zone_name":"1545"},"geometry":{"type":"Polygon","coordinates":[[[138.5,30],[139,30],[139,30.5],[138.5,30.5],[138.5,30]]]}},{"type":"Feature","properties":{"zone_name":"1546"},"geometry":{"type":"Polygon","coordinates":[[[139,30],[139.5,30],[139.5,30.5],[139,30.5],[139,30]]]}},{"type":"Feature","properties":{"zone_name":"1547"},"geometry":{"type":"Polygon","coordinates":[[[139.5,30],[140,30],[140,30.5],[139.5,30.5],[139.5,30]]]}},{"type":"Feature","properties":{"zone_name":"1558"},"geometry":{"type":"Polygon","coordinates":[[[138,29.5],[138.5,29.5],[138.5,30],[138,30],[138,29.5]]]}},{"type":"Feature","properties":{"zone_name":"1559"},"geometry":{"type":"Polygon","coordinates":[[[138.5,29.5],[139,29.5],[139,30],[138.5,30],[138.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"1560"},"geometry":{"type":"Polygon","coordinates":[[[139,29.5],[139.5,29.5],[139.5,30],[139,30],[139,29.5]]]}},{"type":"Feature","properties":{"zone_name":"1561"},"geometry":{"type":"Polygon","coordinates":[[[139.5,29.5],[140,29.5],[140,30],[139.5,30],[139.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"1572"},"geometry":{"type":"Polygon","coordinates":[[[138,29],[138.5,29],[138.5,29.5],[138,29.5],[138,29]]]}},{"type":"Feature","properties":{"zone_name":"1573"},"geometry":{"type":"Polygon","coordinates":[[[138.5,29],[139,29],[139,29.5],[138.5,29.5],[138.5,29]]]}},{"type":"Feature","properties":{"zone_name":"1574"},"geometry":{"type":"Polygon","coordinates":[[[139,29],[139.5,29],[139.5,29.5],[139,29.5],[139,29]]]}},{"type":"Feature","properties":{"zone_name":"1575"},"geometry":{"type":"Polygon","coordinates":[[[139.5,29],[140,29],[140,29.5],[139.5,29.5],[139.5,29]]]}},{"type":"Feature","properties":{"zone_name":"1586"},"geometry":{"type":"Polygon","coordinates":[[[138,28.5],[138.5,28.5],[138.5,29],[138,29],[138,28.5]]]}},{"type":"Feature","properties":{"zone_name":"1587"},"geometry":{"type":"Polygon","coordinates":[[[138.5,28.5],[139,28.5],[139,29],[138.5,29],[138.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"1588"},"geometry":{"type":"Polygon","coordinates":[[[139,28.5],[139.5,28.5],[139.5,29],[139,29],[139,28.5]]]}},{"type":"Feature","properties":{"zone_name":"1589"},"geometry":{"type":"Polygon","coordinates":[[[139.5,28.5],[140,28.5],[140,29],[139.5,29],[139.5,28.5]]]}},{"type":"Feature","properties":{"zone_name":"1600"},"geometry":{"type":"Polygon","coordinates":[[[138,28],[138.5,28],[138.5,28.5],[138,28.5],[138,28]]]}},{"type":"Feature","properties":{"zone_name":"1601"},"geometry":{"type":"Polygon","coordinates":[[[138.5,28],[139,28],[139,28.5],[138.5,28.5],[138.5,28]]]}},{"type":"Feature","properties":{"zone_name":"1602"},"geometry":{"type":"Polygon","coordinates":[[[139,28],[139.5,28],[139.5,28.5],[139,28.5],[139,28]]]}},{"type":"Feature","properties":{"zone_name":"1603"},"geometry":{"type":"Polygon","coordinates":[[[139.5,28],[140,28],[140,28.5],[139.5,28.5],[139.5,28]]]}},{"type":"Feature","properties":{"zone_name":"1614"},"geometry":{"type":"Polygon","coordinates":[[[138,27.5],[138.5,27.5],[138.5,28],[138,28],[138,27.5]]]}},{"type":"Feature","properties":{"zone_name":"1615"},"geometry":{"type":"Polygon","coordinates":[[[138.5,27.5],[139,27.5],[139,28],[138.5,28],[138.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"1616"},"geometry":{"type":"Polygon","coordinates":[[[139,27.5],[139.5,27.5],[139.5,28],[139,28],[139,27.5]]]}},{"type":"Feature","properties":{"zone_name":"1617"},"geometry":{"type":"Polygon","coordinates":[[[139.5,27.5],[140,27.5],[140,28],[139.5,28],[139.5,27.5]]]}},{"type":"Feature","properties":{"zone_name":"1628"},"geometry":{"type":"Polygon","coordinates":[[[138,27],[138.5,27],[138.5,27.5],[138,27.5],[138,27]]]}},{"type":"Feature","properties":{"zone_name":"1629"},"geometry":{"type":"Polygon","coordinates":[[[138.5,27],[139,27],[139,27.5],[138.5,27.5],[138.5,27]]]}},{"type":"Feature","properties":{"zone_name":"1630"},"geometry":{"type":"Polygon","coordinates":[[[139,27],[139.5,27],[139.5,27.5],[139,27.5],[139,27]]]}},{"type":"Feature","properties":{"zone_name":"1631"},"geometry":{"type":"Polygon","coordinates":[[[139.5,27],[140,27],[140,27.5],[139.5,27.5],[139.5,27]]]}},{"type":"Feature","properties":{"zone_name":"1642"},"geometry":{"type":"Polygon","coordinates":[[[138,26.5],[138.5,26.5],[138.5,27],[138,27],[138,26.5]]]}},{"type":"Feature","properties":{"zone_name":"1643"},"geometry":{"type":"Polygon","coordinates":[[[138.5,26.5],[139,26.5],[139,27],[138.5,27],[138.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"1644"},"geometry":{"type":"Polygon","coordinates":[[[139,26.5],[139.5,26.5],[139.5,27],[139,27],[139,26.5]]]}},{"type":"Feature","properties":{"zone_name":"1645"},"geometry":{"type":"Polygon","coordinates":[[[139.5,26.5],[140,26.5],[140,27],[139.5,27],[139.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"1656"},"geometry":{"type":"Polygon","coordinates":[[[138,26],[138.5,26],[138.5,26.5],[138,26.5],[138,26]]]}},{"type":"Feature","properties":{"zone_name":"1657"},"geometry":{"type":"Polygon","coordinates":[[[138.5,26],[139,26],[139,26.5],[138.5,26.5],[138.5,26]]]}},{"type":"Feature","properties":{"zone_name":"1658"},"geometry":{"type":"Polygon","coordinates":[[[139,26],[139.5,26],[139.5,26.5],[139,26.5],[139,26]]]}},{"type":"Feature","properties":{"zone_name":"1659"},"geometry":{"type":"Polygon","coordinates":[[[139.5,26],[140,26],[140,26.5],[139.5,26.5],[139.5,26]]]}},{"type":"Feature","properties":{"zone_name":"1670"},"geometry":{"type":"Polygon","coordinates":[[[138,25.5],[138.5,25.5],[138.5,26],[138,26],[138,25.5]]]}},{"type":"Feature","properties":{"zone_name":"1671"},"geometry":{"type":"Polygon","coordinates":[[[138.5,25.5],[139,25.5],[139,26],[138.5,26],[138.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"1672"},"geometry":{"type":"Polygon","coordinates":[[[139,25.5],[139.5,25.5],[139.5,26],[139,26],[139,25.5]]]}},{"type":"Feature","properties":{"zone_name":"1673"},"geometry":{"type":"Polygon","coordinates":[[[139.5,25.5],[140,25.5],[140,26],[139.5,26],[139.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"1684"},"geometry":{"type":"Polygon","coordinates":[[[138,25],[138.5,25],[138.5,25.5],[138,25.5],[138,25]]]}},{"type":"Feature","properties":{"zone_name":"1685"},"geometry":{"type":"Polygon","coordinates":[[[138.5,25],[139,25],[139,25.5],[138.5,25.5],[138.5,25]]]}},{"type":"Feature","properties":{"zone_name":"1686"},"geometry":{"type":"Polygon","coordinates":[[[139,25],[139.5,25],[139.5,25.5],[139,25.5],[139,25]]]}},{"type":"Feature","properties":{"zone_name":"1687"},"geometry":{"type":"Polygon","coordinates":[[[139.5,25],[140,25],[140,25.5],[139.5,25.5],[139.5,25]]]}},{"type":"Feature","properties":{"zone_name":"5021"},"geometry":{"type":"Polygon","coordinates":[[[127.5,40],[128,40],[128,40.5],[127.5,40.5],[127.5,40]]]}},{"type":"Feature","properties":{"zone_name":"5022"},"geometry":{"type":"Polygon","coordinates":[[[128,40],[128.5,40],[128.5,40.5],[128,40.5],[128,40]]]}},{"type":"Feature","properties":{"zone_name":"5029"},"geometry":{"type":"Polygon","coordinates":[[[127,39.5],[127.5,39.5],[127.5,40],[127,40],[127,39.5]]]}},{"type":"Feature","properties":{"zone_name":"5038"},"geometry":{"type":"Polygon","coordinates":[[[127,39],[127.5,39],[127.5,39.5],[127,39.5],[127,39]]]}},{"type":"Feature","properties":{"zone_name":"5047"},"geometry":{"type":"Polygon","coordinates":[[[127.5,38.5],[128,38.5],[128,39],[127.5,39],[127.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"5055"},"geometry":{"type":"Polygon","coordinates":[[[128,38],[128.5,38],[128.5,38.5],[128,38.5],[128,38]]]}},{"type":"Feature","properties":{"zone_name":"5087"},"geometry":{"type":"Polygon","coordinates":[[[129,35.5],[129.5,35.5],[129.5,36],[129,36],[129,35.5]]]}},{"type":"Feature","properties":{"zone_name":"5097"},"geometry":{"type":"Polygon","coordinates":[[[127.5,35],[128,35],[128,35.5],[127.5,35.5],[127.5,35]]]}},{"type":"Feature","properties":{"zone_name":"5098"},"geometry":{"type":"Polygon","coordinates":[[[128,35],[128.5,35],[128.5,35.5],[128,35.5],[128,35]]]}},{"type":"Feature","properties":{"zone_name":"5099"},"geometry":{"type":"Polygon","coordinates":[[[128.5,35],[129,35],[129,35.5],[128.5,35.5],[128.5,35]]]}},{"type":"Feature","properties":{"zone_name":"5118"},"geometry":{"type":"Polygon","coordinates":[[[124.5,39.5],[125,39.5],[125,40],[124.5,40],[124.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"5124"},"geometry":{"type":"Polygon","coordinates":[[[125,39.5],[125.5,39.5],[125.5,40],[125,40],[125,39.5]]]}},{"type":"Feature","properties":{"zone_name":"5130"},"geometry":{"type":"Polygon","coordinates":[[[125,38.5],[125.5,38.5],[125.5,39],[125,39],[125,38.5]]]}},{"type":"Feature","properties":{"zone_name":"5131"},"geometry":{"type":"Polygon","coordinates":[[[125.5,38.5],[126,38.5],[126,39],[125.5,39],[125.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"5137"},"geometry":{"type":"Polygon","coordinates":[[[125,38],[125.5,38],[125.5,38.5],[125,38.5],[125,38]]]}},{"type":"Feature","properties":{"zone_name":"5138"},"geometry":{"type":"Polygon","coordinates":[[[125.5,38],[126,38],[126,38.5],[125.5,38.5],[125.5,38]]]}},{"type":"Feature","properties":{"zone_name":"5146"},"geometry":{"type":"Polygon","coordinates":[[[126,37.5],[126.5,37.5],[126.5,38],[126,38],[126,37.5]]]}},{"type":"Feature","properties":{"zone_name":"5147"},"geometry":{"type":"Polygon","coordinates":[[[126.5,37.5],[127,37.5],[127,38],[126.5,38],[126.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"5154"},"geometry":{"type":"Polygon","coordinates":[[[126.5,37],[127,37],[127,37.5],[126.5,37.5],[126.5,37]]]}},{"type":"Feature","properties":{"zone_name":"5164"},"geometry":{"type":"Polygon","coordinates":[[[126.5,36.5],[127,36.5],[127,37],[126.5,37],[126.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"5174"},"geometry":{"type":"Polygon","coordinates":[[[126.5,36],[127,36],[127,36.5],[126.5,36.5],[126.5,36]]]}},{"type":"Feature","properties":{"zone_name":"5184"},"geometry":{"type":"Polygon","coordinates":[[[126.5,35.5],[127,35.5],[127,36],[126.5,36],[126.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"5213"},"geometry":{"type":"Polygon","coordinates":[[[126.5,34.5],[127,34.5],[127,35],[126.5,35],[126.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"5214"},"geometry":{"type":"Polygon","coordinates":[[[127,34.5],[127.5,34.5],[127.5,35],[127,35],[127,34.5]]]}},{"type":"Feature","properties":{"zone_name":"6003"},"geometry":{"type":"Polygon","coordinates":[[[121.5,41],[122,41],[122,41.5],[121.5,41.5],[121.5,41]]]}},{"type":"Feature","properties":{"zone_name":"6005"},"geometry":{"type":"Polygon","coordinates":[[[119.5,40],[120,40],[120,40.5],[119.5,40.5],[119.5,40]]]}},{"type":"Feature","properties":{"zone_name":"6058"},"geometry":{"type":"Polygon","coordinates":[[[119,35],[119.5,35],[119.5,35.5],[119,35.5],[119,35]]]}},{"type":"Feature","properties":{"zone_name":"6062"},"geometry":{"type":"Polygon","coordinates":[[[119,34.5],[119.5,34.5],[119.5,35],[119,35],[119,34.5]]]}},{"type":"Feature","properties":{"zone_name":"6066"},"geometry":{"type":"Polygon","coordinates":[[[119.5,34],[120,34],[120,34.5],[119.5,34.5],[119.5,34]]]}},{"type":"Feature","properties":{"zone_name":"6076"},"geometry":{"type":"Polygon","coordinates":[[[120,32],[120.5,32],[120.5,32.5],[120,32.5],[120,32]]]}},{"type":"Feature","properties":{"zone_name":"6077"},"geometry":{"type":"Polygon","coordinates":[[[120.5,32],[121,32],[121,32.5],[120.5,32.5],[120.5,32]]]}},{"type":"Feature","properties":{"zone_name":"6116"},"geometry":{"type":"Polygon","coordinates":[[[122.5,39.5],[123,39.5],[123,40],[122.5,40],[122.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6119"},"geometry":{"type":"Polygon","coordinates":[[[122,39],[122.5,39],[122.5,39.5],[122,39.5],[122,39]]]}},{"type":"Feature","properties":{"zone_name":"6137"},"geometry":{"type":"Polygon","coordinates":[[[121,37],[121.5,37],[121.5,37.5],[121,37.5],[121,37]]]}},{"type":"Feature","properties":{"zone_name":"6138"},"geometry":{"type":"Polygon","coordinates":[[[121.5,37],[122,37],[122,37.5],[121.5,37.5],[121.5,37]]]}},{"type":"Feature","properties":{"zone_name":"6139"},"geometry":{"type":"Polygon","coordinates":[[[122,37],[122.5,37],[122.5,37.5],[122,37.5],[122,37]]]}},{"type":"Feature","properties":{"zone_name":"6154"},"geometry":{"type":"Polygon","coordinates":[[[120.5,36.5],[121,36.5],[121,37],[120.5,37],[120.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"6155"},"geometry":{"type":"Polygon","coordinates":[[[121,36.5],[121.5,36.5],[121.5,37],[121,37],[121,36.5]]]}},{"type":"Feature","properties":{"zone_name":"6454"},"geometry":{"type":"Polygon","coordinates":[[[120,31.5],[120.5,31.5],[120.5,32],[120,32],[120,31.5]]]}},{"type":"Feature","properties":{"zone_name":"6455"},"geometry":{"type":"Polygon","coordinates":[[[120.5,31.5],[121,31.5],[121,32],[120.5,32],[120.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"6456"},"geometry":{"type":"Polygon","coordinates":[[[121,31.5],[121.5,31.5],[121.5,32],[121,32],[121,31.5]]]}},{"type":"Feature","properties":{"zone_name":"6457"},"geometry":{"type":"Polygon","coordinates":[[[121.5,31.5],[122,31.5],[122,32],[121.5,32],[121.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"6486"},"geometry":{"type":"Polygon","coordinates":[[[121,31],[121.5,31],[121.5,31.5],[121,31.5],[121,31]]]}},{"type":"Feature","properties":{"zone_name":"6487"},"geometry":{"type":"Polygon","coordinates":[[[121.5,31],[122,31],[122,31.5],[121.5,31.5],[121.5,31]]]}},{"type":"Feature","properties":{"zone_name":"6601"},"geometry":{"type":"Polygon","coordinates":[[[120.5,30.5],[121,30.5],[121,31],[120.5,31],[120.5,30.5]]]}},{"type":"Feature","properties":{"zone_name":"6602"},"geometry":{"type":"Polygon","coordinates":[[[120,30],[120.5,30],[120.5,30.5],[120,30.5],[120,30]]]}},{"type":"Feature","properties":{"zone_name":"6603"},"geometry":{"type":"Polygon","coordinates":[[[120.5,30],[121,30],[121,30.5],[120.5,30.5],[120.5,30]]]}},{"type":"Feature","properties":{"zone_name":"6604"},"geometry":{"type":"Polygon","coordinates":[[[121,29.5],[121.5,29.5],[121.5,30],[121,30],[121,29.5]]]}},{"type":"Feature","properties":{"zone_name":"6605"},"geometry":{"type":"Polygon","coordinates":[[[121.5,29.5],[122,29.5],[122,30],[121.5,30],[121.5,29.5]]]}},{"type":"Feature","properties":{"zone_name":"6614"},"geometry":{"type":"Polygon","coordinates":[[[121,29],[121.5,29],[121.5,29.5],[121,29.5],[121,29]]]}},{"type":"Feature","properties":{"zone_name":"6615"},"geometry":{"type":"Polygon","coordinates":[[[121.5,29],[122,29],[122,29.5],[121.5,29.5],[121.5,29]]]}},{"type":"Feature","properties":{"zone_name":"6624"},"geometry":{"type":"Polygon","coordinates":[[[121,28.5],[121.5,28.5],[121.5,29],[121,29],[121,28.5]]]}},{"type":"Feature","properties":{"zone_name":"6642"},"geometry":{"type":"Polygon","coordinates":[[[120.5,28],[121,28],[121,28.5],[120.5,28.5],[120.5,28]]]}},{"type":"Feature","properties":{"zone_name":"6660"},"geometry":{"type":"Polygon","coordinates":[[[119.5,26.5],[120,26.5],[120,27],[119.5,27],[119.5,26.5]]]}},{"type":"Feature","properties":{"zone_name":"6670"},"geometry":{"type":"Polygon","coordinates":[[[119.5,26],[120,26],[120,26.5],[119.5,26.5],[119.5,26]]]}},{"type":"Feature","properties":{"zone_name":"6680"},"geometry":{"type":"Polygon","coordinates":[[[119,25.5],[119.5,25.5],[119.5,26],[119,26],[119,25.5]]]}},{"type":"Feature","properties":{"zone_name":"6681"},"geometry":{"type":"Polygon","coordinates":[[[119.5,25.5],[120,25.5],[120,26],[119.5,26],[119.5,25.5]]]}},{"type":"Feature","properties":{"zone_name":"6690"},"geometry":{"type":"Polygon","coordinates":[[[119,25],[119.5,25],[119.5,25.5],[119,25.5],[119,25]]]}},{"type":"Feature","properties":{"zone_name":"6691"},"geometry":{"type":"Polygon","coordinates":[[[119.5,25],[120,25],[120,25.5],[119.5,25.5],[119.5,25]]]}},{"type":"Feature","properties":{"zone_name":"6901"},"geometry":{"type":"Polygon","coordinates":[[[120.5,40.5],[121,40.5],[121,41],[120.5,41],[120.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"6902"},"geometry":{"type":"Polygon","coordinates":[[[121,40.5],[121.5,40.5],[121.5,41],[121,41],[121,40.5]]]}},{"type":"Feature","properties":{"zone_name":"6903"},"geometry":{"type":"Polygon","coordinates":[[[121.5,40.5],[122,40.5],[122,41],[121.5,41],[121.5,40.5]]]}},{"type":"Feature","properties":{"zone_name":"6904"},"geometry":{"type":"Polygon","coordinates":[[[122,40.5],[122.5,40.5],[122.5,41],[122,41],[122,40.5]]]}},{"type":"Feature","properties":{"zone_name":"6905"},"geometry":{"type":"Polygon","coordinates":[[[120,40],[120.5,40],[120.5,40.5],[120,40.5],[120,40]]]}},{"type":"Feature","properties":{"zone_name":"6906"},"geometry":{"type":"Polygon","coordinates":[[[120.5,40],[121,40],[121,40.5],[120.5,40.5],[120.5,40]]]}},{"type":"Feature","properties":{"zone_name":"6907"},"geometry":{"type":"Polygon","coordinates":[[[121,40],[121.5,40],[121.5,40.5],[121,40.5],[121,40]]]}},{"type":"Feature","properties":{"zone_name":"6908"},"geometry":{"type":"Polygon","coordinates":[[[121.5,40],[122,40],[122,40.5],[121.5,40.5],[121.5,40]]]}},{"type":"Feature","properties":{"zone_name":"6909"},"geometry":{"type":"Polygon","coordinates":[[[122,40],[122.5,40],[122.5,40.5],[122,40.5],[122,40]]]}},{"type":"Feature","properties":{"zone_name":"6910"},"geometry":{"type":"Polygon","coordinates":[[[119,39.5],[119.5,39.5],[119.5,40],[119,40],[119,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6911"},"geometry":{"type":"Polygon","coordinates":[[[119.5,39.5],[120,39.5],[120,40],[119.5,40],[119.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6912"},"geometry":{"type":"Polygon","coordinates":[[[120,39.5],[120.5,39.5],[120.5,40],[120,40],[120,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6913"},"geometry":{"type":"Polygon","coordinates":[[[120.5,39.5],[121,39.5],[121,40],[120.5,40],[120.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6914"},"geometry":{"type":"Polygon","coordinates":[[[121,39.5],[121.5,39.5],[121.5,40],[121,40],[121,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6915"},"geometry":{"type":"Polygon","coordinates":[[[121.5,39.5],[122,39.5],[122,40],[121.5,40],[121.5,39.5]]]}},{"type":"Feature","properties":{"zone_name":"6918"},"geometry":{"type":"Polygon","coordinates":[[[119,39],[119.5,39],[119.5,39.5],[119,39.5],[119,39]]]}},{"type":"Feature","properties":{"zone_name":"6919"},"geometry":{"type":"Polygon","coordinates":[[[119.5,39],[120,39],[120,39.5],[119.5,39.5],[119.5,39]]]}},{"type":"Feature","properties":{"zone_name":"6920"},"geometry":{"type":"Polygon","coordinates":[[[120,39],[120.5,39],[120.5,39.5],[120,39.5],[120,39]]]}},{"type":"Feature","properties":{"zone_name":"6921"},"geometry":{"type":"Polygon","coordinates":[[[120.5,39],[121,39],[121,39.5],[120.5,39.5],[120.5,39]]]}},{"type":"Feature","properties":{"zone_name":"6922"},"geometry":{"type":"Polygon","coordinates":[[[121,39],[121.5,39],[121.5,39.5],[121,39.5],[121,39]]]}},{"type":"Feature","properties":{"zone_name":"6923"},"geometry":{"type":"Polygon","coordinates":[[[121.5,39],[122,39],[122,39.5],[121.5,39.5],[121.5,39]]]}},{"type":"Feature","properties":{"zone_name":"6928"},"geometry":{"type":"Polygon","coordinates":[[[119,38.5],[119.5,38.5],[119.5,39],[119,39],[119,38.5]]]}},{"type":"Feature","properties":{"zone_name":"6929"},"geometry":{"type":"Polygon","coordinates":[[[119.5,38.5],[120,38.5],[120,39],[119.5,39],[119.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"6930"},"geometry":{"type":"Polygon","coordinates":[[[120,38.5],[120.5,38.5],[120.5,39],[120,39],[120,38.5]]]}},{"type":"Feature","properties":{"zone_name":"6931"},"geometry":{"type":"Polygon","coordinates":[[[120.5,38.5],[121,38.5],[121,39],[120.5,39],[120.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"6932"},"geometry":{"type":"Polygon","coordinates":[[[121,38.5],[121.5,38.5],[121.5,39],[121,39],[121,38.5]]]}},{"type":"Feature","properties":{"zone_name":"6933"},"geometry":{"type":"Polygon","coordinates":[[[121.5,38.5],[122,38.5],[122,39],[121.5,39],[121.5,38.5]]]}},{"type":"Feature","properties":{"zone_name":"6937"},"geometry":{"type":"Polygon","coordinates":[[[119,38],[119.5,38],[119.5,38.5],[119,38.5],[119,38]]]}},{"type":"Feature","properties":{"zone_name":"6938"},"geometry":{"type":"Polygon","coordinates":[[[119.5,38],[120,38],[120,38.5],[119.5,38.5],[119.5,38]]]}},{"type":"Feature","properties":{"zone_name":"6939"},"geometry":{"type":"Polygon","coordinates":[[[120,38],[120.5,38],[120.5,38.5],[120,38.5],[120,38]]]}},{"type":"Feature","properties":{"zone_name":"6940"},"geometry":{"type":"Polygon","coordinates":[[[120.5,38],[121,38],[121,38.5],[120.5,38.5],[120.5,38]]]}},{"type":"Feature","properties":{"zone_name":"6941"},"geometry":{"type":"Polygon","coordinates":[[[121,38],[121.5,38],[121.5,38.5],[121,38.5],[121,38]]]}},{"type":"Feature","properties":{"zone_name":"6943"},"geometry":{"type":"Polygon","coordinates":[[[119,37.5],[119.5,37.5],[119.5,38],[119,38],[119,37.5]]]}},{"type":"Feature","properties":{"zone_name":"6944"},"geometry":{"type":"Polygon","coordinates":[[[119.5,37.5],[120,37.5],[120,38],[119.5,38],[119.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"6945"},"geometry":{"type":"Polygon","coordinates":[[[120,37.5],[120.5,37.5],[120.5,38],[120,38],[120,37.5]]]}},{"type":"Feature","properties":{"zone_name":"6946"},"geometry":{"type":"Polygon","coordinates":[[[120.5,37.5],[121,37.5],[121,38],[120.5,38],[120.5,37.5]]]}},{"type":"Feature","properties":{"zone_name":"6947"},"geometry":{"type":"Polygon","coordinates":[[[121,37.5],[121.5,37.5],[121.5,38],[121,38],[121,37.5]]]}},{"type":"Feature","properties":{"zone_name":"6948"},"geometry":{"type":"Polygon","coordinates":[[[119,37],[119.5,37],[119.5,37.5],[119,37.5],[119,37]]]}},{"type":"Feature","properties":{"zone_name":"6949"},"geometry":{"type":"Polygon","coordinates":[[[119.5,37],[120,37],[120,37.5],[119.5,37.5],[119.5,37]]]}},{"type":"Feature","properties":{"zone_name":"6950"},"geometry":{"type":"Polygon","coordinates":[[[120,37],[120.5,37],[120.5,37.5],[120,37.5],[120,37]]]}},{"type":"Feature","properties":{"zone_name":"6951"},"geometry":{"type":"Polygon","coordinates":[[[120,36],[120.5,36],[120.5,36.5],[120,36.5],[120,36]]]}},{"type":"Feature","properties":{"zone_name":"6952"},"geometry":{"type":"Polygon","coordinates":[[[120.5,36],[121,36],[121,36.5],[120.5,36.5],[120.5,36]]]}},{"type":"Feature","properties":{"zone_name":"6953"},"geometry":{"type":"Polygon","coordinates":[[[121,36],[121.5,36],[121.5,36.5],[121,36.5],[121,36]]]}},{"type":"Feature","properties":{"zone_name":"6954"},"geometry":{"type":"Polygon","coordinates":[[[119.5,35.5],[120,35.5],[120,36],[119.5,36],[119.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"6955"},"geometry":{"type":"Polygon","coordinates":[[[120,35.5],[120.5,35.5],[120.5,36],[120,36],[120,35.5]]]}},{"type":"Feature","properties":{"zone_name":"6956"},"geometry":{"type":"Polygon","coordinates":[[[120.5,35.5],[121,35.5],[121,36],[120.5,36],[120.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"6957"},"geometry":{"type":"Polygon","coordinates":[[[121,35.5],[121.5,35.5],[121.5,36],[121,36],[121,35.5]]]}},{"type":"Feature","properties":{"zone_name":"6958"},"geometry":{"type":"Polygon","coordinates":[[[119.5,35],[120,35],[120,35.5],[119.5,35.5],[119.5,35]]]}},{"type":"Feature","properties":{"zone_name":"6959"},"geometry":{"type":"Polygon","coordinates":[[[120,35],[120.5,35],[120.5,35.5],[120,35.5],[120,35]]]}},{"type":"Feature","properties":{"zone_name":"6960"},"geometry":{"type":"Polygon","coordinates":[[[120.5,35],[121,35],[121,35.5],[120.5,35.5],[120.5,35]]]}},{"type":"Feature","properties":{"zone_name":"6961"},"geometry":{"type":"Polygon","coordinates":[[[121,35],[121.5,35],[121.5,35.5],[121,35.5],[121,35]]]}},{"type":"Feature","properties":{"zone_name":"6962"},"geometry":{"type":"Polygon","coordinates":[[[119.5,34.5],[120,34.5],[120,35],[119.5,35],[119.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"6963"},"geometry":{"type":"Polygon","coordinates":[[[120,34.5],[120.5,34.5],[120.5,35],[120,35],[120,34.5]]]}},{"type":"Feature","properties":{"zone_name":"6964"},"geometry":{"type":"Polygon","coordinates":[[[120.5,34.5],[121,34.5],[121,35],[120.5,35],[120.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"6965"},"geometry":{"type":"Polygon","coordinates":[[[121,34.5],[121.5,34.5],[121.5,35],[121,35],[121,34.5]]]}},{"type":"Feature","properties":{"zone_name":"6966"},"geometry":{"type":"Polygon","coordinates":[[[120,34],[120.5,34],[120.5,34.5],[120,34.5],[120,34]]]}},{"type":"Feature","properties":{"zone_name":"6967"},"geometry":{"type":"Polygon","coordinates":[[[120.5,34],[121,34],[121,34.5],[120.5,34.5],[120.5,34]]]}},{"type":"Feature","properties":{"zone_name":"6968"},"geometry":{"type":"Polygon","coordinates":[[[121,34],[121.5,34],[121.5,34.5],[121,34.5],[121,34]]]}},{"type":"Feature","properties":{"zone_name":"6969"},"geometry":{"type":"Polygon","coordinates":[[[120,33.5],[120.5,33.5],[120.5,34],[120,34],[120,33.5]]]}},{"type":"Feature","properties":{"zone_name":"6970"},"geometry":{"type":"Polygon","coordinates":[[[120.5,33.5],[121,33.5],[121,34],[120.5,34],[120.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"6971"},"geometry":{"type":"Polygon","coordinates":[[[121,33.5],[121.5,33.5],[121.5,34],[121,34],[121,33.5]]]}},{"type":"Feature","properties":{"zone_name":"6972"},"geometry":{"type":"Polygon","coordinates":[[[120.5,33],[121,33],[121,33.5],[120.5,33.5],[120.5,33]]]}},{"type":"Feature","properties":{"zone_name":"6973"},"geometry":{"type":"Polygon","coordinates":[[[121,33],[121.5,33],[121.5,33.5],[121,33.5],[121,33]]]}},{"type":"Feature","properties":{"zone_name":"6974"},"geometry":{"type":"Polygon","coordinates":[[[120.5,32.5],[121,32.5],[121,33],[120.5,33],[120.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"6975"},"geometry":{"type":"Polygon","coordinates":[[[121,32.5],[121.5,32.5],[121.5,33],[121,33],[121,32.5]]]}},{"type":"Feature","properties":{"zone_name":"6976"},"geometry":{"type":"Polygon","coordinates":[[[121.5,32.5],[122,32.5],[122,33],[121.5,33],[121.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"6977"},"geometry":{"type":"Polygon","coordinates":[[[121,32],[121.5,32],[121.5,32.5],[121,32.5],[121,32]]]}},{"type":"Feature","properties":{"zone_name":"6978"},"geometry":{"type":"Polygon","coordinates":[[[121.5,32],[122,32],[122,32.5],[121.5,32.5],[121.5,32]]]}},{"type":"Feature","properties":{"zone_name":"7349"},"geometry":{"type":"Polygon","coordinates":[[[139.5,38],[140,38],[140,38.5],[139.5,38.5],[139.5,38]]]}},{"type":"Feature","properties":{"zone_name":"7359"},"geometry":{"type":"Polygon","coordinates":[[[139,37.5],[139.5,37.5],[139.5,38],[139,38],[139,37.5]]]}},{"type":"Feature","properties":{"zone_name":"7360"},"geometry":{"type":"Polygon","coordinates":[[[133,35],[133.5,35],[133.5,35.5],[133,35.5],[133,35]]]}},{"type":"Feature","properties":{"zone_name":"7361"},"geometry":{"type":"Polygon","coordinates":[[[133.5,35],[134,35],[134,35.5],[133.5,35.5],[133.5,35]]]}},{"type":"Feature","properties":{"zone_name":"7363"},"geometry":{"type":"Polygon","coordinates":[[[132.5,35],[133,35],[133,35.5],[132.5,35.5],[132.5,35]]]}},{"type":"Feature","properties":{"zone_name":"7366"},"geometry":{"type":"Polygon","coordinates":[[[132,34.5],[132.5,34.5],[132.5,35],[132,35],[132,34.5]]]}},{"type":"Feature","properties":{"zone_name":"7367"},"geometry":{"type":"Polygon","coordinates":[[[131,34],[131.5,34],[131.5,34.5],[131,34.5],[131,34]]]}},{"type":"Feature","properties":{"zone_name":"7368"},"geometry":{"type":"Polygon","coordinates":[[[138.5,37],[139,37],[139,37.5],[138.5,37.5],[138.5,37]]]}},{"type":"Feature","properties":{"zone_name":"7370"},"geometry":{"type":"Polygon","coordinates":[[[130.5,33.5],[131,33.5],[131,34],[130.5,34],[130.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"7372"},"geometry":{"type":"Polygon","coordinates":[[[129.5,33],[130,33],[130,33.5],[129.5,33.5],[129.5,33]]]}},{"type":"Feature","properties":{"zone_name":"7373"},"geometry":{"type":"Polygon","coordinates":[[[130,33],[130.5,33],[130.5,33.5],[130,33.5],[130,33]]]}},{"type":"Feature","properties":{"zone_name":"7377"},"geometry":{"type":"Polygon","coordinates":[[[130,32.5],[130.5,32.5],[130.5,33],[130,33],[130,32.5]]]}},{"type":"Feature","properties":{"zone_name":"7378"},"geometry":{"type":"Polygon","coordinates":[[[130.5,32.5],[131,32.5],[131,33],[130.5,33],[130.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"7382"},"geometry":{"type":"Polygon","coordinates":[[[130,32],[130.5,32],[130.5,32.5],[130,32.5],[130,32]]]}},{"type":"Feature","properties":{"zone_name":"7383"},"geometry":{"type":"Polygon","coordinates":[[[130.5,32],[131,32],[131,32.5],[130.5,32.5],[130.5,32]]]}},{"type":"Feature","properties":{"zone_name":"7388"},"geometry":{"type":"Polygon","coordinates":[[[136.5,35],[137,35],[137,35.5],[136.5,35.5],[136.5,35]]]}},{"type":"Feature","properties":{"zone_name":"7413"},"geometry":{"type":"Polygon","coordinates":[[[133,33.5],[133.5,33.5],[133.5,34],[133,34],[133,33.5]]]}},{"type":"Feature","properties":{"zone_name":"7414"},"geometry":{"type":"Polygon","coordinates":[[[133.5,33.5],[134,33.5],[134,34],[133.5,34],[133.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"7415"},"geometry":{"type":"Polygon","coordinates":[[[134,33.5],[134.5,33.5],[134.5,34],[134,34],[134,33.5]]]}},{"type":"Feature","properties":{"zone_name":"7420"},"geometry":{"type":"Polygon","coordinates":[[[131.5,33],[132,33],[132,33.5],[131.5,33.5],[131.5,33]]]}},{"type":"Feature","properties":{"zone_name":"7421"},"geometry":{"type":"Polygon","coordinates":[[[132.5,33],[133,33],[133,33.5],[132.5,33.5],[132.5,33]]]}},{"type":"Feature","properties":{"zone_name":"7422"},"geometry":{"type":"Polygon","coordinates":[[[133,33],[133.5,33],[133.5,33.5],[133,33.5],[133,33]]]}},{"type":"Feature","properties":{"zone_name":"7426"},"geometry":{"type":"Polygon","coordinates":[[[135.5,33.5],[136,33.5],[136,34],[135.5,34],[135.5,33.5]]]}},{"type":"Feature","properties":{"zone_name":"7432"},"geometry":{"type":"Polygon","coordinates":[[[131.5,32.5],[132,32.5],[132,33],[131.5,33],[131.5,32.5]]]}},{"type":"Feature","properties":{"zone_name":"7432"},"geometry":{"type":"Polygon","coordinates":[[[138,34.5],[138.5,34.5],[138.5,35],[138,35],[138,34.5]]]}},{"type":"Feature","properties":{"zone_name":"7433"},"geometry":{"type":"Polygon","coordinates":[[[138.5,34.5],[139,34.5],[139,35],[138.5,35],[138.5,34.5]]]}},{"type":"Feature","properties":{"zone_name":"7434"},"geometry":{"type":"Polygon","coordinates":[[[138.5,35],[139,35],[139,35.5],[138.5,35.5],[138.5,35]]]}},{"type":"Feature","properties":{"zone_name":"7435"},"geometry":{"type":"Polygon","coordinates":[[[139,35],[139.5,35],[139.5,35.5],[139,35.5],[139,35]]]}},{"type":"Feature","properties":{"zone_name":"7436"},"geometry":{"type":"Polygon","coordinates":[[[139.5,35],[140,35],[140,35.5],[139.5,35.5],[139.5,35]]]}},{"type":"Feature","properties":{"zone_name":"7437"},"geometry":{"type":"Polygon","coordinates":[[[139.5,35.5],[140,35.5],[140,36],[139.5,36],[139.5,35.5]]]}},{"type":"Feature","properties":{"zone_name":"7472"},"geometry":{"type":"Polygon","coordinates":[[[130,31.5],[130.5,31.5],[130.5,32],[130,32],[130,31.5]]]}},{"type":"Feature","properties":{"zone_name":"7473"},"geometry":{"type":"Polygon","coordinates":[[[130.5,31.5],[131,31.5],[131,32],[130.5,32],[130.5,31.5]]]}},{"type":"Feature","properties":{"zone_name":"7474"},"geometry":{"type":"Polygon","coordinates":[[[131,31.5],[131.5,31.5],[131.5,32],[131,32],[131,31.5]]]}},{"type":"Feature","properties":{"zone_name":"7904"},"geometry":{"type":"Polygon","coordinates":[[[136,35.5],[136.5,35.5],[136.5,36],[136,36],[136,35.5]]]}},{"type":"Feature","properties":{"zone_name":"7922"},"geometry":{"type":"Polygon","coordinates":[[[137,36.5],[137.5,36.5],[137.5,37],[137,37],[137,36.5]]]}},{"type":"Feature","properties":{"zone_name":"7923"},"geometry":{"type":"Polygon","coordinates":[[[137.5,36.5],[138,36.5],[138,37],[137.5,37],[137.5,36.5]]]}},{"type":"Feature","properties":{"zone_name":"8101"},"geometry":{"type":"Polygon","coordinates":[[[134,43],[134.5,43],[134.5,43.5],[134,43.5],[134,43]]]}},{"type":"Feature","properties":{"zone_name":"8102"},"geometry":{"type":"Polygon","coordinates":[[[134.5,43],[135,43],[135,43.5],[134.5,43.5],[134.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8103"},"geometry":{"type":"Polygon","coordinates":[[[135,43],[135.5,43],[135.5,43.5],[135,43.5],[135,43]]]}},{"type":"Feature","properties":{"zone_name":"8104"},"geometry":{"type":"Polygon","coordinates":[[[135.5,43],[136,43],[136,43.5],[135.5,43.5],[135.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8105"},"geometry":{"type":"Polygon","coordinates":[[[136,43],[136.5,43],[136.5,43.5],[136,43.5],[136,43]]]}},{"type":"Feature","properties":{"zone_name":"8106"},"geometry":{"type":"Polygon","coordinates":[[[136.5,43],[137,43],[137,43.5],[136.5,43.5],[136.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8107"},"geometry":{"type":"Polygon","coordinates":[[[137,43],[137.5,43],[137.5,43.5],[137,43.5],[137,43]]]}},{"type":"Feature","properties":{"zone_name":"8108"},"geometry":{"type":"Polygon","coordinates":[[[137.5,43],[138,43],[138,43.5],[137.5,43.5],[137.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8109"},"geometry":{"type":"Polygon","coordinates":[[[138,43],[138.5,43],[138.5,43.5],[138,43.5],[138,43]]]}},{"type":"Feature","properties":{"zone_name":"8110"},"geometry":{"type":"Polygon","coordinates":[[[138.5,43],[139,43],[139,43.5],[138.5,43.5],[138.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8111"},"geometry":{"type":"Polygon","coordinates":[[[139,43],[139.5,43],[139.5,43.5],[139,43.5],[139,43]]]}},{"type":"Feature","properties":{"zone_name":"8112"},"geometry":{"type":"Polygon","coordinates":[[[139.5,43],[140,43],[140,43.5],[139.5,43.5],[139.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8113"},"geometry":{"type":"Polygon","coordinates":[[[135,43.5],[135.5,43.5],[135.5,44],[135,44],[135,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8114"},"geometry":{"type":"Polygon","coordinates":[[[135.5,43.5],[136,43.5],[136,44],[135.5,44],[135.5,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8115"},"geometry":{"type":"Polygon","coordinates":[[[136,43.5],[136.5,43.5],[136.5,44],[136,44],[136,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8116"},"geometry":{"type":"Polygon","coordinates":[[[136.5,43.5],[137,43.5],[137,44],[136.5,44],[136.5,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8117"},"geometry":{"type":"Polygon","coordinates":[[[137,43.5],[137.5,43.5],[137.5,44],[137,44],[137,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8118"},"geometry":{"type":"Polygon","coordinates":[[[137.5,43.5],[138,43.5],[138,44],[137.5,44],[137.5,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8119"},"geometry":{"type":"Polygon","coordinates":[[[138,43.5],[138.5,43.5],[138.5,44],[138,44],[138,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8120"},"geometry":{"type":"Polygon","coordinates":[[[138.5,43.5],[139,43.5],[139,44],[138.5,44],[138.5,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8121"},"geometry":{"type":"Polygon","coordinates":[[[139,43.5],[139.5,43.5],[139.5,44],[139,44],[139,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8122"},"geometry":{"type":"Polygon","coordinates":[[[139.5,43.5],[140,43.5],[140,44],[139.5,44],[139.5,43.5]]]}},{"type":"Feature","properties":{"zone_name":"8123"},"geometry":{"type":"Polygon","coordinates":[[[135.5,44],[136,44],[136,44.5],[135.5,44.5],[135.5,44]]]}},{"type":"Feature","properties":{"zone_name":"8124"},"geometry":{"type":"Polygon","coordinates":[[[136,44],[136.5,44],[136.5,44.5],[136,44.5],[136,44]]]}},{"type":"Feature","properties":{"zone_name":"8125"},"geometry":{"type":"Polygon","coordinates":[[[136.5,44],[137,44],[137,44.5],[136.5,44.5],[136.5,44]]]}},{"type":"Feature","properties":{"zone_name":"8126"},"geometry":{"type":"Polygon","coordinates":[[[137,44],[137.5,44],[137.5,44.5],[137,44.5],[137,44]]]}},{"type":"Feature","properties":{"zone_name":"8127"},"geometry":{"type":"Polygon","coordinates":[[[137.5,44],[138,44],[138,44.5],[137.5,44.5],[137.5,44]]]}},{"type":"Feature","properties":{"zone_name":"8128"},"geometry":{"type":"Polygon","coordinates":[[[138,44],[138.5,44],[138.5,44.5],[138,44.5],[138,44]]]}},{"type":"Feature","properties":{"zone_name":"8129"},"geometry":{"type":"Polygon","coordinates":[[[138.5,44],[139,44],[139,44.5],[138.5,44.5],[138.5,44]]]}},{"type":"Feature","properties":{"zone_name":"8130"},"geometry":{"type":"Polygon","coordinates":[[[139,44],[139.5,44],[139.5,44.5],[139,44.5],[139,44]]]}},{"type":"Feature","properties":{"zone_name":"8131"},"geometry":{"type":"Polygon","coordinates":[[[139.5,44],[140,44],[140,44.5],[139.5,44.5],[139.5,44]]]}},{"type":"Feature","properties":{"zone_name":"8132"},"geometry":{"type":"Polygon","coordinates":[[[136,44.5],[136.5,44.5],[136.5,45],[136,45],[136,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8133"},"geometry":{"type":"Polygon","coordinates":[[[136.5,44.5],[137,44.5],[137,45],[136.5,45],[136.5,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8134"},"geometry":{"type":"Polygon","coordinates":[[[137,44.5],[137.5,44.5],[137.5,45],[137,45],[137,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8135"},"geometry":{"type":"Polygon","coordinates":[[[137.5,44.5],[138,44.5],[138,45],[137.5,45],[137.5,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8136"},"geometry":{"type":"Polygon","coordinates":[[[138,44.5],[138.5,44.5],[138.5,45],[138,45],[138,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8137"},"geometry":{"type":"Polygon","coordinates":[[[138.5,44.5],[139,44.5],[139,45],[138.5,45],[138.5,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8138"},"geometry":{"type":"Polygon","coordinates":[[[139,44.5],[139.5,44.5],[139.5,45],[139,45],[139,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8139"},"geometry":{"type":"Polygon","coordinates":[[[139.5,44.5],[140,44.5],[140,45],[139.5,45],[139.5,44.5]]]}},{"type":"Feature","properties":{"zone_name":"8140"},"geometry":{"type":"Polygon","coordinates":[[[136.5,45],[137,45],[137,45.5],[136.5,45.5],[136.5,45]]]}},{"type":"Feature","properties":{"zone_name":"8141"},"geometry":{"type":"Polygon","coordinates":[[[137,45],[137.5,45],[137.5,45.5],[137,45.5],[137,45]]]}},{"type":"Feature","properties":{"zone_name":"8142"},"geometry":{"type":"Polygon","coordinates":[[[137.5,45],[138,45],[138,45.5],[137.5,45.5],[137.5,45]]]}},{"type":"Feature","properties":{"zone_name":"8143"},"geometry":{"type":"Polygon","coordinates":[[[138,45],[138.5,45],[138.5,45.5],[138,45.5],[138,45]]]}},{"type":"Feature","properties":{"zone_name":"8144"},"geometry":{"type":"Polygon","coordinates":[[[138.5,45],[139,45],[139,45.5],[138.5,45.5],[138.5,45]]]}},{"type":"Feature","properties":{"zone_name":"8145"},"geometry":{"type":"Polygon","coordinates":[[[139,45],[139.5,45],[139.5,45.5],[139,45.5],[139,45]]]}},{"type":"Feature","properties":{"zone_name":"8146"},"geometry":{"type":"Polygon","coordinates":[[[139.5,45],[140,45],[140,45.5],[139.5,45.5],[139.5,45]]]}},{"type":"Feature","properties":{"zone_name":"8147"},"geometry":{"type":"Polygon","coordinates":[[[137,45.5],[137.5,45.5],[137.5,46],[137,46],[137,45.5]]]}},{"type":"Feature","properties":{"zone_name":"8148"},"geometry":{"type":"Polygon","coordinates":[[[137.5,45.5],[138,45.5],[138,46],[137.5,46],[137.5,45.5]]]}},{"type":"Feature","properties":{"zone_name":"8149"},"geometry":{"type":"Polygon","coordinates":[[[138,45.5],[138.5,45.5],[138.5,46],[138,46],[138,45.5]]]}},{"type":"Feature","properties":{"zone_name":"8150"},"geometry":{"type":"Polygon","coordinates":[[[138.5,45.5],[139,45.5],[139,46],[138.5,46],[138.5,45.5]]]}},{"type":"Feature","properties":{"zone_name":"8151"},"geometry":{"type":"Polygon","coordinates":[[[139,45.5],[139.5,45.5],[139.5,46],[139,46],[139,45.5]]]}},{"type":"Feature","properties":{"zone_name":"8152"},"geometry":{"type":"Polygon","coordinates":[[[139.5,45.5],[140,45.5],[140,46],[139.5,46],[139.5,45.5]]]}},{"type":"Feature","properties":{"zone_name":"8300"},"geometry":{"type":"Polygon","coordinates":[[[131,43],[131.5,43],[131.5,43.5],[131,43.5],[131,43]]]}},{"type":"Feature","properties":{"zone_name":"8301"},"geometry":{"type":"Polygon","coordinates":[[[131.5,43],[132,43],[132,43.5],[131.5,43.5],[131.5,43]]]}},{"type":"Feature","properties":{"zone_name":"8302"},"geometry":{"type":"Polygon","coordinates":[[[132,43],[132.5,43],[132.5,43.5],[132,43.5],[132,43]]]}},{"type":"Feature","properties":{"zone_name":"8303"},"geometry":{"type":"Polygon","coordinates":[[[130.5,42.5],[131,42.5],[131,43],[130.5,43],[130.5,42.5]]]}},{"type":"Feature","properties":{"zone_name":"8304"},"geometry":{"type":"Polygon","coordinates":[[[131,42.5],[131.5,42.5],[131.5,43],[131,43],[131,42.5]]]}}]} \ No newline at end of file diff --git a/src/assets/data/shiptype.js b/src/assets/data/shiptype.js new file mode 100644 index 00000000..adcacd13 --- /dev/null +++ b/src/assets/data/shiptype.js @@ -0,0 +1,182 @@ +export const shipTypeMap = new Map(); + +shipTypeMap.set('0', 'Not available'); +shipTypeMap.set('1', 'Reserved for future use'); +shipTypeMap.set('2', 'Reserved for future use'); +shipTypeMap.set('3', 'Reserved for future use'); +shipTypeMap.set('4', 'Reserved for future use'); +shipTypeMap.set('5', 'Reserved for future use'); +shipTypeMap.set('6', 'Reserved for future use'); +shipTypeMap.set('7', 'Reserved for future use'); +shipTypeMap.set('8', 'Reserved for future use'); +shipTypeMap.set('9', 'Reserved for future use'); +shipTypeMap.set('10', 'Reserved for future use'); +shipTypeMap.set('11', 'Reserved for future use'); +shipTypeMap.set('12', 'Reserved for future use'); +shipTypeMap.set('13', 'Reserved for future use'); +shipTypeMap.set('14', 'Reserved for future use'); +shipTypeMap.set('15', 'Reserved for future use'); +shipTypeMap.set('16', 'Reserved for future use'); +shipTypeMap.set('17', 'Reserved for future use'); +shipTypeMap.set('18', 'Reserved for future use'); +shipTypeMap.set('19', 'Reserved for future use'); +shipTypeMap.set('20', 'Wing in ground (WIG)'); +shipTypeMap.set('21', 'Wing in ground (WIG), Hazardous category A'); +shipTypeMap.set('22', 'Wing in ground (WIG), Hazardous category B'); +shipTypeMap.set('23', 'Wing in ground (WIG), Hazardous category C'); +shipTypeMap.set('24', 'Wing in ground (WIG), Hazardous category D'); +shipTypeMap.set('25', 'Wing in ground (WIG), Reserved for future use'); +shipTypeMap.set('26', 'Wing in ground (WIG), Reserved for future use'); +shipTypeMap.set('27', 'Wing in ground (WIG), Reserved for future use'); +shipTypeMap.set('28', 'Wing in ground (WIG), Reserved for future use'); +shipTypeMap.set('29', 'Wing in ground (WIG), Reserved for future use'); +shipTypeMap.set('30', 'Fishing'); +shipTypeMap.set('31', 'Towing'); +shipTypeMap.set('32', 'Towing, length exceeds 200m or breadth exceeds 25m'); +shipTypeMap.set('33', 'Dredging or underwater ops'); +shipTypeMap.set('34', 'Diving ops'); +shipTypeMap.set('35', 'Military ops'); +shipTypeMap.set('36', 'Sailing'); +shipTypeMap.set('37', 'Pleasure Craft'); +shipTypeMap.set('38', 'Reserved'); +shipTypeMap.set('39', 'Reserved'); +shipTypeMap.set('40', 'High speed craft (HSC)'); +shipTypeMap.set('41', 'High speed craft (HSC), Hazardous category A'); +shipTypeMap.set('42', 'High speed craft (HSC), Hazardous category B'); +shipTypeMap.set('43', 'High speed craft (HSC), Hazardous category C'); +shipTypeMap.set('44', 'High speed craft (HSC), Hazardous category D'); +shipTypeMap.set('45', 'High speed craft (HSC), Reserved for future use'); +shipTypeMap.set('46', 'High speed craft (HSC), Reserved for future use'); +shipTypeMap.set('47', 'High speed craft (HSC), Reserved for future use'); +shipTypeMap.set('48', 'High speed craft (HSC), Reserved for future use'); +shipTypeMap.set('49', 'High speed craft (HSC), No additional information'); +shipTypeMap.set('50', 'Pilot Vessel'); +shipTypeMap.set('51', 'Search and Rescue vessel'); +shipTypeMap.set('52', 'Tug'); +shipTypeMap.set('53', 'Port Tender'); +shipTypeMap.set('54', 'Anti-pollution equipment'); +shipTypeMap.set('55', 'Law Enforcement'); +shipTypeMap.set('56', 'Spare - Local Vessel'); +shipTypeMap.set('57', 'Spare - Local Vessel'); +shipTypeMap.set('58', 'Medical Transport'); +shipTypeMap.set('59', 'Noncombatant ship according to RR Resolution No. 18'); +shipTypeMap.set('60', 'Passenger'); +shipTypeMap.set('61', 'Passenger, Hazardous category A'); +shipTypeMap.set('62', 'Passenger, Hazardous category B'); +shipTypeMap.set('63', 'Passenger, Hazardous category C'); +shipTypeMap.set('64', 'Passenger, Hazardous category D'); +shipTypeMap.set('65', 'Passenger, Reserved for future use'); +shipTypeMap.set('66', 'Passenger, Reserved for future use'); +shipTypeMap.set('67', 'Passenger, Reserved for future use'); +shipTypeMap.set('68', 'Passenger, Reserved for future use'); +shipTypeMap.set('69', 'Passenger, No additional information'); +shipTypeMap.set('70', 'Cargo'); +shipTypeMap.set('71', 'Cargo, Hazardous category A'); +shipTypeMap.set('72', 'Cargo, Hazardous category B'); +shipTypeMap.set('73', 'Cargo, Hazardous category C'); +shipTypeMap.set('74', 'Cargo, Hazardous category D'); +shipTypeMap.set('75', 'Cargo, Reserved for future use'); +shipTypeMap.set('76', 'Cargo, Reserved for future use'); +shipTypeMap.set('77', 'Cargo, Reserved for future use'); +shipTypeMap.set('78', 'Cargo, Reserved for future use'); +shipTypeMap.set('79', 'Cargo, No additional information'); +shipTypeMap.set('80', 'Tanker'); +shipTypeMap.set('81', 'Tanker, Hazardous category A'); +shipTypeMap.set('82', 'Tanker, Hazardous category B'); +shipTypeMap.set('83', 'Tanker, Hazardous category C'); +shipTypeMap.set('84', 'Tanker, Hazardous category D'); +shipTypeMap.set('85', 'Tanker, Reserved for future use'); +shipTypeMap.set('86', 'Tanker, Reserved for future use'); +shipTypeMap.set('87', 'Tanker, Reserved for future use'); +shipTypeMap.set('88', 'Tanker, Reserved for future use'); +shipTypeMap.set('89', 'Tanker, No additional information'); +shipTypeMap.set('90', 'Other Type'); +shipTypeMap.set('91', 'Other Type, Hazardous category A'); +shipTypeMap.set('92', 'Other Type, Hazardous category B'); +shipTypeMap.set('93', 'Other Type, Hazardous category C'); +shipTypeMap.set('94', 'Other Type, Hazardous category D'); +shipTypeMap.set('95', 'Other Type, Reserved for future use'); +shipTypeMap.set('96', 'Other Type, Reserved for future use'); +shipTypeMap.set('97', 'Other Type, Reserved for future use'); +shipTypeMap.set('98', 'Other Type, Reserved for future use'); +shipTypeMap.set('99', 'Other Type, no additional information'); +shipTypeMap.set('100', 'Unspecified'); + +// E-Navigation +shipTypeMap.set('A001', '\uC5EC\uAC1D\uC120(\uC77C\uBC18)'); +shipTypeMap.set('A002', '\uC5EC\uAC1D\uC120(\uACE0\uC18D\uC120)'); +shipTypeMap.set('A003', '\uC5EC\uAC1D\uC120(\uC7D8\uC18D\uC120)'); +shipTypeMap.set('A004', '\uC5EC\uAC1D\uC120(\uCD08\uC7D8\uC18D\uC120)'); +shipTypeMap.set('A005', '\uC5EC\uAC1D\uC120(\uCE74\uD6FC\uB9AC)'); +shipTypeMap.set('A006', '\uC5EC\uAC1D\uC120(\uCC28\uB3C4\uC120)'); +shipTypeMap.set('A007', '\uC5EC\uAC1D\uC120(\uD654\uAC1D\uC120)'); +shipTypeMap.set('A008', '\uC5EC\uAC1D\uC120(\uC720\uB78C\uC120)'); +shipTypeMap.set('B001', '\uC5B4\uC120(\uC77C\uBC18)'); +shipTypeMap.set('B002', '\uC5B4\uC120(\uC720\uC790\uB9DD)'); +shipTypeMap.set('B003', '\uC5B4\uC120(\uC120\uB9DD)'); +shipTypeMap.set('B004', '\uC5B4\uC120(\uC548\uAC15\uB9DD)'); +shipTypeMap.set('B005', '\uC5B4\uC120(\uCC44\uB0DA\uAE30)'); +shipTypeMap.set('B006', '\uC5B4\uC120(\uC5F0\uC2B9)'); +shipTypeMap.set('B007', '\uC5B4\uC120(\uD2B8\uB864)'); +shipTypeMap.set('B008', '\uC5B4\uC120(\uD1B5\uBC1C)'); +shipTypeMap.set('B009', '\uC5B4\uC120(\uBCF5\uD569\uC5B4\uC5C5)'); +shipTypeMap.set('B010', '\uC5B4\uC120(\uC800\uC778\uB9DD)'); +shipTypeMap.set('B011', '\uC5B4\uC120(\uC790\uB9DD)'); +shipTypeMap.set('B012', '\uC5B4\uC120(\uAD8C\uD604\uB9DD)'); +shipTypeMap.set('B013', '\uC5B4\uC120(\uC6D0\uC591\uC5B4\uC120)'); +shipTypeMap.set('B014', '\uC5B4\uC120(\uC6D0\uC591\uCC38\uCE58\uC5F0\uC2B9\uC5B4\uC5C5)'); +shipTypeMap.set('B015', '\uC5B4\uC120(\uC6D0\uC591\uC120\uB9DD\uC5B4\uC5C5)'); +shipTypeMap.set('B016', '\uC5B4\uC120(\uC6D0\uC591\uD2B8\uB864\uC5B4\uC5C5)'); +shipTypeMap.set('B017', '\uC5B4\uC120(\uC6D0\uC591\uC800\uC778\uB9DD\uC5B4\uC5C5)'); +shipTypeMap.set('B018', '\uC5B4\uC120(\uC6D0\uC591\uCC44\uB0DA\uAE30\uC5B4\uC5C5)'); +shipTypeMap.set('B019', '\uC5B4\uC120(\uC6D0\uC591\uD1B5\uBC1C\uC5B4\uC5C5)'); +shipTypeMap.set('B020', '\uC5B4\uC120(\uC6D0\uC591\uC800\uC5F0\uC2B9\uC5B4\uC5C5)'); +shipTypeMap.set('B021', '\uC5B4\uC120(\uC6D0\uC591\uBD09\uC218\uB9DD\uC5B4\uC5C5)'); +shipTypeMap.set('B022', '\uC5B4\uC120(\uC6D0\uC591\uBAA8\uC120\uC2DD\uC5B4\uC5C5)'); +shipTypeMap.set('B023', '\uC5B4\uC120(\uC6D0\uC591\uC5B4\uC5C5\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C001', '\uD654\uBB3C\uC120(\uC77C\uBC18)'); +shipTypeMap.set('C002', '\uD654\uBB3C\uC120(\uBC8C\uD06C\uC120)'); +shipTypeMap.set('C003', '\uD654\uBB3C\uC120(\uC591\uACE1\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C004', '\uD654\uBB3C\uC120(\uC6D0\uBAA9\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C005', '\uD654\uBB3C\uC120(\uAD11\uBAA9\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C006', '\uD654\uBB3C\uC120(\uC11D\uD0C4\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C007', '\uD654\uBB3C\uC120(\uC2DC\uBA58\uD2B8\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C008', '\uD654\uBB3C\uC120(\uC790\uB3D9\uCC28\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C009', '\uD654\uBB3C\uC120(\uD56B\uCF54\uC77C\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C010', '\uD654\uBB3C\uC120(\uCCA0\uAC15\uC7AC\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C011', '\uD654\uBB3C\uC120(\uBAA8\uB798\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C012', '\uD654\uBB3C\uC120(\uD3D0\uAE30\uBB3C\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C013', '\uD654\uBB3C\uC120(\uCF54\uC77C\uC6B4\uBC18\uC120-RORO\uC120)'); +shipTypeMap.set('C014', '\uD654\uBB3C\uC120(\uB0C9\uB3D9, \uB0C9\uC7A5\uC120)'); +shipTypeMap.set('C015', '\uD654\uBB3C\uC120(\uCEE8\uD14C\uC774\uB108\uC120)'); +shipTypeMap.set('C016', '\uD654\uBB3C\uC120(\uC6D0\uC720\uC6B4\uBC18\uC120)'); +shipTypeMap.set('C017', '\uD654\uBB3C\uC120(\uC11D\uC720\uC81C\uD488 \uC6B4\uBC18\uC120)'); +shipTypeMap.set('C018', '\uD654\uBB3C\uC120(\uAE30\uD0C0 \uC720\uC870\uC120)'); +shipTypeMap.set('C019', '\uD654\uBB3C\uC120(\uCF00\uBBF8\uCE7C \uC6B4\uBC18\uC120)'); +shipTypeMap.set('C020', '\uD654\uBB3C\uC120(\uCF00\uBBF8\uCE7C\uAC00\uC2A4 \uC6B4\uBC18\uC120)'); +shipTypeMap.set('C021', '\uD654\uBB3C\uC120(LPG \uC6B4\uBC18\uC120)'); +shipTypeMap.set('C022', '\uD654\uBB3C\uC120(LNG \uC6B4\uBC18\uC120)'); +shipTypeMap.set('C023', '\uD654\uBB3C\uC120(\uC77C\uBC18\uD0F1\uCEE4)'); +shipTypeMap.set('D001', '\uAD00\uACF5\uC120(\uC77C\uBC18)'); +shipTypeMap.set('D002', '\uAD00\uACF5\uC120(\uD574\uACBD\uC815)'); +shipTypeMap.set('D003', '\uAD00\uACF5\uC120(\uC870\uC0AC\uC120)'); +shipTypeMap.set('D004', '\uAD00\uACF5\uC120(\uC9C0\uB3C4\uC120)'); +shipTypeMap.set('D005', '\uAD00\uACF5\uC120(\uC2DC\uD5D8\uC120)'); +shipTypeMap.set('D006', '\uAD00\uACF5\uC120(\uAD70\uC120)'); +shipTypeMap.set('D007', '\uAD00\uACF5\uC120(\uD574\uACBD\uD56D\uACF5\uAE30)'); +shipTypeMap.set('D008', '\uAD00\uACF5\uC120(\uBC29\uC81C\uC120)'); +shipTypeMap.set('D009', '\uAD00\uACF5\uC120(\uC758\uB8CC\uC120)'); +shipTypeMap.set('E001', '\uAE30\uD0C0(\uC77C\uBC18)'); +shipTypeMap.set('E002', '\uAE30\uD0C0(\uC608\uC120-\uB3C4\uC120)'); +shipTypeMap.set('E003', '\uAE30\uD0C0(\uC678\uAD6D\uC801\uC120)'); +shipTypeMap.set('E004', '\uAE30\uD0C0(\uC720\uB958\uAE09\uC720\uAE09\uC218\uC120)'); +shipTypeMap.set('E005', '\uAE30\uD0C0(\uD1B5\uC120)'); +shipTypeMap.set('E006', '\uAE30\uD0C0(\uBD80\uC120)'); +shipTypeMap.set('E007', '\uAE30\uD0C0(\uC2E0\uC870\uC120)'); +shipTypeMap.set('E008', '\uAE30\uD0C0(\uD2B9\uC218\uBAA9\uC801\uC120)'); +shipTypeMap.set('E009', '\uAE30\uD0C0(\uC900\uC124\uC120)'); +shipTypeMap.set('E010', '\uAE30\uD0C0(\uC720\uCC3D\uCCAD\uC18C\uC120)'); +shipTypeMap.set('E011', '\uAE30\uD0C0(\uC704\uADF8\uC120)'); +shipTypeMap.set('E012', '\uAE30\uD0C0(\uC7A0\uC218\uC9C0\uC6D0\uC120)'); +shipTypeMap.set('E013', '\uAE30\uD0C0(\uBC94\uC120)'); +shipTypeMap.set('E014', '\uAE30\uD0C0(\uACE0\uC18D\uC120-HSC)'); diff --git a/src/assets/img/default-ship.png b/src/assets/img/default-ship.png new file mode 100644 index 0000000000000000000000000000000000000000..7e61ce6f81ca22aec4d681d353488ce49712c46b GIT binary patch literal 1293 zcmeAS@N?(olHy`uVBq!ia0y~yVCi6BU|hk(3=|2tW_A!rF%}28J29*~C-V}>;Vkfo zEM{QP@djbWrMA;n0tI;jd_r75eE4wf+O^%gcduHt>dmid+CZabdAc};RNQ)V=WOrg z0EX6!&pjdzRh#J@4BgQZX&~)YAR64{ed3VE<7ibcCy@=0(r@tSoYQ=Gd20RAaGbuX6sY|v5|;h^1`*zs;ksrP1@{o)g^e7SBUo&m6IBQs)4h2 zp49z-nrS?@D)CxtG$HM?%U z%3_z6H1Bq)?)-IUjnG@wS)VRzT>R|f`!20>o_bdCc8@PMf%A8VE_&Yi$-rxi@0Xud zr|V~^-cpk~7PQMoM<`eLY0Rl9?d6?joqm=2zH3%YTH$@nEG)#+M5Q?2b*hW+q|{A| zycU^>1BI?UKh&kwsitk^apg!CpU~EgK!5EOl~tX!=7qvV&9IUQE4oExRixIu&#$uDR!vgvw5pUlQ}}}k9Uc}oei>ORn9Lw`SSLH~zb%<50G@ z?aO)DqL*|D-HYGUnoKs%-|li- zRg@84vE}dYAnAccyip z!~45mXJv@1>dGG8p=!m(BeVa5*`I41xvYy!KEBs4ymxKUo&&9Bb#iIUxz%CuFOw?+`M(YVUcaHaWn5~h>nYp@k3T}OivV^Sz|Q=)p8aRv%>IvHZ|(mJ z`4_z<{|Ef1|0DmC{$HhL0TUAwUtiy_urNnQM>{(^A0HnEQAJ{ET1qB%IsqBw=yIdd zDT~HUyY3U`k669jAwDAyHbp}|6Yoa>Z^WHrHPYJHRZN-06u=N%avFNtJ9k}}0IYT! zRCcO=5Lxlr-KnME0b&?kz5B z@krT1#oos7zXr|WKh4X_D<~)^K0ZD>J3Bo+z2+qlS9zVbk<5>Bcf zdGAw(RSkHxA~;DIlCA_vmjZ<=;iPF2)eQl_3IO~904xBE13(!7)B`{T05sofDykYz z&MF3mI?e~RoSe1y@27B3IU@mY76KEcKcj-czAL{SPo8390=r=4}b^=2-t3y`EBSn1qV#d z%l43*;DH^xlpO5?Z)!RB8kx_xksEIv4P5{Xy{kYbNf)|4cXWlt{@MB9*A=lD!oU9iw(G<3jlvjL-j>sUxB-(~aFt#E{`G&8{@;XG-7AOJf3etq z;qi)}8#h;-t_j*QJkDExu{VR)6^toso429Mp_}dc_~FvEV=Q(6>y~xgU^G&P{WjS? zlBDVxW-m^AWO9zh7HHoi=UIP*MBqblKpsXg`7KUoe%4*`r5hRuAtQ!5 z!v@{vKOA_&0)FiKj_msiymUF^UF)|cA)1o=^02yxcRthHom@v5vus!LF#WCIc8acM ze@(s3NIi{Oy_w6{KFi3T#moq%QH_>WwKt)=rvZ6~r(vXAoN(jny0_CxmA}9DRaHgj zGmErKq9|b>Oa)W&bvb0W-H( z{u#>Eyu2d&OQ?6ZI&(YZz89lpBv+v0Wvh}pH%-bMZvPM#zAZmu+bscj!VVC4bflTS0ezvjK1Uq<;# zFy(&6CtJvi>nReMzuwY7`X}O+xhARozxAIE&q&IRW~JLR4WMNY-go?TF4KDrjw!_J z9TM=t``Tyf-e+GEa&C>Pgvx76$za!&#+Ob{JpifBAKv~!jLChZ?j#^kG*0{GsRR?} zk;6hUDA@nBs9Mx53OG*{;r&@${H!UR)0+6qfxci%r((v;voD4SG7 zU0i}ZKqS!PNPa*=BuuQmwG|rcCriwQ`odtS+VYHJMA=iRc>Qf6u$TVUSKrvmT}0QZ z7^whsWOIZ1emLzFD8P1KcNG4Sc!{<%kR*ScM6lt^p*MsnncH<6dJGo{V^x5~l|T0H zN;U*8Yq`r8C-`0?wgNj6Aw87qHt@qp%5E*{FS&0W2>|DyPj;Y$s+kC6@50DP9XiVf zhwpneAS)&g&K<7bwtPh!2yw4@`9>gUI$&r>>2+Ryu2JxaQ(r2 zqw%7HaNn9jR&}UK9gcdOna4D#F{*~MyWX^)zMIcrXFgiw{IcOa-+cK}aA6-6UBTmc z{0Z^%*9#EH^TiZRPX!xl7_Rl58ThNgd#CVC5pguG>r3jmcV4}$Yw_&w-)nxTh5D@v zY?aT@4Q8q}J>ywBehmteL`9S9iVsPcP{y&ahFQ~X^|rI*^s*W;gka;Bd1c>g=D#jM zzl&{xuOMGHPUy=P4tuI_Yra0;8hO>8CzZ{($)hPuqd{h4%ihRl zi|2DzWEqSCa)Lnl0}Ee#OkNPwjvR~6wirt_Y|J^8LfLPudAoNNE}!GhtLVd|(g~v% z6sM|V0!~lQ_Cay#aczFgcfat(6kiGg;Jzm>>%#r2RX@fR$DW#p?zlYOn%9x6>*~r^ zexUs1`RPh$jra031|Cp0Rp0D)6;`bK`BOP3$D9~i#Vtp5)lzb=S6UL|NL*X z%nDI$+eLITiasajE;K9PXpO~`gfY+pN>2~9u-**8(aKnZrBn!0P-f32kWtXOZASwW zRt38vqN4li`{f<->xL3#W3sH7l~Kxe{>4{bUd>*>P(r`)_rO4NT_LhDg8>1~W$zS< zY*?Y8NPf)FAEHOxdBe7H4&meuThO$7CeFoaT3F?7<;Du;Pq2nC{o;`n=ZCK}@5r+Q z6XEg#2E3bkUehG>hxD`_rQneq%6J3ceq;B4h}^n~0P*)6+$XGF>j>f}C@R5@mMPi7 zPF-u-3T_Go0-gx?;Q^(%@|Kqm)vk?E-XAlzfoGB+`(OUDGAG}u?yF2$GpRT!qHNBqHjNB0PxZ0T|jdFTRBM(XTrIxh4LK>o|JKk|;Tf0+CDrT_w9(Tm?t zGJDK+Z@r=e6ti8^``el4mHLlZc^_aPOVEKG$>^`ILT$_(WuM>@QGQwlH>-|aD)YuA zi(Rxus2U`c%5kfT(10nua8MfZvCNeH)KpBVfzk^ak(B}*)H>g%o3m(n9$Ul5$4m9B zkg2o!1zH?Af+?)^5f6AAG(}$+*8%V1xRw2xx8aViMcjhiBMDCi;57JTOb?(c+7Jyg zhbBpK%)MT&xC4y^VDz#JZ~Sp%Ml#L&l<4hLH$n%I+5;wJ8&1&s$Le1iif94Y%!=w}2 z?_IhIsN*OtAnt@sK2SwF>2Ym_FY8%;4s%ui~k2(~H2zop|uh)1jjmX14nC{d-$T2V~!x?03)swPtbo-;15B3iZ4i66$ zYQlt;mDs%h!Z#i~!w_4KWSR-^GBQ3vn(6*HsulS?tybGz4y~`HmYD|0GkGBBgO!qY zN!Tm=h^P!_(Isaak{wB^O6%95f0%JU+Vsd1g`uDI4*Xh?4ONgW%)`c{@DxNTlLdE{ z8u4l4Jbu-z`#LQk)mPQP3Y?<+{l0S%2RKYTuR7F0wx3(p|4wODJwNW)(ABsCVch~c zLfMHbT% zb|sx~1#3=aivi=(cb}|&yt3N+uWZ(R8b~_Tzq=%u#M!J;GBd3)XAYJueqz1K$@}B| zOm5M1O|5{)V*e1PwDa~Xn8d` z{!oOVPM|K9tr1cq9VpwzJUqFFK~JWrcAX?7Xm;O!&~x+)TIfLw;~+9GHTO5usOntu z7d2u#>QD`l72?Sa_b&|P@Voh&+*=SL=E|ZK%8S!$ zOSx8y(39go7Wp%@IafY9D9>ClGFW^eN*I%yh#!O!Nh9sZ#wAK{R^Jcv6e&~>^d%M= zU&sPbB0npO8b}rSMDgg}IY(?qI#16EHhAEhkrfMhS6pv8Pm^ga#3=EJ8k8|*M9qlN zH$f_$^{T_cs(o{ghDistZx8o`mWzm4PW0r( zz&ZD_1JzrSF3}%L8Qs&{p2ogy(VD~o&)Y_cBqWv@D?UI6=rh6ZRT`zK zA=GXP?J#B1h0<>#DRlvm36}~+7x#>3MIoFSZOkUOV%!Z-AMT+ z^xMOqptm4f%diV)g(*f(jwh#@c6&}1d1UXS_@>1N|0pl@s%;K{EFvg0=rtZ5$R4`_Jc3C{MtxKl+lNx58e<){%rd#3Cu| z4scfQrNiXU90QVmposDCWzo!Qm(KTa=Mx%p`ii052@@2bhK#{V+lcyLB3Z4%H!0dd z>PTNYuKWbrL#Ic8S)s<;$mo}bQEQb#*6I&G(x?k&26D$SixhQ+b>Ahn0yoo&#}}cH z?WV4E?#wQ;xq`jYY;rOFTohr+GZJUtzSTap$VxqLwHZV|x}y2%B;~cE^vb_PPw-hL z$Gy9F@lvGX6?*4}=$pjAv%y6YMJ=Xw^>AZB|82xdR~Iu=ASQ6}Q{1H#)mM>~6A$aY zXV1}bxpsQsb&DR^^)i`aq=+hW+!s$Q-IBOITJPwU7C{S$~`zn2H*ici~B% zNaJ;5KBsa0G3FY)`0=t3!_J!VdHCAK#}5;NYY7%EIUp`=xHDxG^A7Ej*o;GjTFI1&3FJ+rS~g`}=#gC8;np?ygTKI}=&q9TWGjxLUl zuFeuOcJm7%OWLh(2R>XMus7ZRGtKDswWclj9nrb)rX+;=aFxEb%F1Fb@4j8g)QW_> z`#FM2*?nI*I_gGG(1fw|^jkrk5=n69T)DuV%6rcpoQ7I>i?&fX+S=v;r~~Yu@Y1e_ z!I!^|H{X*S{ypJ+V9-9LSRXlqf>nSTA`VWN=V=D%Md~fPo!pvnAoOV#e8H*x@@I+g zli3x;iBbBmlA7-PlMnT0T-%pTi!%$$?xwa^{J8oe{(Hu8y8&Xhq3875)zgvmL2LKs z((azrYqQnB8nCk@|KE!ttBduM`V*~=2SI&Cl^O`NO8wfWdyHq#MGbI&bqJ4?T~vK8 z12W=I18+{1Q0HNpm70mExRQQ7b4yDneR8xnoVnEQw0gkqC4IN>^oCPe;0ru$iIO?K z?)POzcp#2f$jIcgfsbcSteN+IzbPqU@JC-%+HA9nS7b901ksM z6PejAo@De#7(6koaitV9Fa$T_t1H1lw?tl@9M}uVbe<#uqq@Kj2yjA?07bvtueRwx zVk6>}U$lW8Up};#BV?`4Ga#IbMXI5SSs++)5qtu(~xHw6K>{dg&rZoLEW4N)g=@Q3Ce0%1}f-o*p}(t z&vfH@&cQhZ)!w+OYGdjy4U`VK!KVmF+L1{}F3YtF$^^b<#t&Gv>0J6J9rk{dgm*it za+^y_XZ{QY>|qj{(@TvKg3-^OYc}5ad6=TF8$R_diM2IhRND+gD^9d7y%hwuqKI4q zi5_vhJHVj4n*a$xv$-5*I+BdZ7OwQ}{aHw<@Gq6!{WnEgXgDEm6N`)Ujap7fi!jb% zb&Vi?;3Ss03?JxZb8kX7)6ND>dq|}#QCKcFIF`l?_DnJr_*)SdAL;tp@2Pl^39Tkx-)XF zioGeqq{xNbrm|QMj#?L>N<-tkR*Z`3)ss{dhc&Vb6VYl8wjfG+h`o=`FaZUEVpL6S zs+8}MbJ{_yIyr|EF7%kmEu)Y^^l|5^`N=kgD%rMkoQcM2m;hGZ5eJb}D@080psr~F zZwlFh3HJ!UfWu6llg~4V$^FcSN$fO0?ho>;YN;87`k-fN8$4spXS_UYZ;8V8^+&Sf z&6g%@3M_u9Il_FE6>;(`!~L2eV*@M#wr0>88ujPHp^At|^V|X$S=HbpN|2PWf>VWR zlKi^C?o0iL|hnX&!)>c+zAXKx=`1&1`HQ(ZBHh5b-Ee2V&&V8w?pI ziR?RuA5?DO} zG@_n=fuKM)1P3wsL?Vi84P@2$jGziS0>su?Ywtqt6cQjbU*k^(c1DT3^m0(OD;TV2 z1-KOjL%E-<0(o>L%x`sR$N6d5?X?9%-y#`Eyc}SY4x&l@`am2F6HXIk)+)V`48gBj zLT16>B;Ke9(xDx%gQf`jJXg^hL&qGq@pL4m1AB&_n5gPz`O}jjlUg=7VH6{2Kn#th z;K&>)L!bWO?%7M^wDE?8{LncYqrD1pL4Fa<2QbL=-8{P)5pmbek&XKX25_9ax0QyF=p;>}PW+d#cF%@;4V99?p zmD(xkn9`X!NcZf@PBK_=Klh|cF6NUZ5b}8~7^D4Dj~*f8;fN@hcNU_!Ap(?>i|UwU z@6$dM31+-`>Lw!D@porZS+%o_yy#B6J<|;nh+Uy@p;9BEMyFvR4bGGJRw&0!HAQg3 zk2zy@XG({e{Ik5bMASJE577(ub%Pjwf@6C8v*vu zF|uhBLZzrDBHSItv(paRg=*iZbo)BaL!4g?N`sb)Ey-(5>8@_QsK@_-MNH|2SLJJq z*e%`*IfEs6h=!PJwkYZRzYq1lXeTJnoyRI6ntcJK-g#&NqetSB|@xV&cLr7Ur@~RkRi(Op4R1c z5+8wj;0t*;UcQ`E^vEv4Vb&i;SH3>Nl}20JDsM(A6cF|31~$C z#bB~`Ky-I$)fryjk!Vnd^mFtx98$D&F)Xyt6cC#y!K)e=K3- zm&ULTh3q5~MgRq43>p3*&2bY6Xx70HKLI&X^{!;f>OUv*4mffRBwj}=2u|YsdYotT&0($-w10>@SrQN28z+hs z6=oUdXOvF>>Egpdza%#}&`YJ6=oo&nXL_+geGbWsD8nJT?hn{u8=T*Xa{TQ=%0pt; zIF)ua0dJd<$|QdN4!+?Z?ZND<6%56fJ-njn*`y;a8fPPThCQz+-8r(F=D#00NBuLP zmhs`uTK9CarA%|E9)HR2(`Z)<>7#@85Z^lFx+0wmMuTKP(-~P1&$t*x|D7O!1Vi0+ z-i7r(I0%qUxJl6-%JO!=S!d_0y1IY$5D$U11YjsqXP`=VC?e-t+l4QZU|QzYwgjeQOkc2Ymb zSz{CHJ91Oc0;NI{Ux%EO*lekJTc>``~kH@NxOpNGlzvSUR9mlxO%Mt^(t zi!XL$#;|*2=Tmp9d;X39UZUi6P7Pkw0^?p;T`j6O8WPzEO;)moQxDG|DN*{z^104Y zwAgJb{bJdmo&G*UW{F)!fP<_q$IgNFTl^R)c`#$&-%gy4+AkW^1*9rFh*zsU>@juIF@3@#VY^2`0cVT&QDm^5jH|H?5hhyVI+`zkb8 z@&OKM=KcTIR3N=Kx9d`KlPnBXXrqHA( zuF*j(MQ^9ILSNbyQC@y|B2gBhQ84q)i9ztZJ#9N81vHvE%b{<~WQz=aIaN+;C0lI9 z;nV|eYvi_#REEhmq}Paq<4&h@-&&hc4Qqy?uQ*I#8Hh#c9NVA1Xff$s=Xgc4 z@WwO`78CevxHZ-_G|EjO&pwSDtgN__kAG!6(+u3(9El00<3ge+S`)@14s#+qShOZ3 zK17phLJUXzl1{Vu^cKCTSbM6+v^beEJH2Q*w)6tQL##~K+lCF7c^bjQ9o)g`0mTwlC(6A?) z{OS73lPa^h_AkIAowRekFN{bQ_q6`DmmY@IGeTpMPmSAV_r(aA20rnH1OP}Kzic&o z<&L&h^hH3qkbRYL(PLz^N}i^z3C`A}3eTR+sS*8-roQ8veYvmzJ6G&xgZLOA9=e+z zbJ|teM04nz>mB{l4|nH~7T}nEo8px=ag-6NK6U*gFQs5leGSKopk|T}56N%sWek1& z^Z%FjuL9$0eRDn@p~t(WcY{~1{zxkB?j@7bbmTUQZlzQdv0!8A0%XDTE!s@e{rRXG`)9_<6OZARRD(tjjNkn_;}hiaJws{Z zzPv7c47oUF;k>$R$o@iokr|-{H$__T1(w!mE*=!mG2Z4dw8jQUQ9JEvEYTni+p?>}s^kM73#rhA%qa+XpVQz%9WNaafzcO7bYiIHe0^8nX|9 zdvY|Egj!OUkgIj^aK2p#&Qps(zqlN+N1+xTnQ^%>=*+w} z@k<4s{2HN@skoh0a|EHVhi@AjF%H$r=? zK`^No!?hNXJ+%N80%`?neB5Ubf+=&FQ<+5j-Oe2lg%?ct1cqY^vP4qx(YVhTJ^3yb z<-prFdS5y<(P|h79Z0b=MA5|HW`#Bb)CXl!P{Iy4)DUfl;>Q%$Inpr z;UB>hLqI5kM6gbpd!7mVa>9 zv>Lr|8_rd<-}DZdsFlZgAa0Va;L+Me$_IZp zFJ1juTn \ No newline at end of file diff --git a/src/assets/img/shipDetail/detailKindIcon/etc.svg b/src/assets/img/shipDetail/detailKindIcon/etc.svg new file mode 100644 index 00000000..b2905c9d --- /dev/null +++ b/src/assets/img/shipDetail/detailKindIcon/etc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipDetail/detailKindIcon/fishing.svg b/src/assets/img/shipDetail/detailKindIcon/fishing.svg new file mode 100644 index 00000000..65bd936a --- /dev/null +++ b/src/assets/img/shipDetail/detailKindIcon/fishing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipDetail/detailKindIcon/gov.svg b/src/assets/img/shipDetail/detailKindIcon/gov.svg new file mode 100644 index 00000000..f30fb141 --- /dev/null +++ b/src/assets/img/shipDetail/detailKindIcon/gov.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipDetail/detailKindIcon/kcgv.svg b/src/assets/img/shipDetail/detailKindIcon/kcgv.svg new file mode 100644 index 00000000..50d0f20d --- /dev/null +++ b/src/assets/img/shipDetail/detailKindIcon/kcgv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipDetail/detailKindIcon/passenger.svg b/src/assets/img/shipDetail/detailKindIcon/passenger.svg new file mode 100644 index 00000000..c5c8da7e --- /dev/null +++ b/src/assets/img/shipDetail/detailKindIcon/passenger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipDetail/detailKindIcon/tanker.svg b/src/assets/img/shipDetail/detailKindIcon/tanker.svg new file mode 100644 index 00000000..fe9b1533 --- /dev/null +++ b/src/assets/img/shipDetail/detailKindIcon/tanker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/bouy.svg b/src/assets/img/shipKindIcons/bouy.svg new file mode 100644 index 00000000..3cd0df45 --- /dev/null +++ b/src/assets/img/shipKindIcons/bouy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/cargo.svg b/src/assets/img/shipKindIcons/cargo.svg new file mode 100644 index 00000000..9045d733 --- /dev/null +++ b/src/assets/img/shipKindIcons/cargo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/etc.svg b/src/assets/img/shipKindIcons/etc.svg new file mode 100644 index 00000000..13eea7d4 --- /dev/null +++ b/src/assets/img/shipKindIcons/etc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/fishing.svg b/src/assets/img/shipKindIcons/fishing.svg new file mode 100644 index 00000000..d3b098fd --- /dev/null +++ b/src/assets/img/shipKindIcons/fishing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/gov.svg b/src/assets/img/shipKindIcons/gov.svg new file mode 100644 index 00000000..7ed615ed --- /dev/null +++ b/src/assets/img/shipKindIcons/gov.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/hazard.svg b/src/assets/img/shipKindIcons/hazard.svg new file mode 100644 index 00000000..0bdf0328 --- /dev/null +++ b/src/assets/img/shipKindIcons/hazard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/kcgv.svg b/src/assets/img/shipKindIcons/kcgv.svg new file mode 100644 index 00000000..5afdfa0c --- /dev/null +++ b/src/assets/img/shipKindIcons/kcgv.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons/pass.svg b/src/assets/img/shipKindIcons/pass.svg new file mode 100644 index 00000000..f1ec302e --- /dev/null +++ b/src/assets/img/shipKindIcons/pass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconsetc.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconsetc.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons new file mode 100644 index 00000000..3cd0df45 --- /dev/null +++ b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconsetc.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconshazard.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconshazard.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons new file mode 100644 index 00000000..9045d733 --- /dev/null +++ b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconshazard.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconskcgv.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconskcgv.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons new file mode 100644 index 00000000..7ed615ed --- /dev/null +++ b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconskcgv.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconspass.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconspass.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons new file mode 100644 index 00000000..d3b098fd --- /dev/null +++ b/src/assets/img/shipKindIcons && cp CUserslht87IdeaProjectsmda-react-frontsrcassetsimgmainGisshipKindIconspass.svg CUserslht87IdeaProjectsdarksrcassetsimgshipKindIcons @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/common/stompClient.js b/src/common/stompClient.js new file mode 100644 index 00000000..81894628 --- /dev/null +++ b/src/common/stompClient.js @@ -0,0 +1,237 @@ +/** + * STOMP WebSocket 클라이언트 + * 참조: mda-react-front/src/common/stompClient.ts + * 참조: mda-react-front/src/map/MapUpdater.tsx + */ +import { Client } from '@stomp/stompjs'; +import { SHIP_MSG_INDEX, STOMP_TOPICS } from '../types/constants'; + +/** + * STOMP 클라이언트 인스턴스 + * 환경변수: VITE_SIGNAL_WS (예: ws://10.26.252.39:9090/connect) + */ +export const signalStompClient = new Client({ + brokerURL: import.meta.env.VITE_SIGNAL_WS || 'ws://localhost:8080/connect', + reconnectDelay: 10000, + connectionTimeout: 5000, + debug: () => { + // STOMP 디버그 로그 비활성화 (너무 많은 로그 발생) + }, +}); + +/** + * 파이프 구분 문자열을 배열로 파싱 + * 메인 프로젝트 메시지 형식: "value1|value2|value3|..." + * @param {string} msgString - 파이프 구분 문자열 + * @returns {Array} 파싱된 배열 + */ +export function parsePipeMessage(msgString) { + return msgString.split('|'); +} + +/** + * 메시지 배열을 선박 객체로 변환 + * 참조: mda-react-front/src/feature/commonFeature.ts - deckSplitCommonTarget() + * + * @param {Array} row - 파싱된 메시지 배열 (38개 요소) + * @returns {Object} 선박 데이터 객체 + */ +export function rowToShipObject(row) { + const idx = SHIP_MSG_INDEX; + + const targetId = row[idx.TARGET_ID] || ''; + const originalTargetId = row[idx.ORIGINAL_TARGET_ID] || ''; + const signalSourceCode = row[idx.SIGNAL_SOURCE_CODE] || ''; + + return { + // 고유 식별자 (signalSourceCode + originalTargetId) + featureId: signalSourceCode + originalTargetId, + + // 기본 식별 정보 + targetId, // 통합 TARGET_ID + originalTargetId, // 개별 장비 고유 TARGET_ID + signalSourceCode, + shipName: row[idx.SHIP_NAME] || '', // 선박명 + shipType: row[idx.SHIP_TYPE] || '', // 선박 타입 + + // 위치 정보 + longitude: parseFloat(row[idx.LONGITUDE]) || 0, + latitude: parseFloat(row[idx.LATITUDE]) || 0, + + // 항해 정보 + sog: parseFloat(row[idx.SOG]) || 0, + cog: parseFloat(row[idx.COG]) || 0, + + // 시간 정보 + receivedTime: row[idx.RECV_DATE_TIME] || '', + + // 선종 코드 + signalKindCode: row[idx.SIGNAL_KIND_CODE] || '', + + // 상태 플래그 + lost: row[idx.LOST] === '1', + integrate: row[idx.INTEGRATE] === '1', + isPriority: row[idx.IS_PRIORITY] === '1', + + // 위험물 카테고리 + hazardousCategory: row[idx.HAZARDOUS_CATEGORY] || '', + + // 국적 코드 + nationalCode: row[idx.NATIONAL_CODE] || '', + + // IMO 번호 + imo: row[idx.IMO] || '', + + // 흘수 + draught: row[idx.DRAUGHT] || '', + + // 선박 크기 (DIM) + dimA: row[idx.DIM_A] || '', + dimB: row[idx.DIM_B] || '', + dimC: row[idx.DIM_C] || '', + dimD: row[idx.DIM_D] || '', + + // AVETDR 신호장비 플래그 + // 값: '1'=활성, '0'=비활성, ''=장비없음 + ais: row[idx.AIS], // AIS + vpass: row[idx.VPASS], // V-Pass + enav: row[idx.ENAV], // E-Nav + vtsAis: row[idx.VTS_AIS], // VTS-AIS + dMfHf: row[idx.D_MF_HF], // D-MF/HF + vtsRadar: row[idx.VTS_RADAR], // VTS-Radar + + // 다크시그널 판단은 shipStore에서 처리 (darkSignalIds Set + isAnyEquipmentActive) + // 장비 활성 플래그(ais, vpass, enav, vtsAis, dMfHf, vtsRadar)는 위에서 개별 저장 + + // 원본 배열 (상세정보 등에 필요) + _raw: row, + }; +} + +/** + * AVETDR 문자열 파싱 + * @param {string} avetdr - "A_V_E_T_D_R" 형식 (0 또는 1) + * @returns {Object} 각 신호원 활성 상태 + */ +export function parseAvetdr(avetdr) { + const parts = (avetdr || '0_0_0_0_0_0').split('_'); + return { + A: parts[0] === '1', // AIS + V: parts[1] === '1', // VPASS + E: parts[2] === '1', // ENAV + T: parts[3] === '1', // VTS_AIS + D: parts[4] === '1', // D_MF_HF + R: parts[5] === '1', // RADAR + }; +} + +/** + * STOMP 연결 시작 + * @param {Object} callbacks - 콜백 함수들 + * @param {Function} callbacks.onConnect - 연결 성공 시 + * @param {Function} callbacks.onDisconnect - 연결 해제 시 + * @param {Function} callbacks.onError - 에러 발생 시 + */ +export function connectStomp(callbacks = {}) { + const { onConnect, onDisconnect, onError } = callbacks; + + signalStompClient.onConnect = (frame) => { + console.log('[STOMP] Connected'); + onConnect?.(frame); + }; + + signalStompClient.onDisconnect = () => { + console.log('[STOMP] Disconnected'); + onDisconnect?.(); + }; + + signalStompClient.onStompError = (frame) => { + console.error('[STOMP] Error:', frame.headers?.message || 'Unknown error'); + onError?.(frame); + }; + + signalStompClient.activate(); +} + +/** + * STOMP 연결 해제 + */ +export function disconnectStomp() { + if (signalStompClient.connected) { + signalStompClient.deactivate(); + } +} + +/** + * 선박 토픽 구독 + * - 개발: /topic/ship (실시간) + * - 프로덕션: /topic/ship-throttled-60s (위성망 대응) + * @param {Function} onMessage - 메시지 수신 콜백 (파싱된 선박 데이터 배열) + * @returns {Object} 구독 객체 (unsubscribe 호출용) + */ +export function subscribeShips(onMessage) { + // 환경변수로 쓰로틀 설정 (VITE_SHIP_THROTTLE: 0=실시간, 5/10/30/60=쓰로틀) + // const throttleSeconds = parseInt(import.meta.env.VITE_SHIP_THROTTLE || '0', 10); + const throttleSeconds = 60; + + const topic = throttleSeconds > 0 + ? `${STOMP_TOPICS.SHIP_THROTTLED}${throttleSeconds}s` + : STOMP_TOPICS.SHIP; + + console.log(`[STOMP] Subscribing to ${topic}`); + + return signalStompClient.subscribe(topic, (message) => { + try { + const body = message.body; + const lines = body.split('\n').filter(line => line.trim()); + + const ships = lines.map(line => { + const row = parsePipeMessage(line); + return rowToShipObject(row); + }); + + onMessage(ships); + } catch (error) { + console.error('[STOMP] Ship message parse error:', error); + } + }); +} + +/** + * 선박 삭제 토픽 구독 + * @param {Function} onDelete - 삭제 메시지 수신 콜백 (featureId) + * @returns {Object} 구독 객체 + */ +export function subscribeShipDelete(onDelete) { + console.log(`[STOMP] Subscribing to ${STOMP_TOPICS.SHIP_DELETE}`); + + return signalStompClient.subscribe(STOMP_TOPICS.SHIP_DELETE, (message) => { + try { + const body = message.body; + const [signalSourceCode, targetId] = body.split('|'); + + if (signalSourceCode && targetId) { + const featureId = signalSourceCode + targetId; + onDelete(featureId); + } + } catch (error) { + console.error('[STOMP] Ship delete parse error:', error); + } + }); +} + +/** + * 선박 카운트 토픽 구독 + * @param {Function} onCount - 카운트 메시지 수신 콜백 + * @returns {Object} 구독 객체 + */ +export function subscribeShipCount(onCount) { + return signalStompClient.subscribe(STOMP_TOPICS.COUNT, (message) => { + try { + const counts = JSON.parse(message.body); + onCount(counts); + } catch (error) { + console.error('[STOMP] Count message parse error:', error); + } + }); +} diff --git a/src/component/wrap/ToolComponent.jsx b/src/component/wrap/ToolComponent.jsx index 99ac99cf..845902c8 100644 --- a/src/component/wrap/ToolComponent.jsx +++ b/src/component/wrap/ToolComponent.jsx @@ -1,14 +1,27 @@ import { useState } from "react" -export default function ToolComponent() { +import useShipStore from '../../stores/shipStore'; + +export default function ToolComponent() { const [isLegendOpen, setIsLegendOpen] = useState(false); - + const { isIntegrate, toggleIntegrate } = useShipStore(); + return(
{/* 툴바 */}
  • -
  • +
  • + +
    diff --git a/src/component/wrap/side/DisplayComponent.jsx b/src/component/wrap/side/DisplayComponent.jsx index ac8502e9..b016adca 100644 --- a/src/component/wrap/side/DisplayComponent.jsx +++ b/src/component/wrap/side/DisplayComponent.jsx @@ -1,24 +1,129 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { Link, useNavigate } from "react-router-dom"; import Slider from '../../common/Slider'; +import useShipStore from '../../../stores/shipStore'; +import { + SIGNAL_SOURCE_CODE_AIS, + SIGNAL_SOURCE_CODE_ENAV, + SIGNAL_SOURCE_CODE_VPASS, + SIGNAL_SOURCE_CODE_VTS_AIS, + SIGNAL_SOURCE_CODE_D_MF_HF, + SIGNAL_SOURCE_CODE_RADAR, + SIGNAL_KIND_CODE_FISHING, + SIGNAL_KIND_CODE_PASSENGER, + SIGNAL_KIND_CODE_CARGO, + SIGNAL_KIND_CODE_TANKER, + SIGNAL_KIND_CODE_GOV, + SIGNAL_KIND_CODE_KCGV, + SIGNAL_KIND_CODE_NORMAL, + SIGNAL_KIND_CODE_BUOY, + NATIONAL_CODE_KR, + NATIONAL_CODE_CN, + NATIONAL_CODE_JP, + NATIONAL_CODE_KP, + NATIONAL_CODE_OTHER, +} from '../../../types/constants'; + +// 신호원 필터 매핑 +const SIGNAL_FILTERS = [ + { code: SIGNAL_SOURCE_CODE_AIS, label: 'AIS' }, + { code: SIGNAL_SOURCE_CODE_ENAV, label: 'E-NAV' }, + { code: SIGNAL_SOURCE_CODE_VPASS, label: 'V-PASS' }, + { code: SIGNAL_SOURCE_CODE_VTS_AIS, label: 'VTS_AIS' }, + { code: SIGNAL_SOURCE_CODE_D_MF_HF, label: 'D_MF_HF' }, + { code: SIGNAL_SOURCE_CODE_RADAR, label: 'VTS_RADAR' }, +]; + +// 선종 필터 매핑 +const KIND_FILTERS = [ + { code: SIGNAL_KIND_CODE_FISHING, label: '어선' }, + { code: SIGNAL_KIND_CODE_PASSENGER, label: '여객선' }, + { code: SIGNAL_KIND_CODE_CARGO, label: '화물선' }, + { code: SIGNAL_KIND_CODE_TANKER, label: '유조선' }, + { code: SIGNAL_KIND_CODE_GOV, label: '관공선' }, + { code: SIGNAL_KIND_CODE_KCGV, label: '함정' }, + { code: SIGNAL_KIND_CODE_NORMAL, label: '기타' }, + { code: SIGNAL_KIND_CODE_BUOY, label: '어망/부이' }, +]; + +// 국적 필터 매핑 +const NATIONAL_FILTERS = [ + { code: NATIONAL_CODE_KR, label: '한국' }, + { code: NATIONAL_CODE_CN, label: '중국' }, + { code: NATIONAL_CODE_JP, label: '일본' }, + { code: NATIONAL_CODE_KP, label: '북한' }, + { code: NATIONAL_CODE_OTHER, label: '기타' }, +]; export default function DisplayComponent({ isOpen, onToggle }) { const navigate = useNavigate(); + // 스토어 연결 + const { + sourceVisibility, + kindVisibility, + nationalVisibility, + darkSignalVisible, + darkSignalCount, + toggleSourceVisibility, + toggleKindVisibility, + toggleNationalVisibility, + toggleDarkSignalVisible, + clearDarkSignals, + } = useShipStore(); + // 투명도 const [opacity, setOpacity] = useState(70); // 아코디언 - const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); // 기존 - const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); // - const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); // - const [isAccordionOpen4, setIsAccordionOpen4] = useState(false); // + const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); // 신호 + const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); // 선종 + const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); // 국적 + const [isAccordionOpen4, setIsAccordionOpen4] = useState(false); // AI 모드 const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev); const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev); const toggleAccordion3 = () => setIsAccordionOpen3(prev => !prev); const toggleAccordion4 = () => setIsAccordionOpen4(prev => !prev); + // 신호 전체 On/Off + const isAllSignalsOn = SIGNAL_FILTERS.every(f => sourceVisibility[f.code]); + const toggleAllSignals = useCallback(() => { + SIGNAL_FILTERS.forEach(f => { + if (isAllSignalsOn) { + // 모두 켜져있으면 모두 끄기 + if (sourceVisibility[f.code]) toggleSourceVisibility(f.code); + } else { + // 하나라도 꺼져있으면 모두 켜기 + if (!sourceVisibility[f.code]) toggleSourceVisibility(f.code); + } + }); + }, [isAllSignalsOn, sourceVisibility, toggleSourceVisibility]); + + // 선종 전체 On/Off + const isAllKindsOn = KIND_FILTERS.every(f => kindVisibility[f.code]); + const toggleAllKinds = useCallback(() => { + KIND_FILTERS.forEach(f => { + if (isAllKindsOn) { + if (kindVisibility[f.code]) toggleKindVisibility(f.code); + } else { + if (!kindVisibility[f.code]) toggleKindVisibility(f.code); + } + }); + }, [isAllKindsOn, kindVisibility, toggleKindVisibility]); + + // 국적 전체 On/Off + const isAllNationalsOn = NATIONAL_FILTERS.every(f => nationalVisibility[f.code]); + const toggleAllNationals = useCallback(() => { + NATIONAL_FILTERS.forEach(f => { + if (isAllNationalsOn) { + if (nationalVisibility[f.code]) toggleNationalVisibility(f.code); + } else { + if (!nationalVisibility[f.code]) toggleNationalVisibility(f.code); + } + }); + }, [isAllNationalsOn, nationalVisibility, toggleNationalVisibility]); + // 탭이동 const [activeTab, setActiveTab] = useState('filter'); @@ -50,140 +155,134 @@ export default function DisplayComponent({ isOpen, onToggle }) {
    - {/* 스위치그룹 01 */} + {/* 스위치그룹 01 - 신호 */}
    신호 - +
    + >
    {/* 여기서부터 토글 */}
    -
      -
    • - AIS - -
    • -
    • - V-PASS - -
    • -
    • - VTS_AIS - -
    • -
    • - D_MF_HF - -
    • -
    • - VTS_RADAR - -
    • -
    +
      + {SIGNAL_FILTERS.map(({ code, label }) => ( +
    • + {label} + +
    • + ))} +
    {/* 여기까지 */}
    - {/* 스위치그룹 02 */} + {/* 스위치그룹 02 - 선종 */}
    선종/기종 - +
    + >
    {/* 여기서부터 토글 */}
    -
      -
    • - 어선 - -
    • -
    • - 여객선 - -
    • -
    • - 화물선 - -
    • -
    • - 유조선 - -
    • -
    • - 관공선 - -
    • -
    • - 함정 - -
    • -
    • - 항공기 - -
    • -
    • - 기타 - -
    • -
    +
      + {KIND_FILTERS.map(({ code, label }) => ( +
    • + {label} + +
    • + ))} +
    {/* 여기까지 */}
    - {/* 스위치그룹 03 */} + {/* 스위치그룹 03 - 국적 */}
    국적 - +
    + >
    {/* 여기서부터 토글 */}
    -
      -
    • - 한국 - -
    • -
    • - 중국 - -
    • -
    • - 일본 - -
    • -
    • - 북한 - -
    • -
    • - 기타 - -
    • -
    +
      + {NATIONAL_FILTERS.map(({ code, label }) => ( +
    • + {label} + +
    • + ))} +
    {/* 여기까지 */}
    @@ -233,13 +332,32 @@ export default function DisplayComponent({ isOpen, onToggle }) { {/* 여기까지 */}
    - {/* 스위치그룹 05 */} + {/* 스위치그룹 05 - 다크시그널 */}
    다크시그널 + {darkSignalCount > 0 && ({darkSignalCount})} + {darkSignalCount > 0 && ( + + )}
    - +
    diff --git a/src/components/layout/Header.jsx b/src/components/layout/Header.jsx new file mode 100644 index 00000000..fb9254e0 --- /dev/null +++ b/src/components/layout/Header.jsx @@ -0,0 +1,42 @@ +import { Link } from 'react-router-dom'; + +/** + * 헤더 컴포넌트 + * - 로고, 알람, 설정(드롭다운), 마이페이지 + */ +export default function Header() { + return ( + + ); +} diff --git a/src/components/layout/MainLayout.jsx b/src/components/layout/MainLayout.jsx new file mode 100644 index 00000000..68bbb06d --- /dev/null +++ b/src/components/layout/MainLayout.jsx @@ -0,0 +1,24 @@ +import Header from './Header'; +import Sidebar from './Sidebar'; +import MapContainer from '../../map/MapContainer'; +import ToolBar from './ToolBar'; + +/** + * 메인 레이아웃 + * - Header: 상단 헤더 + * - Sidebar: 좌측 사이드바 (네비게이션 + 패널) + * - MapContainer: 중앙 지도 영역 + * - ToolBar: 우측 툴바 + */ +export default function MainLayout() { + return ( +
    +
    + +
    + +
    + +
    + ); +} diff --git a/src/components/layout/SideNav.jsx b/src/components/layout/SideNav.jsx new file mode 100644 index 00000000..32f49d15 --- /dev/null +++ b/src/components/layout/SideNav.jsx @@ -0,0 +1,76 @@ +/** + * 사이드 네비게이션 메뉴 + * - 퍼블리시 NavComponent 구조와 동일하게 맞춤 + */ + +const gnbList = [ + { key: 'gnb1', className: 'gnb1', label: '선박', path: 'ship' }, + { key: 'gnb2', className: 'gnb2', label: '위성', path: 'satellite' }, + { key: 'gnb3', className: 'gnb3', label: '기상', path: 'weather' }, + { key: 'gnb4', className: 'gnb4', label: '분석', path: 'analysis' }, + { key: 'gnb5', className: 'gnb5', label: '타임라인', path: 'timeline' }, + { key: 'gnb6', className: 'gnb6', label: 'AI모드', path: 'ai' }, + { key: 'gnb7', className: 'gnb7', label: '리플레이', path: 'replay' }, + { key: 'gnb8', className: 'gnb8', label: '항적조회', path: 'tracking' }, +]; + +const sideList = [ + { key: 'filter', className: 'filter', label: '필터', path: 'filter' }, + { key: 'layer', className: 'layer', label: '레이어', path: 'layer' }, +]; + +export default function SideNav({ activeKey, onChange }) { + return ( + + ); +} + +// 키-경로 매핑 export (Sidebar에서 사용) +export const keyToPath = { + gnb1: 'ship', + gnb2: 'satellite', + gnb3: 'weather', + gnb4: 'analysis', + gnb5: 'timeline', + gnb6: 'ai', + gnb7: 'replay', + gnb8: 'tracking', + filter: 'filter', + layer: 'layer', +}; + +export const pathToKey = Object.fromEntries( + Object.entries(keyToPath).map(([k, v]) => [v, k]) +); diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx new file mode 100644 index 00000000..e10dfffe --- /dev/null +++ b/src/components/layout/Sidebar.jsx @@ -0,0 +1,86 @@ +import { useState } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import SideNav, { keyToPath, pathToKey } from './SideNav'; + +// 퍼블리시 패널 컴포넌트 재사용 +import Panel1Component from '../../publish/pages/Panel1Component'; +import Panel2Component from '../../publish/pages/Panel2Component'; +import Panel3Component from '../../publish/pages/Panel3Component'; +import Panel4Component from '../../publish/pages/Panel4Component'; +import Panel5Component from '../../publish/pages/Panel5Component'; +import Panel6Component from '../../publish/pages/Panel6Component'; +import Panel7Component from '../../publish/pages/Panel7Component'; +import Panel8Component from '../../publish/pages/Panel8Component'; +// DisplayComponent는 스토어 연결된 버전 사용 +import DisplayComponent from '../../component/wrap/side/DisplayComponent'; + +/** + * 사이드바 컴포넌트 + * - 네비게이션 메뉴 + * - 패널 영역 (경로에 따라 다른 패널 표시) + */ +export default function Sidebar() { + const navigate = useNavigate(); + const location = useLocation(); + const [isPanelOpen, setIsPanelOpen] = useState(true); + + // URL에서 활성 메뉴 키 추출 + const getActiveKey = () => { + const path = location.pathname.split('/')[1] || 'ship'; + return pathToKey[path] || 'gnb1'; + }; + + const activeKey = getActiveKey(); + + const handleMenuChange = (key) => { + setIsPanelOpen(true); + const path = keyToPath[key] || 'ship'; + navigate(`/${path}`); + }; + + const handleTogglePanel = () => { + setIsPanelOpen((prev) => !prev); + }; + + // 패널 공통 props + const panelProps = { + isOpen: isPanelOpen, + onToggle: handleTogglePanel, + }; + + // 활성 키에 따른 패널 컴포넌트 렌더링 + const renderPanel = () => { + switch (activeKey) { + case 'gnb1': + return ; + case 'gnb2': + return ; + case 'gnb3': + return ; + case 'gnb4': + return ; + case 'gnb5': + return ; + case 'gnb6': + return ; + case 'gnb7': + return ; + case 'gnb8': + return ; + case 'filter': + case 'layer': + return ; + default: + return ; + } + }; + + return ( +
    + +
    + {renderPanel()} +
    +
    + ); +} diff --git a/src/components/layout/ToolBar.jsx b/src/components/layout/ToolBar.jsx new file mode 100644 index 00000000..ff1e74a2 --- /dev/null +++ b/src/components/layout/ToolBar.jsx @@ -0,0 +1,201 @@ +import { useState, useCallback } from 'react'; +import { useMapStore } from '../../stores/mapStore'; +import useShipStore from '../../stores/shipStore'; +import { downloadShipCsv } from '../../utils/csvDownload'; + +// 면적 도형 옵션 +const AREA_SHAPES = [ + { key: 'Box', label: '사각형' }, + { key: 'Polygon', label: '다각형' }, + { key: 'Circle', label: '원형' }, +]; + +/** + * 우측 툴바 컴포넌트 + * - 지도 도구 (초기화, 거리, 면적 등) + * - 선명표시 (호버 패널) + * - 줌 컨트롤 + * - 범례, 미니맵 + */ +export default function ToolBar() { + const [isLabelPanelOpen, setIsLabelPanelOpen] = useState(false); + const [isAreaPanelOpen, setIsAreaPanelOpen] = useState(false); + const { zoom, zoomIn, zoomOut, activeMeasureTool, setMeasureTool, setAreaShape } = useMapStore(); + const { + isIntegrate, + toggleIntegrate, + showLabels, + toggleShowLabels, + labelOptions, + toggleLabelOption, + showLegend, + toggleShowLegend, + } = useShipStore(); + + // 선명표시 옵션 목록 + const labelOptionList = [ + { key: 'showShipName', label: '선박명' }, + { key: 'showSpeedVector', label: '속도벡터' }, + { key: 'showShipSize', label: '선박크기' }, + { key: 'showSignalStatus', label: '신호상태' }, + ]; + + return ( +
    + {/* 툴바 */} +
    +
      +
    • +
    • + +
    • + {/* 선명표시 버튼 + 호버 패널 영역 */} +
    • setIsLabelPanelOpen(true)} + onMouseLeave={() => setIsLabelPanelOpen(false)} + > + + {/* 선명표시 호버 패널 */} + {isLabelPanelOpen && ( +
      +
      +
      + 선명표시 옵션 +
      +
        + {labelOptionList.map(({ key, label }) => ( +
      • + {label} + +
      • + ))} +
      +
      +
      + )} +
    • +
    • +
    +
      +
    • + +
    • +
    • activeMeasureTool === 'area' && setIsAreaPanelOpen(true)} + onMouseLeave={() => setIsAreaPanelOpen(false)} + > + + {activeMeasureTool === 'area' && isAreaPanelOpen && ( +
      +
      +
      도형 선택
      +
        + {AREA_SHAPES.map(({ key, label }) => ( +
      • { + setAreaShape(key); + setIsAreaPanelOpen(false); + }} + > + {label} +
      • + ))} +
      +
      +
      + )} +
    • +
    • + +
    • +
    +
      +
    • +
    • + +
    • +
    +
    + + {/* 맵컨트롤 툴바 */} +
    +
      +
    • + +
    • +
    • {zoom}
    • +
    • + +
    • +
    +
      +
    • + +
    • +
    • +
    +
    +
    + ); +} diff --git a/src/components/ship/ShipContextMenu.jsx b/src/components/ship/ShipContextMenu.jsx new file mode 100644 index 00000000..71ad5cd1 --- /dev/null +++ b/src/components/ship/ShipContextMenu.jsx @@ -0,0 +1,82 @@ +/** + * 선박 우클릭 컨텍스트 메뉴 + * - 단일 선박 우클릭: 해당 선박 메뉴 + * - Ctrl+Drag 선택 후 우클릭: 선택된 선박 전체 메뉴 + */ +import { useEffect, useRef, useCallback } from 'react'; +import useShipStore from '../../stores/shipStore'; +import './ShipContextMenu.scss'; + +const MENU_ITEMS = [ + { key: 'track', label: '항적조회' }, + { key: 'analysis', label: '항적분석' }, + { key: 'detail', label: '상세정보' }, +]; + +export default function ShipContextMenu() { + const contextMenu = useShipStore((s) => s.contextMenu); + const closeContextMenu = useShipStore((s) => s.closeContextMenu); + const menuRef = useRef(null); + + // 외부 클릭 시 닫기 + useEffect(() => { + if (!contextMenu) return; + + const handleClick = (e) => { + if (menuRef.current && !menuRef.current.contains(e.target)) { + closeContextMenu(); + } + }; + + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [contextMenu, closeContextMenu]); + + // 메뉴 항목 클릭 + const handleAction = useCallback((key) => { + if (!contextMenu) return; + const { ships } = contextMenu; + + // TODO: 향후 API 연결 + console.log(`[ContextMenu] action=${key}, ships=`, ships.map((s) => ({ + featureId: s.featureId, + shipName: s.shipName, + targetId: s.targetId, + }))); + + closeContextMenu(); + }, [contextMenu, closeContextMenu]); + + if (!contextMenu) return null; + + const { x, y, ships } = contextMenu; + + // 화면 밖 넘침 방지 + const menuWidth = 160; + const menuHeight = MENU_ITEMS.length * 36 + 40; // 항목 + 헤더 + const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x; + const adjustedY = y + menuHeight > window.innerHeight ? y - menuHeight : y; + + const title = ships.length === 1 + ? (ships[0].shipName || ships[0].featureId) + : `${ships.length}척 선택`; + + return ( +
    +
    {title}
    + {MENU_ITEMS.map((item) => ( +
    handleAction(item.key)} + > + {item.label} +
    + ))} +
    + ); +} diff --git a/src/components/ship/ShipContextMenu.scss b/src/components/ship/ShipContextMenu.scss new file mode 100644 index 00000000..cd648cab --- /dev/null +++ b/src/components/ship/ShipContextMenu.scss @@ -0,0 +1,34 @@ +.ship-context-menu { + position: fixed; + z-index: 1000; + background: #1a1a2e; + border: 1px solid #3a3a5e; + border-radius: 4px; + min-width: 140px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); + user-select: none; + + &__header { + padding: 6px 12px; + font-size: 12px; + color: #aaa; + border-bottom: 1px solid #3a3a5e; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 200px; + } + + &__item { + padding: 8px 12px; + font-size: 13px; + color: #ddd; + cursor: pointer; + transition: background-color 0.15s; + + &:hover { + background: #2a2a4e; + color: #fff; + } + } +} diff --git a/src/components/ship/ShipDetailModal.jsx b/src/components/ship/ShipDetailModal.jsx new file mode 100644 index 00000000..8b9b6ffd --- /dev/null +++ b/src/components/ship/ShipDetailModal.jsx @@ -0,0 +1,325 @@ +/** + * 선박 상세 모달 컴포넌트 (다중 모달 지원, 최대 3개) + * 퍼블리시 ShipComponent.jsx의 popupMap shipInfo 구조 활용 + * 참조: mda-react-front/src/components/popup/ShipDetailModal.tsx + * + * - 헤더 드래그로 위치 이동 가능 + * - 선박 사진 갤러리 (없으면 기본 이미지) + * - 새 모달은 직전 모달의 현재 위치(드래그 반영) 기준 우측 140px 오프셋으로 생성 + */ +import { useRef, useState, useCallback, useEffect } from 'react'; +import useShipStore from '../../stores/shipStore'; +import { + SHIP_KIND_LABELS, + SIGNAL_FLAG_CONFIGS, + SPEED_THRESHOLD, + SIGNAL_KIND_CODE_FISHING, + SIGNAL_KIND_CODE_KCGV, + SIGNAL_KIND_CODE_PASSENGER, + SIGNAL_KIND_CODE_CARGO, + SIGNAL_KIND_CODE_TANKER, + SIGNAL_KIND_CODE_GOV, +} from '../../types/constants'; +import defaultShipImg from '../../assets/img/default-ship.png'; +import fishingIcon from '../../assets/img/shipDetail/detailKindIcon/fishing.svg'; +import kcgvIcon from '../../assets/img/shipDetail/detailKindIcon/kcgv.svg'; +import passengerIcon from '../../assets/img/shipDetail/detailKindIcon/passenger.svg'; +import cargoIcon from '../../assets/img/shipDetail/detailKindIcon/cargo.svg'; +import tankerIcon from '../../assets/img/shipDetail/detailKindIcon/tanker.svg'; +import govIcon from '../../assets/img/shipDetail/detailKindIcon/gov.svg'; +import etcIcon from '../../assets/img/shipDetail/detailKindIcon/etc.svg'; +import './ShipDetailModal.scss'; + +/** 선종코드 → 아이콘 매핑 */ +const SHIP_KIND_ICONS = { + [SIGNAL_KIND_CODE_FISHING]: fishingIcon, + [SIGNAL_KIND_CODE_KCGV]: kcgvIcon, + [SIGNAL_KIND_CODE_PASSENGER]: passengerIcon, + [SIGNAL_KIND_CODE_CARGO]: cargoIcon, + [SIGNAL_KIND_CODE_TANKER]: tankerIcon, + [SIGNAL_KIND_CODE_GOV]: govIcon, +}; + +/** 선종 아이콘 URL 반환 */ +function getShipKindIcon(signalKindCode) { + return SHIP_KIND_ICONS[signalKindCode] || etcIcon; +} + +/** + * 국기 아이콘 URL 반환 (서버 API) + * 참조: mda-react-front/src/services/filterCheck.ts - filterNationFlag() + * @param {string} nationalCode - MID 숫자코드 (예: '440', '412') + * @returns {string} 국기 이미지 URL + */ +function getNationalFlagUrl(nationalCode) { + if (!nationalCode) return null; + const baseUrl = import.meta.env.VITE_API_URL || ''; + return `${baseUrl}/ship/image/small/${nationalCode}.svg`; +} + +/** + * receivedTime 문자열을 YYYY-MM-DD HH:mm:ss 형식으로 변환 + * 입력 예: '20241123112300' 또는 '2024-11-23 11:23:00' 또는 '2024-11-23T11:23:00' + * @param {string} raw + * @returns {string} + */ +function formatDateTime(raw) { + if (!raw) return '-'; + + // 이미 YYYY-MM-DD HH:mm:ss 형태면 그대로 반환 + if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)) { + return raw; + } + + // 숫자만 추출 (구분자 제거) + const digits = raw.replace(/\D/g, ''); + + if (digits.length >= 14) { + const y = digits.slice(0, 4); + const M = digits.slice(4, 6); + const d = digits.slice(6, 8); + const h = digits.slice(8, 10); + const m = digits.slice(10, 12); + const s = digits.slice(12, 14); + return `${y}-${M}-${d} ${h}:${m}:${s}`; + } + + // 파싱 불가하면 원본 반환 + return raw; +} + +/** + * AVETDR 신호 플래그 표시 + */ +function SignalFlags({ ship }) { + const isIntegrate = useShipStore((s) => s.isIntegrate); + const isIntegratedShip = ship.targetId && ship.targetId.includes('_'); + const useIntegratedMode = isIntegrate && isIntegratedShip; + + return ( +
      + {SIGNAL_FLAG_CONFIGS.map((config) => { + let isActive = false; + let isVisible = false; + + if (useIntegratedMode) { + const val = ship[config.dataKey]; + if (val === '1') { isVisible = true; isActive = true; } + else if (val === '0') { isVisible = true; } + } else { + if (config.signalSourceCode === ship.signalSourceCode) { + isVisible = true; + isActive = true; + } + } + + if (!isVisible) return null; + + return ( +
    • + {config.key} +
    • + ); + })} +
    + ); +} + +/** + * 선박 사진 갤러리 + * 이미지가 없으면 기본 이미지(default-ship.png) 표시 + */ +function ShipGallery({ imageUrlList }) { + const [currentIndex, setCurrentIndex] = useState(0); + const hasImages = imageUrlList && imageUrlList.length > 0; + const images = hasImages ? imageUrlList : [defaultShipImg]; + const total = images.length; + const canSlide = total > 1; + + const handlePrev = useCallback(() => { + setCurrentIndex((prev) => (prev === 0 ? total - 1 : prev - 1)); + }, [total]); + + const handleNext = useCallback(() => { + setCurrentIndex((prev) => (prev === total - 1 ? 0 : prev + 1)); + }, [total]); + + const handleIndicatorClick = useCallback((index) => { + setCurrentIndex(index); + }, []); + + return ( +
    + {canSlide && ( + <> + + + + )} +
    + 선박 이미지 { e.target.src = defaultShipImg; }} + /> +
    + {canSlide && ( +
    + {images.map((_, i) => ( +
    + )} +
    + ); +} + +/** + * 단일 선박 상세 모달 + * @param {Object} props.modal - { ship, id, initialPos } + */ +export default function ShipDetailModal({ modal }) { + const closeDetailModal = useShipStore((s) => s.closeDetailModal); + const updateModalPos = useShipStore((s) => s.updateModalPos); + + // 드래그 상태 - 초기 위치는 스토어에서 계산된 initialPos 사용 + const [position, setPosition] = useState(() => ({ ...modal.initialPos })); + const posRef = useRef(modal.initialPos); + const dragging = useRef(false); + const dragStart = useRef({ x: 0, y: 0 }); + + // 드래그 핸들러 + const handleMouseDown = useCallback((e) => { + dragging.current = true; + dragStart.current = { + x: e.clientX - position.x, + y: e.clientY - position.y, + }; + e.preventDefault(); + }, [position]); + + useEffect(() => { + const handleMouseMove = (e) => { + if (!dragging.current) return; + const newPos = { + x: e.clientX - dragStart.current.x, + y: e.clientY - dragStart.current.y, + }; + posRef.current = newPos; + setPosition(newPos); + }; + + const handleMouseUp = () => { + if (dragging.current) { + dragging.current = false; + // 드래그 종료 시 스토어에 현재 위치 보고 (ref에서 읽어서 render 중 setState 회피) + updateModalPos(modal.id, posRef.current); + } + }; + + window.addEventListener('mousemove', handleMouseMove); + window.addEventListener('mouseup', handleMouseUp); + return () => { + window.removeEventListener('mousemove', handleMouseMove); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, [modal.id, updateModalPos]); + + const { ship, id } = modal; + const kindLabel = SHIP_KIND_LABELS[ship.signalKindCode] || '기타'; + const sog = Number(ship.sog) || 0; + const cog = Number(ship.cog) || 0; + const isMoving = sog > SPEED_THRESHOLD; + const draught = ship.draught ? `${(Number(ship.draught) / 10).toFixed(1)}m` : '-'; + const formattedTime = formatDateTime(ship.receivedTime); + + return ( +
    + {/* header - 드래그 핸들 */} +
    +
    + + {kindLabel} + + {ship.nationalCode && ( + + 국기 { e.target.style.display = 'none'; }} + /> + + )} + {ship.shipName || '-'} + {ship.originalTargetId || '-'} +
    +
    + + {/* gallery */} + + + {/* body */} +
    +
    +
    + + +
    +
    + +
      +
    • +
      + 선박상태 + {isMoving ? '항해' : '정박'} +
      +
      + 속도/항로 + {sog.toFixed(1)} kn / {cog.toFixed(1)}° +
      +
      + 흘수 + {draught} +
      +
    • +
    + +
    + + +
    +
    + + {/* footer */} +
    데이터 수신시간 : {formattedTime}
    +
    + ); +} diff --git a/src/components/ship/ShipDetailModal.scss b/src/components/ship/ShipDetailModal.scss new file mode 100644 index 00000000..a2de7bf9 --- /dev/null +++ b/src/components/ship/ShipDetailModal.scss @@ -0,0 +1,203 @@ +/** + * 선박 상세 모달 스타일 + * public/css/common.css의 .popupMap 스타일을 재사용하면서 추가 스타일 정의 + */ +.ship-detail-modal { + // common.css의 .popupMap 기본 스타일 오버라이드 + // transform은 인라인으로 none 지정 (드래그 위치 직접 제어) + + // common.css의 .popupMap > .pmHeader > .rowL 보다 높은 specificity 필요 + &.popupMap > .pmHeader { + cursor: grab; + user-select: none; + + &:active { + cursor: grabbing; + } + + > .rowL { + min-width: 0; + flex: 1; + overflow: hidden; + } + + > .pmClose { + flex-shrink: 0; + } + + .shipTypeIcon { + display: inline-flex; + align-items: center; + flex-shrink: 0; + + img { + width: 2.8rem; + height: 2.8rem; + } + } + + .countryFlag { + display: inline-flex; + align-items: center; + flex-shrink: 0; + + img { + width: 2.4rem; + height: 1.7rem; + object-fit: cover; + border-radius: 2px; + } + } + + .shipName { + flex-shrink: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .shipNum { + flex-shrink: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .shipTypeIco { + display: flex; + gap: 2px; + list-style: none; + padding: 0; + margin: 0; + + li { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + border-radius: 3px; + font-size: 11px; + font-weight: 700; + color: #fff; + + &.inactive { + opacity: 0.5; + } + } + } + + // 상세정보 버튼과 신호 아이콘 사이 간격 + .shipAction .rowL { + gap: 8px; + } + + // 속도/항로 영역: 중앙 컬럼 넓히고 줄바꿈 방지 + .shipStatus li.status { + grid-template-columns: 5.5rem 1fr 5.5rem; + + .statusItem { + white-space: nowrap; + } + } + + // 갤러리: 이미지 없을 때도 빈 프레임 유지 + .pmGallery { + position: relative; + overflow: hidden; + width: 29rem; + height: 10.6rem; + background-color: var(--gray-scale2, #2a2d35); + + .galleryView { + width: 100%; + height: 100%; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + } + + // 좌우 슬라이드 버튼 + .navBtn { + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: 2; + width: 24px; + height: 40px; + border: none; + background: rgba(0, 0, 0, 0.4); + color: #fff; + font-size: 14px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.2s; + padding: 0; + + &:hover { + background: rgba(0, 0, 0, 0.7); + } + + &.prev { + left: 0; + border-radius: 0 3px 3px 0; + + &::after { + content: '‹'; + font-size: 18px; + line-height: 1; + } + } + + &.next { + right: 0; + border-radius: 3px 0 0 3px; + + &::after { + content: '›'; + font-size: 18px; + line-height: 1; + } + } + } + + // 하단 인디케이터 (클릭으로 바로가기) + .galleryIndicators { + position: absolute; + bottom: 6px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 5px; + z-index: 2; + + .indicator { + width: 8px; + height: 8px; + border-radius: 50%; + border: none; + padding: 0; + background: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: background 0.2s, transform 0.2s; + + &:hover { + background: rgba(255, 255, 255, 0.7); + } + + &.active { + background: #fff; + transform: scale(1.2); + } + } + } + } +} diff --git a/src/components/ship/ShipLegend.jsx b/src/components/ship/ShipLegend.jsx new file mode 100644 index 00000000..5601f92b --- /dev/null +++ b/src/components/ship/ShipLegend.jsx @@ -0,0 +1,139 @@ +/** + * 선박 범례 컴포넌트 + * - 선박 종류별 아이콘 및 카운트 표시 + * - 선박 표시 On/Off 토글 + * - 선박 종류별 필터 토글 + */ +import { memo } from 'react'; +import useShipStore from '../../stores/shipStore'; +import { + SIGNAL_KIND_CODE_FISHING, + SIGNAL_KIND_CODE_KCGV, + SIGNAL_KIND_CODE_PASSENGER, + SIGNAL_KIND_CODE_CARGO, + SIGNAL_KIND_CODE_TANKER, + SIGNAL_KIND_CODE_GOV, + SIGNAL_KIND_CODE_NORMAL, + SIGNAL_KIND_CODE_BUOY, +} from '../../types/constants'; +import './ShipLegend.scss'; + +// 선박 종류별 SVG 아이콘 +import fishingIcon from '../../assets/img/shipKindIcons/fishing.svg'; +import passIcon from '../../assets/img/shipKindIcons/pass.svg'; +import cargoIcon from '../../assets/img/shipKindIcons/cargo.svg'; +import hazardIcon from '../../assets/img/shipKindIcons/hazard.svg'; +import govIcon from '../../assets/img/shipKindIcons/gov.svg'; +import kcgvIcon from '../../assets/img/shipKindIcons/kcgv.svg'; +import bouyIcon from '../../assets/img/shipKindIcons/bouy.svg'; +import etcIcon from '../../assets/img/shipKindIcons/etc.svg'; + +/** + * 선박 종류 코드 → 아이콘 매핑 + */ +const SHIP_KIND_ICONS = { + [SIGNAL_KIND_CODE_FISHING]: fishingIcon, + [SIGNAL_KIND_CODE_KCGV]: kcgvIcon, + [SIGNAL_KIND_CODE_PASSENGER]: passIcon, + [SIGNAL_KIND_CODE_CARGO]: cargoIcon, + [SIGNAL_KIND_CODE_TANKER]: hazardIcon, + [SIGNAL_KIND_CODE_GOV]: govIcon, + [SIGNAL_KIND_CODE_NORMAL]: etcIcon, + [SIGNAL_KIND_CODE_BUOY]: bouyIcon, +}; + +/** + * 범례 항목 설정 + */ +const LEGEND_ITEMS = [ + { code: SIGNAL_KIND_CODE_FISHING, label: '어선' }, + { code: SIGNAL_KIND_CODE_PASSENGER, label: '여객선' }, + { code: SIGNAL_KIND_CODE_CARGO, label: '화물선' }, + { code: SIGNAL_KIND_CODE_TANKER, label: '유조선' }, + { code: SIGNAL_KIND_CODE_GOV, label: '관공선' }, + { code: SIGNAL_KIND_CODE_KCGV, label: '경비함정' }, + { code: SIGNAL_KIND_CODE_BUOY, label: '어망/부이' }, + { code: SIGNAL_KIND_CODE_NORMAL, label: '기타' }, +]; + +/** + * 선박 범례 항목 + */ +const LegendItem = memo(({ code, label, count, icon, isVisible, onToggle }) => { + // 부이는 회전하지 않음 + const isBuoy = code === SIGNAL_KIND_CODE_BUOY; + + return ( +
  • +
    onToggle(code)}> + + {label} + + {label} +
    + {count} +
  • + ); +}); + +/** + * 선박 범례 컴포넌트 + */ +const ShipLegend = memo(() => { + const { + kindCounts, + kindVisibility, + isShipVisible, + totalCount, + isConnected, + toggleKindVisibility, + toggleShipVisible, + } = useShipStore(); + + return ( +
    + {/* 헤더 - 전체 On/Off */} +
    +
    + + 선박 현황 +
    + +
    + + {/* 선박 종류별 목록 */} +
      + {LEGEND_ITEMS.map((item) => ( + + ))} +
    + + {/* 푸터 - 전체 카운트 */} +
    + 전체 + {totalCount} +
    +
    + ); +}); + +export default ShipLegend; diff --git a/src/components/ship/ShipLegend.scss b/src/components/ship/ShipLegend.scss new file mode 100644 index 00000000..523b10b0 --- /dev/null +++ b/src/components/ship/ShipLegend.scss @@ -0,0 +1,179 @@ +/** + * 선박 범례 스타일 + */ +.ship-legend { + position: absolute; + right: 70px; + bottom: 72px; + z-index: 100; + width: 160px; + background: rgba(30, 35, 45, 0.95); + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + overflow: hidden; + font-size: 12px; + color: #fff; + + // 헤더 + .legend-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: rgba(0, 0, 0, 0.2); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + + .legend-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + + .connection-status { + width: 8px; + height: 8px; + border-radius: 50%; + background: #f44336; + transition: background 0.3s ease; + + &.connected { + background: #4caf50; + } + } + } + + // 토글 스위치 + .toggle-switch { + position: relative; + display: inline-block; + width: 36px; + height: 20px; + cursor: pointer; + + input { + opacity: 0; + width: 0; + height: 0; + + &:checked + .slider { + background-color: #2196f3; + + &:before { + transform: translateX(16px); + } + } + } + + .slider { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #555; + border-radius: 20px; + transition: 0.3s; + + &:before { + content: ''; + position: absolute; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: #fff; + border-radius: 50%; + transition: 0.3s; + } + } + } + } + + // 범례 목록 + .legend-list { + list-style: none; + margin: 0; + padding: 8px 0; + + .legend-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px 12px; + transition: background 0.2s ease; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + &.disabled { + opacity: 0.4; + + .legend-icon img { + filter: grayscale(100%); + } + } + + .legend-info { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + + .legend-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + img { + width: 16px; + height: 16px; + object-fit: contain; + } + } + + .legend-label { + white-space: nowrap; + } + } + + .legend-count { + font-weight: 500; + color: rgba(255, 255, 255, 0.8); + min-width: 30px; + text-align: right; + } + } + } + + // 푸터 + .legend-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 12px; + background: rgba(0, 0, 0, 0.2); + border-top: 1px solid rgba(255, 255, 255, 0.1); + font-weight: 600; + + .total-count { + color: #4fc3f7; + } + } +} + +// 범례 숨김 상태 (선박 표시 Off 시) +.ship-legend.hidden { + .legend-list { + max-height: 0; + padding: 0; + overflow: hidden; + } + + .legend-footer { + display: none; + } +} diff --git a/src/components/ship/ShipTooltip.jsx b/src/components/ship/ShipTooltip.jsx new file mode 100644 index 00000000..f2280b31 --- /dev/null +++ b/src/components/ship/ShipTooltip.jsx @@ -0,0 +1,40 @@ +/** + * 선박 호버 툴팁 컴포넌트 + * 마우스 위치 기준으로 선박 정보 표시 + */ +import { SHIP_KIND_LABELS, SPEED_THRESHOLD } from '../../types/constants'; +import './ShipTooltip.scss'; + +const OFFSET_X = 12; +const OFFSET_Y = -40; + +export default function ShipTooltip({ ship, x, y }) { + if (!ship) return null; + + const kindLabel = SHIP_KIND_LABELS[ship.signalKindCode] || '기타'; + const sog = Number(ship.sog) || 0; + const cog = Number(ship.cog) || 0; + const isMoving = sog > SPEED_THRESHOLD; + + return ( +
    +
    + {kindLabel} + {ship.shipName || ship.targetId || '-'} +
    +
    + {sog.toFixed(1)} kn + | + {cog.toFixed(1)}° + | + {isMoving ? '항해' : '정박'} +
    +
    + ); +} diff --git a/src/components/ship/ShipTooltip.scss b/src/components/ship/ShipTooltip.scss new file mode 100644 index 00000000..e2ea3ed7 --- /dev/null +++ b/src/components/ship/ShipTooltip.scss @@ -0,0 +1,46 @@ +.ship-tooltip { + position: fixed; + z-index: 200; + pointer-events: none; + background: rgba(20, 24, 32, 0.95); + border-radius: 6px; + padding: 8px 12px; + min-width: 140px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4); + color: #fff; + font-size: 12px; + white-space: nowrap; + + &__header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; + } + + &__kind { + display: inline-block; + padding: 1px 5px; + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; + font-size: 10px; + color: #adb5bd; + } + + &__name { + font-weight: 700; + font-size: 13px; + color: #fff; + } + + &__body { + display: flex; + align-items: center; + gap: 4px; + color: #ced4da; + } + + &__sep { + color: rgba(255, 255, 255, 0.25); + } +} diff --git a/src/hooks/useShipData.js b/src/hooks/useShipData.js new file mode 100644 index 00000000..ebe65e0d --- /dev/null +++ b/src/hooks/useShipData.js @@ -0,0 +1,231 @@ +/** + * 선박 데이터 관리 훅 + * - 초기 선박 데이터 API 로드 (/all/12) + * - STOMP WebSocket 연결 및 구독 + * - 선박 데이터 수신 및 스토어 업데이트 + * - 배치 머지 최적화 (1초 or 500건) + * + * 참조: mda-react-front/src/map/MapUpdater.tsx + * 위성통신망 환경 최적화: 최소 트래픽, 최소 스펙 + */ +import { useEffect, useRef, useCallback, useState } from 'react'; +import { + signalStompClient, + connectStomp, + disconnectStomp, + subscribeShips, + subscribeShipDelete, +} from '../common/stompClient'; +import useShipStore from '../stores/shipStore'; +import { fetchAllSignals } from '../api/signalApi'; + +// ===================== +// 배치 머지 설정 +// ===================== +const BATCH_CONFIG = { + maxInterval: 1000, // 최대 대기 시간 (1초) + maxCount: 500, // 최대 버퍼 크기 (500건) +}; + +/** + * 선박 데이터 관리 훅 + * @param {Object} options - 옵션 + * @param {boolean} options.autoConnect - 자동 연결 여부 (기본값: true) + * @returns {Object} { isConnected, isLoading, connect, disconnect } + */ +export default function useShipData(options = {}) { + const { autoConnect = true } = options; + + const subscriptionsRef = useRef([]); + const shipBufferRef = useRef([]); + const batchTimerRef = useRef(null); + const initialLoadDoneRef = useRef(false); + const [isLoading, setIsLoading] = useState(true); + const { mergeFeatures, deleteFeatureById, setConnected, isConnected } = useShipStore(); + + /** + * 버퍼된 선박 데이터 머지 실행 + */ + const flushBuffer = useCallback(() => { + if (shipBufferRef.current.length === 0) return; + + // 버퍼 복사 후 초기화 + const ships = shipBufferRef.current; + shipBufferRef.current = []; + + // 타이머 클리어 + if (batchTimerRef.current) { + clearTimeout(batchTimerRef.current); + batchTimerRef.current = null; + } + + // 머지 실행 + mergeFeatures(ships); + }, [mergeFeatures]); + + /** + * 선박 메시지 수신 핸들러 (배치 처리) + * 조건: 1초 경과 OR 500건 누적 시 머지 + */ + const handleShipMessage = useCallback((ships) => { + // 버퍼에 추가 + shipBufferRef.current.push(...ships); + + // 조건 1: 500건 이상이면 즉시 머지 + if (shipBufferRef.current.length >= BATCH_CONFIG.maxCount) { + flushBuffer(); + return; + } + + // 조건 2: 타이머가 없으면 1초 타이머 설정 + if (!batchTimerRef.current) { + batchTimerRef.current = setTimeout(() => { + flushBuffer(); + }, BATCH_CONFIG.maxInterval); + } + }, [flushBuffer]); + + /** + * 선박 삭제 메시지 수신 핸들러 + * @param {string} featureId - signalSourceCode + targetId + */ + const handleShipDelete = useCallback((featureId) => { + deleteFeatureById(featureId); + }, [deleteFeatureById]); + + /** + * 토픽 구독 시작 + */ + const startSubscriptions = useCallback(() => { + // 기존 구독 해제 + subscriptionsRef.current.forEach((sub) => { + try { + sub.unsubscribe(); + } catch (e) { + // ignore + } + }); + subscriptionsRef.current = []; + + // 선박 토픽 구독 + const shipSub = subscribeShips(handleShipMessage); + subscriptionsRef.current.push(shipSub); + + // 선박 삭제 토픽 구독 + const deleteSub = subscribeShipDelete(handleShipDelete); + subscriptionsRef.current.push(deleteSub); + }, [handleShipMessage, handleShipDelete]); + + /** + * 연결 성공 시 토픽 구독 + */ + const handleConnect = useCallback(() => { + setConnected(true); + startSubscriptions(); + }, [setConnected, startSubscriptions]); + + /** + * 연결 해제 시 + */ + const handleDisconnect = useCallback(() => { + setConnected(false); + }, [setConnected]); + + /** + * 에러 발생 시 + */ + const handleError = useCallback(() => { + setConnected(false); + }, [setConnected]); + + /** + * STOMP 연결 시작 + */ + const connect = useCallback(() => { + connectStomp({ + onConnect: handleConnect, + onDisconnect: handleDisconnect, + onError: handleError, + }); + }, [handleConnect, handleDisconnect, handleError]); + + /** + * STOMP 연결 해제 + */ + const disconnect = useCallback(() => { + // 남은 버퍼 머지 + flushBuffer(); + + // 타이머 클리어 + if (batchTimerRef.current) { + clearTimeout(batchTimerRef.current); + batchTimerRef.current = null; + } + + // 구독 해제 + subscriptionsRef.current.forEach((sub) => { + try { + sub.unsubscribe(); + } catch (e) { + // ignore + } + }); + subscriptionsRef.current = []; + + disconnectStomp(); + }, [flushBuffer]); + + /** + * 초기 선박 데이터 로드 (API 호출) + * 참조: mda-react-front/src/map/MapUpdater.tsx (라인 128-152) + */ + const loadInitialData = useCallback(async () => { + if (initialLoadDoneRef.current) return; + + setIsLoading(true); + try { + console.log('[useShipData] Loading initial ship data...'); + const ships = await fetchAllSignals(); + + if (ships.length > 0) { + mergeFeatures(ships); + console.log(`[useShipData] Initial load complete: ${ships.length} ships`); + } + } catch (error) { + console.error('[useShipData] Initial load error:', error); + } finally { + initialLoadDoneRef.current = true; + setIsLoading(false); + } + }, [mergeFeatures]); + + // 초기화: API로 선박 데이터 로드 후 STOMP 연결 + useEffect(() => { + if (!autoConnect) return; + + // 1단계: API로 초기 선박 데이터 로드 + // 2단계: 로드 완료 후 STOMP 연결 (실시간 업데이트) + const initialize = async () => { + await loadInitialData(); + connect(); + }; + + initialize(); + + return () => { + // 타이머 클리어 + if (batchTimerRef.current) { + clearTimeout(batchTimerRef.current); + batchTimerRef.current = null; + } + disconnect(); + }; + }, [autoConnect]); // loadInitialData, connect, disconnect를 deps에서 제외 (의도적) + + return { + isConnected, + isLoading, + connect, + disconnect, + }; +} diff --git a/src/hooks/useShipLayer.js b/src/hooks/useShipLayer.js new file mode 100644 index 00000000..7e869915 --- /dev/null +++ b/src/hooks/useShipLayer.js @@ -0,0 +1,273 @@ +/** + * 선박 Deck.gl 레이어 관리 훅 + * - OpenLayers 맵과 Deck.gl 레이어 통합 + * - 배치 렌더러 기반 최적화된 렌더링 + * - 선박 데이터 변경 시 레이어 업데이트 + * + * 참조: mda-react-front/src/common/deck.ts + */ +import { useEffect, useRef, useCallback } from 'react'; +import { Deck } from '@deck.gl/core'; +import { toLonLat } from 'ol/proj'; +import { createShipLayers, clearClusterCache } from '../map/layers/shipLayer'; +import useShipStore from '../stores/shipStore'; +import { shipBatchRenderer } from '../map/ShipBatchRenderer'; + +/** + * 선박 레이어 관리 훅 + * @param {Object} map - OpenLayers 맵 인스턴스 + * @returns {Object} { deckCanvas } + */ +export default function useShipLayer(map) { + const deckRef = useRef(null); + const canvasRef = useRef(null); + const animationFrameRef = useRef(null); + const batchRendererInitialized = useRef(false); + + const { getSelectedShips, isShipVisible, showLabels, labelOptions } = useShipStore(); + + /** + * Deck.gl 인스턴스 초기화 + */ + const initDeck = useCallback((container) => { + if (deckRef.current) return; + + // Canvas 엘리먼트 생성 + const canvas = document.createElement('canvas'); + canvas.id = 'deck-canvas'; + canvas.style.position = 'absolute'; + canvas.style.left = '0'; + canvas.style.top = '0'; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + canvas.style.pointerEvents = 'none'; + canvas.style.zIndex = '0'; + container.appendChild(canvas); + canvasRef.current = canvas; + + // Deck.gl 인스턴스 생성 + deckRef.current = new Deck({ + canvas, + controller: false, + layers: [], + useDevicePixels: true, + onError: (error) => { + console.error('[Deck.gl] Error:', error); + }, + }); + }, []); + + /** + * Deck.gl viewState를 OpenLayers 뷰와 동기화 + */ + const syncViewState = useCallback(() => { + if (!map || !deckRef.current) return; + + const view = map.getView(); + const center = view.getCenter(); + const zoom = view.getZoom(); + const rotation = view.getRotation(); + + if (!center || zoom === undefined) return; + + const [lon, lat] = toLonLat(center); + + deckRef.current.setProps({ + viewState: { + longitude: lon, + latitude: lat, + zoom: zoom - 1, // OpenLayers와 Deck.gl 줌 레벨 차이 보정 + bearing: (-rotation * 180) / Math.PI, + pitch: 0, + }, + }); + }, [map]); + + /** + * 뷰포트 범위 계산 + */ + const getViewportBounds = useCallback(() => { + if (!map) return null; + + const view = map.getView(); + const size = map.getSize(); + if (!size) return null; + + const extent = view.calculateExtent(size); + // OpenLayers 좌표를 경위도로 변환 + const [minX, minY, maxX, maxY] = extent; + const [minLon, minLat] = toLonLat([minX, minY]); + const [maxLon, maxLat] = toLonLat([maxX, maxY]); + + return { minLon, maxLon, minLat, maxLat }; + }, [map]); + + /** + * 배치 렌더러 콜백 - 필터링된 선박으로 레이어 업데이트 + * @param {Array} ships - 밀도 제한 적용된 선박 (아이콘 + 라벨 공통) + * @param {number} trigger - 렌더링 트리거 + */ + const handleBatchRender = useCallback((ships, trigger) => { + if (!deckRef.current || !map) return; + + const view = map.getView(); + const zoom = view.getZoom() || 7; + const selectedShips = getSelectedShips(); + + // 현재 스토어에서 showLabels, labelOptions, isIntegrate, darkSignalIds 가져오기 + const { showLabels: currentShowLabels, labelOptions: currentLabelOptions, isIntegrate: currentIsIntegrate, darkSignalIds } = useShipStore.getState(); + + // 레이어 생성 (밀도 제한 적용된 선박 = 아이콘 + 라벨 공통) + // 아이콘이 표시되는 선박에만 라벨/신호상태도 표시 + const layers = createShipLayers(ships, selectedShips, zoom, currentShowLabels, currentLabelOptions, currentIsIntegrate, trigger, darkSignalIds); + + // Deck.gl 레이어 업데이트 + deckRef.current.setProps({ layers }); + }, [map, getSelectedShips]); + + /** + * 선박 레이어 업데이트 (배치 렌더러 사용) + */ + const updateLayers = useCallback(() => { + if (!deckRef.current || !map) return; + + if (!isShipVisible) { + // 선박 표시 Off + deckRef.current.setProps({ layers: [] }); + return; + } + + // 줌 레벨 업데이트 (렌더링 간격 조정용) + const view = map.getView(); + const zoom = view.getZoom() || 10; + const zoomIntChanged = shipBatchRenderer.setZoom(zoom); + + // 뷰포트 범위 업데이트 + const bounds = getViewportBounds(); + shipBatchRenderer.setViewportBounds(bounds); + + // 줌 정수 레벨이 변경되면 클러스터 캐시 클리어 + 즉시 렌더링 + if (zoomIntChanged) { + clearClusterCache(); + shipBatchRenderer.immediateRender(); + return; + } + + // 배치 렌더러에 렌더링 요청 + shipBatchRenderer.requestRender(); + }, [map, isShipVisible, getViewportBounds]); + + /** + * 렌더링 루프 + */ + const render = useCallback(() => { + syncViewState(); + updateLayers(); + deckRef.current?.redraw(); + }, [syncViewState, updateLayers]); + + // 맵 초기화 및 이벤트 바인딩 + useEffect(() => { + if (!map) return; + + // 맵 컨테이너에 Deck.gl 캔버스 추가 + const viewport = map.getViewport(); + initDeck(viewport); + + // 배치 렌더러 초기화 (1회만) + if (!batchRendererInitialized.current) { + shipBatchRenderer.initialize(handleBatchRender); + batchRendererInitialized.current = true; + } + + // 맵 이동/줌 시 동기화 + const handleMoveEnd = () => { + render(); + }; + + const handlePostRender = () => { + syncViewState(); + deckRef.current?.redraw(); + }; + + map.on('moveend', handleMoveEnd); + map.on('postrender', handlePostRender); + + // 초기 렌더링 + setTimeout(() => { + render(); + }, 100); + + // 클린업 + return () => { + map.un('moveend', handleMoveEnd); + map.un('postrender', handlePostRender); + + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + + if (deckRef.current) { + deckRef.current.finalize(); + deckRef.current = null; + } + + if (canvasRef.current) { + canvasRef.current.remove(); + canvasRef.current = null; + } + + // 배치 렌더러 정리 + shipBatchRenderer.dispose(); + batchRendererInitialized.current = false; + }; + }, [map, initDeck, render, syncViewState, handleBatchRender]); + + // 선박 데이터 변경 시 레이어 업데이트 + useEffect(() => { + // 스토어 구독하여 변경 감지 + const unsubscribe = useShipStore.subscribe( + (state) => [state.features, state.kindVisibility, state.sourceVisibility, state.isShipVisible, state.detailModals, state.isIntegrate, state.nationalVisibility, state.showLabels, state.labelOptions, state.darkSignalVisible, state.darkSignalIds], + (current, prev) => { + // 필터 변경 감지 (kindVisibility, sourceVisibility, isShipVisible, isIntegrate, nationalVisibility, showLabels, labelOptions, darkSignalVisible, darkSignalIds) + const filterChanged = + current[1] !== prev[1] || + current[2] !== prev[2] || + current[3] !== prev[3] || + current[5] !== prev[5] || + current[6] !== prev[6] || + current[7] !== prev[7] || + current[8] !== prev[8] || + current[9] !== prev[9] || + current[10] !== prev[10]; + + if (filterChanged) { + // 필터/선명표시 변경 시 즉시 렌더링 (사용자 인터랙션 응답성) + shipBatchRenderer.clearCache(); + clearClusterCache(); + shipBatchRenderer.immediateRender(); + return; // 즉시 렌더링 후 추가 처리 불필요 + } + + // 데이터 변경 시 일반 렌더링 (적응형 주기 적용) + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + animationFrameRef.current = requestAnimationFrame(() => { + updateLayers(); + deckRef.current?.redraw(); + }); + }, + { equalityFn: (a, b) => a.every((v, i) => v === b[i]) } + ); + + return () => { + unsubscribe(); + }; + }, [updateLayers]); + + return { + deckCanvas: canvasRef.current, + deckRef, + }; +} diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 00000000..c99e8e2d --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from 'react-router-dom'; +import App from './App'; + +// OpenLayers 스타일 +import 'ol/ol.css'; + +// 글로벌 스타일 +import './scss/global.scss'; + +ReactDOM.createRoot(document.getElementById('root')).render( + + + +); diff --git a/src/map/MapContainer.jsx b/src/map/MapContainer.jsx new file mode 100644 index 00000000..969c7440 --- /dev/null +++ b/src/map/MapContainer.jsx @@ -0,0 +1,296 @@ +import { useEffect, useRef, useCallback } from 'react'; +import Map from 'ol/Map'; +import View from 'ol/View'; +import { fromLonLat, transformExtent } from 'ol/proj'; +import { defaults as defaultControls, ScaleLine } from 'ol/control'; +import { defaults as defaultInteractions, DragBox } from 'ol/interaction'; +import { platformModifierKeyOnly } from 'ol/events/condition'; + +import { createBaseLayers } from './layers/baseLayer'; +import { useMapStore } from '../stores/mapStore'; +import useShipStore from '../stores/shipStore'; +import useShipData from '../hooks/useShipData'; +import useShipLayer from '../hooks/useShipLayer'; +import ShipLegend from '../components/ship/ShipLegend'; +import ShipTooltip from '../components/ship/ShipTooltip'; +import ShipDetailModal from '../components/ship/ShipDetailModal'; +import ShipContextMenu from '../components/ship/ShipContextMenu'; + +import useMeasure from './measure/useMeasure'; +import './measure/measure.scss'; +import './MapContainer.scss'; + +/** 호버 쓰로틀 간격 (ms) */ +const HOVER_THROTTLE_MS = 50; + +/** + * 지도 컨테이너 컴포넌트 + * - OpenLayers 맵 초기화 및 관리 + * - STOMP 선박 데이터 연결 + * - Deck.gl 선박 레이어 렌더링 + * - 선박 호버 툴팁 / 더블클릭 상세 모달 + */ +export default function MapContainer() { + const mapRef = useRef(null); + const mapInstanceRef = useRef(null); + const { map, setMap, setZoom, center } = useMapStore(); + const { showLegend } = useShipStore(); + const hoverInfo = useShipStore((s) => s.hoverInfo); + const detailModals = useShipStore((s) => s.detailModals); + + // STOMP 선박 데이터 연결 + useShipData({ autoConnect: true }); + + // Deck.gl 선박 레이어 + const { deckRef } = useShipLayer(map); + + // 측정 도구 + useMeasure(); + + // 호버 쓰로틀 타이머 + const hoverTimerRef = useRef(null); + + /** + * deck.pickObject 헬퍼 + */ + const pickShip = useCallback((pixel) => { + const deck = deckRef.current; + if (!deck) return null; + + // deck.layerManager가 초기화되기 전에 pickObject 호출하면 assertion error 발생 + if (!deck.layerManager) return null; + + try { + const result = deck.pickObject({ + x: pixel[0], + y: pixel[1], + layerIds: ['ship-icon-layer'], + }); + return result?.object || null; + } catch { + return null; + } + }, [deckRef]); + + /** + * OpenLayers pointermove → 호버 툴팁 + */ + const handlePointerMove = useCallback((evt) => { + // 드래그 중이면 무시 + if (evt.dragging) { + useShipStore.getState().setHoverInfo(null); + return; + } + + // 쓰로틀 + if (hoverTimerRef.current) return; + + hoverTimerRef.current = setTimeout(() => { + hoverTimerRef.current = null; + + const pixel = evt.pixel; + const ship = pickShip(pixel); + + if (ship) { + // evt.originalEvent에서 화면 좌표 가져오기 + const { clientX, clientY } = evt.originalEvent; + useShipStore.getState().setHoverInfo({ ship, x: clientX, y: clientY }); + } else { + useShipStore.getState().setHoverInfo(null); + } + }, HOVER_THROTTLE_MS); + }, [pickShip]); + + /** + * OpenLayers dblclick → 상세 모달 + */ + const handleDblClick = useCallback((evt) => { + const pixel = evt.pixel; + const ship = pickShip(pixel); + + if (ship) { + evt.stopPropagation(); + useShipStore.getState().openDetailModal(ship); + } + }, [pickShip]); + + /** + * pointerout → 툴팁 숨김 + */ + const handlePointerOut = useCallback(() => { + useShipStore.getState().setHoverInfo(null); + }, []); + + /** + * singleclick → 빈 영역 클릭 시 선택/메뉴 해제 + */ + const handleSingleClick = useCallback((evt) => { + const ship = pickShip(evt.pixel); + if (!ship) { + useShipStore.getState().clearSelectedShips(); + useShipStore.getState().closeContextMenu(); + } + }, [pickShip]); + + // OL 이벤트 바인딩 + useEffect(() => { + if (!map) return; + + map.on('pointermove', handlePointerMove); + map.on('dblclick', handleDblClick); + map.on('singleclick', handleSingleClick); + + // pointerout은 뷰포트 DOM 이벤트로 처리 + const viewport = map.getViewport(); + viewport.addEventListener('pointerout', handlePointerOut); + + // 우클릭 컨텍스트 메뉴 + const handleContextMenu = (e) => { + e.preventDefault(); + const pixel = map.getEventPixel(e); + const ship = pickShip(pixel); + const state = useShipStore.getState(); + + if (ship) { + state.openContextMenu({ x: e.clientX, y: e.clientY, ships: [ship] }); + } else if (state.selectedShipIds.length > 0) { + const selectedShips = state.getSelectedShips(); + state.openContextMenu({ x: e.clientX, y: e.clientY, ships: selectedShips }); + } + }; + viewport.addEventListener('contextmenu', handleContextMenu); + + return () => { + map.un('pointermove', handlePointerMove); + map.un('dblclick', handleDblClick); + map.un('singleclick', handleSingleClick); + viewport.removeEventListener('pointerout', handlePointerOut); + viewport.removeEventListener('contextmenu', handleContextMenu); + + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = null; + } + }; + }, [map, handlePointerMove, handleDblClick, handleSingleClick, handlePointerOut, pickShip]); + + useEffect(() => { + if (!mapRef.current || mapInstanceRef.current) return; + + // 베이스 레이어 생성 + const { worldMap, eastAsiaMap, korMap } = createBaseLayers(); + + // 스케일라인 컨트롤 (해리 단위) + const scaleLineControl = new ScaleLine({ + units: 'nautical', + bar: true, + text: true, + }); + + // 지도 인스턴스 생성 + const map = new Map({ + target: mapRef.current, + layers: [ + worldMap, + eastAsiaMap, + korMap, + ], + view: new View({ + center: fromLonLat(center), + zoom: 7, + minZoom: 0, + maxZoom: 17, + }), + controls: defaultControls({ + attribution: false, + zoom: false, + rotate: false, + }).extend([scaleLineControl]), + interactions: defaultInteractions({ + doubleClickZoom: false, + }), + }); + + // Ctrl+Drag 박스 선택 인터랙션 + const dragBox = new DragBox({ condition: platformModifierKeyOnly }); + map.addInteraction(dragBox); + + dragBox.on('boxend', () => { + const extent3857 = dragBox.getGeometry().getExtent(); + const [minLon, minLat, maxLon, maxLat] = transformExtent(extent3857, 'EPSG:3857', 'EPSG:4326'); + + const state = useShipStore.getState(); + const { features, darkSignalIds, isIntegrate, kindVisibility, sourceVisibility, + nationalVisibility, darkSignalVisible } = state; + + // 국적 코드 매핑 (shipStore.js와 동일) + const mapNational = (code) => { + if (!code) return 'OTHER'; + const c = code.toUpperCase(); + if (c === 'KR' || c === 'KOR' || c === '440') return 'KR'; + if (c === 'CN' || c === 'CHN' || c === '412' || c === '413' || c === '414') return 'CN'; + if (c === 'JP' || c === 'JPN' || c === '431' || c === '432') return 'JP'; + if (c === 'KP' || c === 'PRK' || c === '445') return 'KP'; + return 'OTHER'; + }; + + const matchedIds = []; + features.forEach((ship, featureId) => { + // 단독 레이더 제외 + if (ship.signalSourceCode === '000005' && !ship.integrate) return; + // 통합 모드 ON: isPriority만 + if (isIntegrate && ship.integrate && !ship.isPriority) return; + + // 다크시그널: 독립 필터 + if (darkSignalIds.has(featureId)) { + if (!darkSignalVisible) return; + } else { + if (!kindVisibility[ship.signalKindCode]) return; + if (!sourceVisibility[ship.signalSourceCode]) return; + const mappedNational = mapNational(ship.nationalCode); + if (!nationalVisibility[mappedNational]) return; + } + + const lon = parseFloat(ship.longitude); + const lat = parseFloat(ship.latitude); + if (lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat) { + matchedIds.push(featureId); + } + }); + + state.setSelectedShipIds(matchedIds); + }); + + // 줌 변경 이벤트 + map.getView().on('change:resolution', () => { + const zoom = Math.round(map.getView().getZoom()); + setZoom(zoom); + }); + + // 스토어에 맵 인스턴스 저장 + setMap(map); + mapInstanceRef.current = map; + + // 클린업 + return () => { + if (mapInstanceRef.current) { + mapInstanceRef.current.setTarget(null); + mapInstanceRef.current = null; + } + }; + }, []); + + return ( + <> +
    + {showLegend && } + {hoverInfo && ( + + )} + {detailModals.map((modal) => ( + + ))} + + + ); +} diff --git a/src/map/MapContainer.scss b/src/map/MapContainer.scss new file mode 100644 index 00000000..9db4911c --- /dev/null +++ b/src/map/MapContainer.scss @@ -0,0 +1,100 @@ +/** + * 지도 컨테이너 스타일 + */ +.map-container { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100%; + height: 100%; + z-index: 0; +} + +// OpenLayers 기본 컨트롤 숨김 (스케일바 제외) +.ol-control { + display: none !important; + background: transparent !important; +} + +// OpenLayers 기본 컨트롤 명시적 숨김 +.ol-attribution, +.ol-zoom, +.ol-rotate, +.ol-mouse-position, +.ol-full-screen, +.ol-overviewmap { + display: none !important; +} + +// OpenLayers 뷰포트 관련 스타일 +.ol-viewport { + background-color: transparent; +} + +// 컨트롤 컨테이너를 Deck.gl 캔버스 위에 표시 +.ol-overlaycontainer-stopevent { + z-index: 1 !important; +} + +// OpenLayers 오버레이 스타일 +.ol-overlay-container { + background: transparent; +} + +// ===================== +// 스케일바 위치 및 스타일 오버라이드 +// ===================== + +// 스케일바 wrapper (.ol-control이 아닌 .ol-scale-bar 직접 타겟) +.ol-scale-bar { + // 좌측→우측으로 위치 변경 + left: auto !important; + right: 70px !important; + bottom: 16px !important; + + // 배경 스타일 + background: rgba(30, 35, 45, 0.92) !important; + border-radius: 4px !important; + padding: 22px 10px 4px !important; + z-index: 100 !important; +} + +// 상단 비율 텍스트 (1:XXX) +.ol-scale-bar .ol-scale-text { + text-shadow: none !important; + font-family: 'Segoe UI', sans-serif !important; + color: #eee !important; + bottom: 30px !important; +} + +// 하단 거리 텍스트 +.ol-scale-bar .ol-scale-step-text { + text-shadow: none !important; + font-family: 'Segoe UI', sans-serif !important; + color: #bbb !important; + bottom: 1px !important; +} + +// 막대 색상 +.ol-scale-bar .ol-scale-singlebar-even { + background-color: #4fc3f7 !important; +} + +.ol-scale-bar .ol-scale-singlebar-odd { + background-color: #555 !important; +} + +// 기존 라인 스케일 숨김 +.ol-scale-line { + display: none !important; +} + +// ===================== +// Ctrl+Drag 박스 선택 스타일 +// ===================== +.ol-dragbox { + border: 2px solid rgba(255, 215, 0, 0.8); + background-color: rgba(255, 215, 0, 0.1); +} diff --git a/src/map/ShipBatchRenderer.js b/src/map/ShipBatchRenderer.js new file mode 100644 index 00000000..089dcdf6 --- /dev/null +++ b/src/map/ShipBatchRenderer.js @@ -0,0 +1,618 @@ +/** + * 선박 배치 렌더러 + * 참조: mda-react-front/src/common/deck.ts + * 참조: mda-react-front/src/tracking/utils/ReplayBatchRenderer.ts + * + * 역할: + * - 필터 캐시 생성 및 O(1) 필터링 + * - 뷰포트 범위 필터링 + * - 통합선박 처리 + * - 밀도 제한 클러스터링 (우선순위 기반) + * - 적응형 렌더링 간격 조정 + * - requestAnimationFrame 기반 렌더링 + */ + +import useShipStore from '../stores/shipStore'; +import { + SIGNAL_KIND_CODE_FISHING, + SIGNAL_KIND_CODE_KCGV, + SIGNAL_KIND_CODE_PASSENGER, + SIGNAL_KIND_CODE_CARGO, + SIGNAL_KIND_CODE_TANKER, + SIGNAL_KIND_CODE_GOV, + SIGNAL_KIND_CODE_NORMAL, + SIGNAL_KIND_CODE_BUOY, +} from '../types/constants'; + +// ===================== +// 렌더링 설정 +// 위성통신망 환경 최적화: 최소 트래픽, 최소 스펙 +// ===================== +const RENDER_CONFIG = { + defaultMinInterval: 1000, // 기본 최소 렌더링 간격 (1초) + maxInterval: 5000, // 최대 렌더링 간격 (5초) + targetRenderTime: 100, // 목표 렌더링 시간 (10fps 기준) + maxRenderTime: 500, // 최대 허용 렌더링 시간 +}; + +// ===================== +// 줌 레벨별 최소 렌더링 간격 +// 낮은 줌 = 많은 선박 = 긴 간격 +// ===================== +const ZOOM_MIN_INTERVAL = { + // zoom < 8: 광역 (전국/동아시아) + 7: 4000, + // zoom 8-9: 중광역 (해역) + 8: 3500, + 9: 3000, + // zoom 10-11: 중간 (항만 주변) + 10: 2500, + 11: 2000, + // zoom 12-13: 상세 (항만) + 12: 1500, + 13: 1500, + // zoom >= 14: 확대 (선석) + 14: 1000, +}; + +/** + * 줌 레벨에 따른 최소 렌더링 간격 반환 + * @param {number} zoom - 현재 줌 레벨 + * @returns {number} 최소 렌더링 간격 (ms) + */ +function getMinIntervalByZoom(zoom) { + const zoomInt = Math.floor(zoom); + + if (zoomInt <= 7) return ZOOM_MIN_INTERVAL[7]; + if (zoomInt >= 14) return ZOOM_MIN_INTERVAL[14]; + + return ZOOM_MIN_INTERVAL[zoomInt] || RENDER_CONFIG.defaultMinInterval; +} + +// ===================== +// 밀도 제한 설정 (선박 아이콘 클러스터링) +// 참조: mda-react-front/src/util/realTimeLayerUtil.ts +// +// 줌 레벨별 셀당 최대 선박 수 및 그리드 크기 설정 +// - 낮은 줌: 더 적은 개수 (밀집 지역 성능 최적화) +// - 높은 줌: 더 많은 개수 (상세 보기) +// ===================== +const DENSITY_LIMITS = [ + { maxZoom: 5, maxPerCell: 20, gridSizeMultiplier: 120 }, + { maxZoom: 6, maxPerCell: 25, gridSizeMultiplier: 100 }, + { maxZoom: 7, maxPerCell: 33, gridSizeMultiplier: 80 }, + { maxZoom: 8, maxPerCell: 35, gridSizeMultiplier: 75 }, + { maxZoom: 9, maxPerCell: 38, gridSizeMultiplier: 70 }, + { maxZoom: 10, maxPerCell: 40, gridSizeMultiplier: 55 }, + { maxZoom: 11, maxPerCell: 43, gridSizeMultiplier: 40 }, + { maxZoom: Infinity, maxPerCell: Infinity, gridSizeMultiplier: 30 }, +]; + +/** + * 밀도 제한 시 선박 우선순위 (낮을수록 높은 우선순위) + * 우선순위: 관심선박 > 함정 > 관공선 > 여객선 > 위험물 > 유조선 > 화물선 > 어선 > 기타 > 어망/부이 + */ +const SHIP_KIND_PRIORITY = { + [SIGNAL_KIND_CODE_KCGV]: 2, // 함정 + [SIGNAL_KIND_CODE_GOV]: 3, // 관공선 + [SIGNAL_KIND_CODE_PASSENGER]: 4, // 여객선 + [SIGNAL_KIND_CODE_TANKER]: 6, // 유조선 + [SIGNAL_KIND_CODE_CARGO]: 7, // 화물선 + [SIGNAL_KIND_CODE_FISHING]: 8, // 어선 + [SIGNAL_KIND_CODE_NORMAL]: 9, // 기타 + [SIGNAL_KIND_CODE_BUOY]: 10, // 어망/부이 +}; +const PRIORITY_FAVORITE = 0; // 관심선박 (최우선) +const PRIORITY_HAZARDOUS = 5; // 위험물 (여객선과 유조선 사이) +const PRIORITY_DEFAULT = 11; // 기본값 (최하위) + +/** + * 선박 우선순위 점수 계산 + * @param {Object} ship - 선박 데이터 + * @param {Set} favoriteSet - 관심선박 ID Set + * @returns {number} 우선순위 점수 (낮을수록 높은 우선순위) + */ +function getShipPriority(ship, favoriteSet) { + // 관심선박 체크 (최우선) + const favoriteKey = `${ship.targetId}_${ship.signalSourceCode}`; + if (favoriteSet && favoriteSet.has(favoriteKey)) { + return PRIORITY_FAVORITE; + } + + // 위험물 체크 (hazardousCategory = '1') + if (ship.hazardousCategory === '1') { + return PRIORITY_HAZARDOUS; + } + + // 선종 코드 기반 우선순위 + return SHIP_KIND_PRIORITY[ship.signalKindCode] ?? PRIORITY_DEFAULT; +} + +/** + * 줌레벨에 따른 밀도 설정 반환 + * @param {number} zoomLevel - 현재 줌 레벨 + * @returns {Object} 밀도 설정 { maxZoom, maxPerCell, gridSizeMultiplier } + */ +function getDensityConfig(zoomLevel) { + for (const config of DENSITY_LIMITS) { + if (zoomLevel <= config.maxZoom) { + return config; + } + } + return DENSITY_LIMITS[DENSITY_LIMITS.length - 1]; +} + +/** + * 선박 아이콘용 밀도 제한 필터링 (우선순위 기반) + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - applyDensityLimit() + * + * - 그리드 기반으로 동일 영역 내 최대 N척까지만 렌더링 + * - 밀집 지역도 N척이 겹쳐 보여서 "빈 공간"처럼 보이지 않음 + * - 줌레벨이 높아지면 제한이 완화되어 더 많은 선박 표시 + * - 우선순위: 관심선박 > 함정 > 관공선 > 여객선 > 위험물 > 유조선 > 화물선 > 어선 > 기타 > 어망/부이 + * + * @param {Array} ships - 필터링된 선박 데이터 + * @param {number} zoomLevel - 현재 줌레벨 + * @param {Set} favoriteSet - 관심선박 ID Set (optional) + * @returns {Array} 밀도 제한이 적용된 선박 데이터 + */ +function applyDensityLimit(ships, zoomLevel, favoriteSet = null) { + const config = getDensityConfig(zoomLevel); + + // 제한 없음 설정이면 원본 반환 + if (config.maxPerCell === Infinity) { + return ships; + } + + // 우선순위 기반 정렬 (낮은 점수 = 높은 우선순위) + const sortedShips = [...ships].sort((a, b) => { + return getShipPriority(a, favoriteSet) - getShipPriority(b, favoriteSet); + }); + + // 그리드 크기 계산 (줌레벨에 따라 조정) + const gridSize = Math.pow(2, -zoomLevel) * config.gridSizeMultiplier; + + // 그리드별 선박 수 카운트 + const gridCounts = new Map(); + const result = []; + + const len = sortedShips.length; + for (let i = 0; i < len; i++) { + const ship = sortedShips[i]; + const gridX = Math.floor(ship.longitude / gridSize); + const gridY = Math.floor(ship.latitude / gridSize); + const gridKey = `${gridX},${gridY}`; + + const currentCount = gridCounts.get(gridKey) || 0; + + // 셀 내 최대 개수 미만이면 포함 + if (currentCount < config.maxPerCell) { + result.push(ship); + gridCounts.set(gridKey, currentCount + 1); + } + // 초과하면 스킵 (우선순위 낮은 선박이 제외됨) + } + + // deck.gl은 배열 뒤쪽 아이템이 위에 그려짐 + // 우선순위 높은 선박이 위에 보이도록 역순 반환 + return result.reverse(); +} + +// ===================== +// 필터 캐시 인터페이스 +// 참조: mda-react-front/src/common/deck.ts (52-108) +// ===================== + +/** + * 필터 캐시 생성 + * 렌더링 시작 시 1회 호출하여 Set 기반 O(1) lookup 가능하게 함 + * + * @returns {Object} 필터 캐시 객체 + */ +function buildFilterCache() { + const { kindVisibility, sourceVisibility, nationalVisibility, isShipVisible, isIntegrate, darkSignalVisible, darkSignalIds } = useShipStore.getState(); + + // 활성화된 선종 코드 Set + const enabledKinds = new Set(); + Object.entries(kindVisibility).forEach(([code, isChecked]) => { + if (isChecked) { + enabledKinds.add(code); + } + }); + + // 활성화된 신호원 코드 Set + const enabledSources = new Set(); + Object.entries(sourceVisibility).forEach(([code, isChecked]) => { + if (isChecked) { + enabledSources.add(code); + } + }); + + // 활성화된 국적 코드 Set + const enabledNationals = new Set(); + Object.entries(nationalVisibility).forEach(([code, isChecked]) => { + if (isChecked) { + enabledNationals.add(code); + } + }); + + return { + enabledKinds, + enabledSources, + enabledNationals, + isShipVisible, + isIntegrate, + darkSignalVisible, + darkSignalIds, + }; +} + +/** + * 국적 코드 매핑 (실제 국적 코드 -> 필터 코드) + * @param {string} nationalCode - 선박의 국적 코드 + * @returns {string} 필터용 국적 코드 + */ +function mapNationalCode(nationalCode) { + if (!nationalCode) return 'OTHER'; + const code = nationalCode.toUpperCase(); + if (code === 'KR' || code === 'KOR' || code === '440') return 'KR'; + if (code === 'CN' || code === 'CHN' || code === '412' || code === '413' || code === '414') return 'CN'; + if (code === 'JP' || code === 'JPN' || code === '431' || code === '432') return 'JP'; + if (code === 'KP' || code === 'PRK' || code === '445') return 'KP'; + return 'OTHER'; +} + +/** + * 캐시된 필터를 사용한 필터링 (O(1) lookup) + * 참조: mda-react-front/src/common/deck.ts - applyFilterWithCache() + * + * @param {Object} ship - 선박 데이터 + * @param {Object} cache - 필터 캐시 + * @returns {boolean} 필터 통과 여부 + */ +function applyFilterWithCache(ship, cache) { + // 전체 선박 표시 Off + if (!cache.isShipVisible) return false; + + // 통합 모드 필터: 통합 모드 On이면 isPriority=1만 표시 + // 참조: mda-react-front/src/common/deck.ts (354-355) + if (cache.isIntegrate && ship.integrate && !ship.isPriority) return false; + + // 다크시그널은 독립 필터 (선종/신호원/국적 필터 무시) + if (cache.darkSignalIds.has(ship.featureId)) return cache.darkSignalVisible; + + // 선종 필터 (Set.has = O(1)) + if (!cache.enabledKinds.has(ship.signalKindCode)) return false; + + // 신호원 필터 (Set.has = O(1)) + if (!cache.enabledSources.has(ship.signalSourceCode)) return false; + + // 국적 필터 (Set.has = O(1)) + const mappedNational = mapNationalCode(ship.nationalCode); + if (!cache.enabledNationals.has(mappedNational)) return false; + + // lost 플래그: 영해선 내/외 구분 (1=영해선 밖, 0=영해선 내) + // 렌더링 제외 대상 아님 - 삭제는 /topic/ship-delete 토픽으로만 처리 + + return true; +} + +/** + * 뷰포트 범위 내 선박 필터링 + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - filterFeaturesWithBounds() + * + * @param {Array} ships - 선박 배열 + * @param {Object} bounds - 뷰포트 범위 { minLon, maxLon, minLat, maxLat } + * @returns {Array} 범위 내 선박 배열 + */ +function filterByViewport(ships, bounds) { + if (!bounds) return ships; + + const { minLon, maxLon, minLat, maxLat } = bounds; + + return ships.filter((ship) => { + const lon = ship.longitude; + const lat = ship.latitude; + return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat; + }); +} + +/** + * 선박 배치 렌더러 클래스 + * 참조: mda-react-front/src/tracking/utils/ReplayBatchRenderer.ts + */ +class ShipBatchRenderer { + constructor() { + // 렌더링 상태 + this.renderState = { + animationFrameId: null, + pendingRender: false, + isRendering: false, + lastRenderTime: 0, + currentInterval: RENDER_CONFIG.defaultMinInterval, + currentZoom: 10, // 현재 줌 레벨 + }; + + // 캐시 + this.cache = { + filterCache: null, + lastFilterHash: '', + lastShipsData: [], // 밀도 제한 적용된 선박 (아이콘 + 라벨 공통) + lastFilteredCount: 0, // 필터링된 선박 수 (밀도 제한 전) + lastRenderTrigger: 0, + favoriteSet: null, // 관심선박 Set (향후 구현) + }; + + // 외부 콜백 + this.onRenderCallback = null; + + // 뷰포트 범위 + this.viewportBounds = null; + } + + /** + * 배치 렌더러 초기화 + * @param {Function} renderCallback - 레이어 렌더링 콜백 + * (ships, trigger) => void + * - ships: 밀도 제한 적용된 선박 (아이콘 + 라벨 공통) + * - trigger: 렌더링 트리거 (주기적 갱신용) + */ + initialize(renderCallback) { + this.onRenderCallback = renderCallback; + console.log('[ShipBatchRenderer] Initialized'); + } + + /** + * 뷰포트 범위 업데이트 + * @param {Object} bounds - { minLon, maxLon, minLat, maxLat } + */ + setViewportBounds(bounds) { + this.viewportBounds = bounds; + } + + /** + * 줌 레벨 업데이트 + * 줌 변경 시 최소 렌더링 간격도 재조정 + * @param {number} zoom - 현재 줌 레벨 + * @returns {boolean} 정수 줌 레벨이 변경되었는지 여부 + */ + setZoom(zoom) { + const prevZoom = this.renderState.currentZoom; + const prevZoomInt = Math.floor(prevZoom); + const newZoomInt = Math.floor(zoom); + + this.renderState.currentZoom = zoom; + + // 줌 레벨이 변경되면 최소 간격에 맞게 현재 간격 재조정 + const newMinInterval = getMinIntervalByZoom(zoom); + const prevMinInterval = getMinIntervalByZoom(prevZoom); + + if (newMinInterval !== prevMinInterval) { + // 새 최소 간격이 현재 간격보다 크면 현재 간격 증가 + if (newMinInterval > this.renderState.currentInterval) { + this.renderState.currentInterval = newMinInterval; + } + // 새 최소 간격이 현재 간격보다 작고, 현재 간격이 이전 최소 간격과 같으면 감소 + else if (this.renderState.currentInterval === prevMinInterval) { + this.renderState.currentInterval = newMinInterval; + } + } + + // 정수 줌 레벨 변경 여부 반환 + return prevZoomInt !== newZoomInt; + } + + /** + * 렌더링 요청 + * 다음 렌더링 사이클에 처리됨 + */ + requestRender() { + if (this.renderState.pendingRender) return; + + this.renderState.pendingRender = true; + + // requestAnimationFrame 사용하여 렌더링 스케줄 + if (!this.renderState.animationFrameId) { + this.scheduleRender(); + } + } + + /** + * 렌더링 스케줄링 + */ + scheduleRender() { + const now = Date.now(); + const elapsed = now - this.renderState.lastRenderTime; + const delay = Math.max(0, this.renderState.currentInterval - elapsed); + + this.renderState.animationFrameId = setTimeout(() => { + this.renderState.animationFrameId = requestAnimationFrame(() => { + this.executeRender(); + }); + }, delay); + } + + /** + * 실제 렌더링 실행 + */ + executeRender() { + if (this.renderState.isRendering || !this.onRenderCallback) { + this.renderState.animationFrameId = null; + return; + } + + const startTime = performance.now(); + + this.renderState.isRendering = true; + this.renderState.pendingRender = false; + + try { + // 1. 스토어에서 전체 선박 가져오기 + const { features } = useShipStore.getState(); + const allShips = Array.from(features.values()); + + // 2. 필터 캐시 생성 (렌더링 시작 시 1회) + this.cache.filterCache = buildFilterCache(); + + // 3. 뷰포트 범위 필터링 (먼저 수행 - 대량 데이터 감소) + const viewportShips = this.viewportBounds + ? filterByViewport(allShips, this.viewportBounds) + : allShips; + + // 4. 필터 적용 (캐시된 필터 사용 - O(1) lookup) + const filteredShips = viewportShips.filter((ship) => + applyFilterWithCache(ship, this.cache.filterCache) + ); + + // 5. 밀도 제한 적용 (선박 아이콘 클러스터링, 우선순위 기반) + const zoom = this.renderState.currentZoom; + const densityLimitedShips = applyDensityLimit(filteredShips, zoom, this.cache.favoriteSet); + + // 6. 렌더링 트리거 증가 + this.cache.lastRenderTrigger++; + + // 7. 콜백 호출 (밀도 제한 적용된 데이터 = 아이콘 + 라벨 공통) + // 아이콘이 표시되는 선박에만 라벨/신호상태도 표시 + this.onRenderCallback(densityLimitedShips, this.cache.lastRenderTrigger); + + // 8. 캐시 업데이트 + this.cache.lastShipsData = densityLimitedShips; + this.cache.lastFilteredCount = filteredShips.length; + + // 9. 렌더링 시간 측정 및 간격 조정 + const renderTime = performance.now() - startTime; + this.adjustRenderInterval(renderTime); + + } catch (error) { + console.error('[ShipBatchRenderer] Render error:', error); + } finally { + this.renderState.isRendering = false; + this.renderState.lastRenderTime = Date.now(); + this.renderState.animationFrameId = null; + + // 대기 중인 렌더링 요청이 있으면 다시 스케줄 + if (this.renderState.pendingRender) { + this.scheduleRender(); + } + } + } + + /** + * 적응형 렌더링 간격 조정 + * 줌 레벨에 따른 최소 간격 + 성능 기반 적응형 조정 + * + * @param {number} renderTime - 렌더링 소요 시간 (ms) + */ + adjustRenderInterval(renderTime) { + const { targetRenderTime, maxRenderTime, maxInterval } = RENDER_CONFIG; + const minInterval = getMinIntervalByZoom(this.renderState.currentZoom); + + if (renderTime > maxRenderTime) { + // 렌더링 시간이 너무 길면 간격 증가 (최대 5초) + this.renderState.currentInterval = Math.min( + this.renderState.currentInterval * 1.2, + maxInterval + ); + } else if (renderTime < targetRenderTime) { + // 렌더링 시간이 충분히 짧으면 간격 감소 (줌 레벨별 최소값까지) + this.renderState.currentInterval = Math.max( + this.renderState.currentInterval * 0.9, + minInterval + ); + } + } + + /** + * 강제 렌더링 (필터 변경 등) + * 일반 렌더링 주기에 따름 + */ + forceRender() { + this.cache.filterCache = null; + this.requestRender(); + } + + /** + * 즉시 렌더링 (필터/선명표시 토글 등 사용자 인터랙션) + * 렌더링 주기를 무시하고 즉시 실행 + */ + immediateRender() { + // 기존 스케줄 취소 + if (this.renderState.animationFrameId) { + clearTimeout(this.renderState.animationFrameId); + cancelAnimationFrame(this.renderState.animationFrameId); + this.renderState.animationFrameId = null; + } + + // 캐시 클리어 + this.cache.filterCache = null; + this.renderState.pendingRender = false; + + // 즉시 렌더링 실행 + requestAnimationFrame(() => { + this.executeRender(); + }); + } + + /** + * 캐시 클리어 + */ + clearCache() { + this.cache.filterCache = null; + this.cache.lastFilterHash = ''; + this.cache.lastShipsData = []; + this.cache.lastFilteredCount = 0; + this.cache.favoriteSet = null; + } + + /** + * 배치 렌더러 정리 + */ + dispose() { + if (this.renderState.animationFrameId) { + cancelAnimationFrame(this.renderState.animationFrameId); + clearTimeout(this.renderState.animationFrameId); + this.renderState.animationFrameId = null; + } + this.clearCache(); + this.onRenderCallback = null; + console.log('[ShipBatchRenderer] Disposed'); + } + + /** + * 마지막 필터링된 선박 데이터 반환 + * @returns {Array} 필터링된 선박 배열 + */ + getFilteredShips() { + return this.cache.lastShipsData; + } + + /** + * 현재 렌더링 통계 반환 + * @returns {Object} 렌더링 통계 + */ + getStats() { + const zoom = this.renderState.currentZoom; + const densityConfig = getDensityConfig(zoom); + return { + currentZoom: zoom, + minInterval: getMinIntervalByZoom(zoom), + currentInterval: this.renderState.currentInterval, + lastRenderTime: this.renderState.lastRenderTime, + filteredCount: this.cache.lastFilteredCount, // 밀도 제한 전 선박 수 + renderedCount: this.cache.lastShipsData.length, // 밀도 제한 후 렌더링 선박 수 + densityConfig: { + maxPerCell: densityConfig.maxPerCell, + gridSizeMultiplier: densityConfig.gridSizeMultiplier, + }, + renderTrigger: this.cache.lastRenderTrigger, + }; + } +} + +// 싱글톤 인스턴스 +export const shipBatchRenderer = new ShipBatchRenderer(); + +// 유틸리티 함수 export +export { buildFilterCache, applyFilterWithCache, filterByViewport, applyDensityLimit, getDensityConfig }; + +export default ShipBatchRenderer; diff --git a/src/map/layers/baseLayer.js b/src/map/layers/baseLayer.js new file mode 100644 index 00000000..b4a8aadf --- /dev/null +++ b/src/map/layers/baseLayer.js @@ -0,0 +1,70 @@ +/** + * 베이스맵 레이어 설정 + * - 메인 프로젝트(mda-react-front)의 mapLayer.ts 참조 + */ +import { XYZ } from 'ol/source'; +import { transformExtent } from 'ol/proj'; +import WebGLTileLayer from 'ol/layer/WebGLTile'; + +// 좌표계 상수 +const EPSG_3857 = 'EPSG:3857'; +const EPSG_4326 = 'EPSG:4326'; + +/** + * 레이어 설정 + */ +export const mapLayerConfig = { + // 세계 지도 (줌 0-11) + worldLayer: { + source: new XYZ({ + url: '/MAPS/WORLD_webp/{z}/{x}/{y}.webp', + minZoom: 0, + maxZoom: 11, + attributions: 'ⓒ OpenStreetMap', + }), + preload: Infinity, + }, + + // 동아시아 상세 (줌 12-15) + eastAsiaLayer: { + source: new XYZ({ + url: '/MAPS/EAST_ASIA_webp/{z}/{x}/{y}.webp', + minZoom: 12, + maxZoom: 15, + }), + preload: 0, + minZoom: 12, + zIndex: 1, + extent: transformExtent([110, 20, 140, 45], EPSG_4326, EPSG_3857), + }, + + // 한국 상세 (줌 16-17) + korLayer: { + source: new XYZ({ + url: '/MAPS/KOR_webp/{z}/{x}/{y}.webp', + minZoom: 16, + maxZoom: 17, + }), + preload: Infinity, + minZoom: 16, + zIndex: 1, + extent: transformExtent([124, 32, 133, 39], EPSG_4326, EPSG_3857), + }, +}; + +/** + * 베이스맵 레이어 생성 + */ +export const createBaseLayers = () => { + const worldMap = new WebGLTileLayer(mapLayerConfig.worldLayer); + const eastAsiaMap = new WebGLTileLayer(mapLayerConfig.eastAsiaLayer); + const korMap = new WebGLTileLayer(mapLayerConfig.korLayer); + + return { + worldMap, + eastAsiaMap, + korMap, + }; +}; + +export default createBaseLayers; diff --git a/src/map/layers/shipLayer.js b/src/map/layers/shipLayer.js new file mode 100644 index 00000000..77d835d4 --- /dev/null +++ b/src/map/layers/shipLayer.js @@ -0,0 +1,1031 @@ +/** + * Deck.gl 선박 레이어 + * 참조: mda-react-front/src/common/deck.ts + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts + */ +import { IconLayer, TextLayer, ScatterplotLayer, LineLayer, PathLayer } from '@deck.gl/layers'; +import { fromLonLat, toLonLat } from 'ol/proj'; +import { + ICON_ATLAS_MAPPING, + ICON_MAPPING_KIND_MOVING, + ICON_MAPPING_KIND_STOPPING, + SPEED_THRESHOLD, + SIGNAL_KIND_CODE_BUOY, + SIGNAL_FLAG_CONFIGS, +} from '../../types/constants'; + +// 아이콘 아틀라스 이미지 +import atlasImg from '../../assets/img/icon/atlas.png'; + +// ===================== +// 도 → 라디안 변환 상수 +// ===================== +const DEG_TO_RAD = 0.0174533; // Math.PI / 180 + +// ===================== +// 클러스터링 캐시 +// 줌 레벨 + isIntegrate 상태 + 렌더 트리거 기반 갱신 +// 참조: mda-react-front/src/util/realTimeLayerUtil.ts (라인 327-363) +// ===================== +const clusterCache = { + lastZoom: null, + lastDataLength: 0, + lastIsIntegrate: null, + lastRenderTrigger: 0, + clusteredData: [], + // 선박 위치 샘플 해시 (변경 감지용) + positionHash: '', +}; + +// 신호상태 레이어용 별도 캐시 +const signalClusterCache = { + lastZoom: null, + lastDataLength: 0, + lastIsIntegrate: null, + lastRenderTrigger: 0, + clusteredData: [], + positionHash: '', +}; + +// 클러스터 갱신 주기 (N회 렌더링마다 재계산) +const CLUSTER_REFRESH_INTERVAL = 3; + +// 클러스터 그리드 크기 배율 상수 +const CLUSTER_GRID_SIZE_LABEL = 50; // 라벨/선박명용 (기본) +const CLUSTER_GRID_SIZE_SIGNAL = 35; // 신호상태용 (더 조밀하게 표시) + +/** + * 그리드 기반 클러스터링 (개선된 버전) + * - 줌 레벨이 높을수록 더 많은 선박 표시 (점진적 증가) + * - 낮은 줌에서 표시된 선박은 높은 줌에서도 반드시 표시 + * + * @param {Array} data - 선박 데이터 배열 + * @param {number} zoomLevel - 현재 줌 레벨 + * @param {number} gridSizeMultiplier - 그리드 크기 배율 (기본: 50) + * @returns {Array} 클러스터링된 선박 배열 + */ +function clusterPoints(data, zoomLevel, gridSizeMultiplier = CLUSTER_GRID_SIZE_LABEL) { + // 그리드 크기: 줌 레벨이 높을수록 작은 그리드 + const gridSize = Math.pow(2, -zoomLevel) * gridSizeMultiplier; + const clusters = {}; + + // 1단계: 그리드별 선박 그룹화 + const len = data.length; + for (let i = 0; i < len; i++) { + const item = data[i]; + const gridX = Math.floor(item.longitude / gridSize); + const gridY = Math.floor(item.latitude / gridSize); + const gridKey = `${gridX},${gridY}`; + + if (!clusters[gridKey]) { + clusters[gridKey] = []; + } + clusters[gridKey].push(item); + } + + // 2단계: 각 그리드에서 대표 선박 선택 + // 우선순위: 신호상태 있음 > 선박명 있음 > 첫 번째 + const clusterKeys = Object.keys(clusters); + const result = []; + + for (let i = 0; i < clusterKeys.length; i++) { + const clusterShips = clusters[clusterKeys[i]]; + + // 우선순위에 따라 대표 선박 선택 + let representative = clusterShips[0]; + + for (let j = 0; j < clusterShips.length; j++) { + const ship = clusterShips[j]; + // 신호상태가 있는 선박 우선 (통합선박이거나 신호원 정보가 있음) + const hasSignalInfo = ship.signalSourceCode || ship.ais || ship.vpass || ship.enav; + const hasName = ship.shipName && ship.shipName.trim(); + + const repHasSignalInfo = representative.signalSourceCode || representative.ais || representative.vpass || representative.enav; + const repHasName = representative.shipName && representative.shipName.trim(); + + // 더 나은 대표 선박 선택 + if ((hasSignalInfo && !repHasSignalInfo) || + (hasSignalInfo === repHasSignalInfo && hasName && !repHasName)) { + representative = ship; + } + } + + result.push(representative); + } + + return result; +} + +/** + * 선박 위치 해시 생성 (샘플링) + * 처음/중간/마지막 선박 위치를 해시로 변환 + * @param {Array} ships - 선박 배열 + * @returns {string} 위치 해시 + */ +function computePositionHash(ships) { + if (ships.length === 0) return ''; + + // 샘플링: 처음, 중간, 마지막 선박 위치 + const indices = [0, Math.floor(ships.length / 2), ships.length - 1]; + const samples = indices.map(i => { + const ship = ships[Math.min(i, ships.length - 1)]; + // 소수점 4자리까지만 (약 10m 정밀도) + return `${ship.longitude.toFixed(4)},${ship.latitude.toFixed(4)}`; + }); + + return samples.join('|'); +} + +/** + * 캐시된 클러스터링 결과 가져오기 + * - 줌 레벨, isIntegrate 변경 시 재계산 + * - 렌더 트리거 주기에 따라 갱신 (선박 위치 변경 반영) + * + * @param {Array} ships - 선박 데이터 배열 + * @param {number} zoom - 현재 줌 레벨 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @param {number} renderTrigger - 렌더링 트리거 (배치 렌더러에서 증가) + * @returns {Array} 클러스터링된 선박 배열 + */ +function getClusteredShips(ships, zoom, isIntegrate, renderTrigger = 0) { + const zoomInt = Math.floor(zoom); + const positionHash = computePositionHash(ships); + + // 캐시 유효 조건 + const zoomMatch = clusterCache.lastZoom === zoomInt; + const integrateMatch = clusterCache.lastIsIntegrate === isIntegrate; + const sizeMatch = Math.abs(clusterCache.lastDataLength - ships.length) < ships.length * 0.1; + const positionMatch = clusterCache.positionHash === positionHash; + + // 렌더 트리거 주기 체크 (N회마다 강제 갱신) + const triggerDiff = renderTrigger - clusterCache.lastRenderTrigger; + const needsPeriodicRefresh = triggerDiff >= CLUSTER_REFRESH_INTERVAL; + + // 캐시 히트 조건: 모든 조건 만족 + 주기적 갱신 불필요 + if (zoomMatch && integrateMatch && sizeMatch && positionMatch && !needsPeriodicRefresh) { + return clusterCache.clusteredData; + } + + // 새로 클러스터링 계산 + const clustered = clusterPoints(ships, zoomInt); + + // 캐시 업데이트 + clusterCache.lastZoom = zoomInt; + clusterCache.lastDataLength = ships.length; + clusterCache.lastIsIntegrate = isIntegrate; + clusterCache.lastRenderTrigger = renderTrigger; + clusterCache.positionHash = positionHash; + clusterCache.clusteredData = clustered; + + return clustered; +} + +/** + * 클러스터 캐시 초기화 + */ +export function clearClusterCache() { + clusterCache.lastZoom = null; + clusterCache.lastDataLength = 0; + clusterCache.lastIsIntegrate = null; + clusterCache.lastRenderTrigger = 0; + clusterCache.positionHash = ''; + clusterCache.clusteredData = []; + + signalClusterCache.lastZoom = null; + signalClusterCache.lastDataLength = 0; + signalClusterCache.lastIsIntegrate = null; + signalClusterCache.lastRenderTrigger = 0; + signalClusterCache.positionHash = ''; + signalClusterCache.clusteredData = []; +} + +/** + * 실제로 신호상태 SVG가 생성 가능한지 체크 + * @param {Object} ship - 선박 데이터 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @returns {boolean} SVG 생성 가능 여부 + */ +function canGenerateSignalSVG(ship, isIntegrate) { + const isIntegratedShipTarget = ship.targetId && ship.targetId.includes('_'); + + if (isIntegrate && isIntegratedShipTarget) { + // 통합선박 + 선박통합 ON: 장비 값이 '0' 또는 '1'인 것이 하나라도 있어야 함 + return ship.ais === '0' || ship.ais === '1' || + ship.vpass === '0' || ship.vpass === '1' || + ship.enav === '0' || ship.enav === '1' || + ship.vtsAis === '0' || ship.vtsAis === '1' || + ship.dMfHf === '0' || ship.dMfHf === '1' || + ship.vtsRadar === '0' || ship.vtsRadar === '1'; + } else { + // 단독선박 또는 선박통합 OFF: signalSourceCode가 있으면 SVG 생성 가능 + return !!ship.signalSourceCode; + } +} + +/** + * 신호상태용 클러스터링 결과 가져오기 + * 실제로 SVG가 생성 가능한 선박만 대상으로 클러스터링 + * + * @param {Array} ships - 필터링된 선박 데이터 배열 (지도에 그려지는 선박) + * @param {number} zoom - 현재 줌 레벨 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @param {number} renderTrigger - 렌더링 트리거 + * @returns {Array} 클러스터링된 선박 배열 + */ +function getSignalClusteredShips(ships, zoom, isIntegrate, renderTrigger = 0) { + const zoomInt = Math.floor(zoom); + + // 실제로 SVG 생성 가능한 선박만 필터링 + const shipsWithSignal = ships.filter(ship => canGenerateSignalSVG(ship, isIntegrate)); + const positionHash = computePositionHash(shipsWithSignal); + + // 캐시 유효 조건 + const zoomMatch = signalClusterCache.lastZoom === zoomInt; + const integrateMatch = signalClusterCache.lastIsIntegrate === isIntegrate; + const sizeMatch = Math.abs(signalClusterCache.lastDataLength - shipsWithSignal.length) < shipsWithSignal.length * 0.1; + const positionMatch = signalClusterCache.positionHash === positionHash; + + // 렌더 트리거 주기 체크 + const triggerDiff = renderTrigger - signalClusterCache.lastRenderTrigger; + const needsPeriodicRefresh = triggerDiff >= CLUSTER_REFRESH_INTERVAL; + + // 캐시 히트 + if (zoomMatch && integrateMatch && sizeMatch && positionMatch && !needsPeriodicRefresh) { + return signalClusterCache.clusteredData; + } + + // 클러스터링 (SVG 생성 가능한 선박만 대상, 신호상태용 작은 그리드 사용) + const clustered = clusterPoints(shipsWithSignal, zoomInt, CLUSTER_GRID_SIZE_SIGNAL); + + // 캐시 업데이트 + signalClusterCache.lastZoom = zoomInt; + signalClusterCache.lastDataLength = shipsWithSignal.length; + signalClusterCache.lastIsIntegrate = isIntegrate; + signalClusterCache.lastRenderTrigger = renderTrigger; + signalClusterCache.positionHash = positionHash; + signalClusterCache.clusteredData = clustered; + + return clustered; +} + +// ===================== +// 속도벡터 스케일 (SOG 기반) +// 참조: mda-react-front/src/common/deck.ts (라인 708) +// +// 개선: 최소 길이 보장 + 속도 증가에 따른 점진적 증가 +// - 최소 길이: 항해중 아이콘(SPEED_THRESHOLD 기준) 크기보다 충분히 길게 +// - 추가 길이: 속도 증가분에 비례하여 완만하게 증가 +// ===================== +const VECTOR_MIN_LENGTH = 500; // 최소 벡터 길이 (투영좌표 단위, 약 500m) +const VECTOR_INCREMENT_SCALE = 40; // 속도 1kn 증가당 추가 길이 + +/** + * 선박 아이콘 결정 + * @param {Object} ship - 선박 데이터 + * @returns {string} 아이콘 이름 (ICON_ATLAS_MAPPING 키) + */ +export function getShipIcon(ship, darkSignalIds) { + // 다크시그널(소실신호): darkSignalIds Set으로 판단 + if (darkSignalIds && darkSignalIds.has(ship.featureId)) { + return 'lostShipImg'; + } + + // 속력에 따른 이동/정지 판단 + const isMoving = Number(ship.sog) > SPEED_THRESHOLD; + + if (isMoving) { + return ICON_MAPPING_KIND_MOVING[ship.signalKindCode] || 'etcImg'; + } else { + return ICON_MAPPING_KIND_STOPPING[ship.signalKindCode] || 'etcStopImg'; + } +} + +/** + * 선박 회전 각도 계산 (COG 기준) + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - targetAngle() + * + * @param {Object} ship - 선박 데이터 + * @returns {number} 회전 각도 (degrees) + */ +export function getShipAngle(ship) { + // 부이는 회전하지 않음 + if (ship.signalKindCode === SIGNAL_KIND_CODE_BUOY) { + return 0; + } + + // COG (Course Over Ground) 기준 회전 + // 메인 프로젝트와 동일하게 -COG 적용 + return -Number(ship.cog) || 0; +} + +/** + * 선박 크기 결정 (운항/정박 상태 + 줌 레벨) + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - targetScale() + * + * @param {Object} ship - 선박 데이터 + * @param {number} zoom - 현재 줌 레벨 + * @returns {number} 아이콘 크기 (px) + */ +export function getShipSize(ship, zoom) { + // 부이: 항상 16px + if (ship.signalKindCode === SIGNAL_KIND_CODE_BUOY) { + return 16; + } + + // 정박 선박 (SOG ≤ 1): 항상 8px + const isMoving = Number(ship.sog) > SPEED_THRESHOLD; + if (!isMoving) { + return 8; + } + + // 운항 선박: 줌 레벨에 따른 크기 + if (zoom < 8) { + return 15; + } else if (zoom < 11) { + return 25; + } else if (zoom < 14) { + return 35; + } else { + return 40; + } +} + +/** + * 선박 IconLayer 생성 + * @param {Array} ships - 선박 데이터 배열 + * @param {number} zoom - 현재 줌 레벨 + * @returns {IconLayer} Deck.gl IconLayer + */ +export function createShipIconLayer(ships, zoom = 10, darkSignalIds = null) { + return new IconLayer({ + id: 'ship-icon-layer', + data: ships, + pickable: true, + iconAtlas: atlasImg, + iconMapping: ICON_ATLAS_MAPPING, + getPosition: (d) => [d.longitude, d.latitude], + getIcon: (d) => getShipIcon(d, darkSignalIds), + getAngle: (d) => getShipAngle(d), + getSize: (d) => getShipSize(d, zoom), + sizeScale: 1, + sizeMinPixels: 6, + sizeMaxPixels: 50, + }); +} + +// ===================== +// 속도벡터 LineLayer +// 참조: mda-react-front/src/common/deck.ts (라인 702-715) +// ===================== + +/** + * 속도벡터 데이터 생성 + * COG(방향)와 SOG(속도)를 사용하여 벡터 끝점 계산 + * 투영좌표(EPSG:3857)에서 계산 후 경위도로 변환 + * + * 벡터 길이 계산: + * - 최소 길이(VECTOR_MIN_LENGTH) + 속도 초과분 * 증가율(VECTOR_INCREMENT_SCALE) + * - SPEED_THRESHOLD(1kn) 기준으로 항해중 아이콘과 함께 표시 + * + * @param {Array} ships - 선박 데이터 배열 + * @returns {Array} 라인 데이터 [{ sourcePosition, targetPosition, color }] + */ +function buildSpeedVectorData(ships) { + return ships + .filter((ship) => Number(ship.sog) > SPEED_THRESHOLD) // 항해중 선박만 (아이콘 기준과 동일) + .map((ship) => { + const lng = Number(ship.longitude); + const lat = Number(ship.latitude); + const cog = Number(ship.cog) || 0; + const sog = Number(ship.sog) || 0; + + // 투영좌표로 변환 (EPSG:3857 - Web Mercator) + const coordinate = fromLonLat([lng, lat]); + const projX = coordinate[0]; + const projY = coordinate[1]; + + // 벡터 길이 계산: 최소 길이 + 속도 초과분에 비례한 추가 길이 + // 예: 1kn → 500m, 5kn → 660m, 10kn → 860m, 20kn → 1260m + const extraSpeed = Math.max(0, sog - SPEED_THRESHOLD); + const vectorLength = VECTOR_MIN_LENGTH + extraSpeed * VECTOR_INCREMENT_SCALE; + + // COG 방향으로 벡터 계산 + // sin(COG): 동-서 방향, cos(COG): 남-북 방향 + const xAdd = Math.sin(cog * DEG_TO_RAD) * vectorLength; + const yAdd = Math.cos(cog * DEG_TO_RAD) * vectorLength; + + // 경위도로 변환하여 반환 + return { + sourcePosition: toLonLat([projX, projY]), + targetPosition: toLonLat([projX + xAdd, projY + yAdd]), + color: [0, 0, 0, 200], // 검정색 + }; + }); +} + +/** + * 속도벡터 LineLayer 생성 + * @param {Array} ships - 선박 데이터 배열 + * @param {number} zoom - 현재 줌 레벨 + * @returns {LineLayer|null} Deck.gl LineLayer + */ +export function createSpeedVectorLayer(ships, zoom) { + // 줌 9 이상에서만 표시 + if (zoom < 9) { + return null; + } + + const vectorData = buildSpeedVectorData(ships); + if (vectorData.length === 0) { + return null; + } + + return new LineLayer({ + id: 'speed-vector-layer', + data: vectorData, + pickable: false, + getSourcePosition: (d) => d.sourcePosition, + getTargetPosition: (d) => d.targetPosition, + getColor: (d) => d.color, + getWidth: 2, + widthMinPixels: 1, + widthMaxPixels: 3, + }); +} + +// ===================== +// 선박크기 (DIM) 폴리곤 레이어 +// 참조: mda-react-front/src/util/realTimeLayerUtil.ts - calDimension() +// ===================== + +// DIM 폴리곤 최소 줌 레벨 (14레벨 이상에서 실제 크기로 렌더링) +const DIM_POLYGON_MIN_ZOOM = 14; + +/** + * 선박 크기 폴리곤 계산 (dimABCD 기준) + * 참조점(안테나 위치)을 기준으로 선박 형태 폴리곤 생성 + * + * dimA: 참조점 → 뱃머리(bow) 거리 + * dimB: 참조점 → 선미(stern) 거리 + * dimC: 참조점 → 좌현(port) 거리 + * dimD: 참조점 → 우현(starboard) 거리 + * + * @param {number} projX - 투영좌표 X (선박 위치) + * @param {number} projY - 투영좌표 Y (선박 위치) + * @param {number} dimA - 뱃머리 거리 (m) + * @param {number} dimB - 선미 거리 (m) + * @param {number} dimC - 좌현 거리 (m) + * @param {number} dimD - 우현 거리 (m) + * @param {number} angleS - sin(360 - COG) + * @param {number} angleC - cos(360 - COG) + * @returns {Array} 경위도 좌표 배열 (6점 폴리곤) + */ +function calDimension(projX, projY, dimA, dimB, dimC, dimD, angleS, angleC) { + // 좌상단 (좌현, 뱃머리 3/4) + let leftTopX = -1 * dimC; + let leftTopY = (dimA * 3) / 4; + let xNew = leftTopX * angleC - leftTopY * angleS; + let yNew = leftTopX * angleS + leftTopY * angleC; + leftTopX = xNew + projX; + leftTopY = yNew + projY; + + // 좌하단 (좌현, 선미) + let leftBottomX = -1 * dimC; + let leftBottomY = -1 * dimB; + xNew = leftBottomX * angleC - leftBottomY * angleS; + yNew = leftBottomX * angleS + leftBottomY * angleC; + leftBottomX = xNew + projX; + leftBottomY = yNew + projY; + + // 우하단 (우현, 선미) + let rightBottomX = dimD; + let rightBottomY = -1 * dimB; + xNew = rightBottomX * angleC - rightBottomY * angleS; + yNew = rightBottomX * angleS + rightBottomY * angleC; + rightBottomX = xNew + projX; + rightBottomY = yNew + projY; + + // 우상단 (우현, 뱃머리 3/4) + let rightTopX = dimD; + let rightTopY = (dimA * 3) / 4; + xNew = rightTopX * angleC - rightTopY * angleS; + yNew = rightTopX * angleS + rightTopY * angleC; + rightTopX = xNew + projX; + rightTopY = yNew + projY; + + // 선수 중앙 (뱃머리 끝) + let centerTopX = (dimD - dimC) / 2; + let centerTopY = dimA; + xNew = centerTopX * angleC - centerTopY * angleS; + yNew = centerTopX * angleS + centerTopY * angleC; + centerTopX = xNew + projX; + centerTopY = yNew + projY; + + // 6점 폴리곤 (시작점으로 복귀) + return [ + toLonLat([leftTopX, leftTopY]), + toLonLat([leftBottomX, leftBottomY]), + toLonLat([rightBottomX, rightBottomY]), + toLonLat([rightTopX, rightTopY]), + toLonLat([centerTopX, centerTopY]), + toLonLat([leftTopX, leftTopY]), + ]; +} + +/** + * 선박 크기 폴리곤 계산 (길이/너비만 있는 경우) + * 중심점 기준으로 사각형 + 선수 형태 생성 + * + * @param {number} projX - 투영좌표 X (선박 중심) + * @param {number} projY - 투영좌표 Y (선박 중심) + * @param {number} length - 선박 총 길이 (m) + * @param {number} width - 선박 총 너비 (m) + * @param {number} angleS - sin(360 - COG) + * @param {number} angleC - cos(360 - COG) + * @returns {Array} 경위도 좌표 배열 (6점 폴리곤) + */ +function calDimensionCentered(projX, projY, length, width, angleS, angleC) { + // 중심 기준이므로 dimA = dimB = length/2, dimC = dimD = width/2 + const dimA = length / 2; // 뱃머리 + const dimB = length / 2; // 선미 + const dimC = width / 2; // 좌현 + const dimD = width / 2; // 우현 + + return calDimension(projX, projY, dimA, dimB, dimC, dimD, angleS, angleC); +} + +/** + * 선박 DIM 폴리곤 데이터 생성 + * dimABCD 우선, 없으면 길이/너비로 중심 기준 폴리곤 생성 + * + * @param {Array} ships - 뷰포트 내 선박 배열 + * @returns {Array} 폴리곤 경로 데이터 + */ +function buildDimPolygonData(ships) { + const result = []; + + for (const ship of ships) { + const dimA = Number(ship.dimA) || 0; + const dimB = Number(ship.dimB) || 0; + const dimC = Number(ship.dimC) || 0; + const dimD = Number(ship.dimD) || 0; + + // 투영좌표 변환 + const coordinate = fromLonLat([Number(ship.longitude), Number(ship.latitude)]); + const projX = coordinate[0]; + const projY = coordinate[1]; + + // 회전 각도 계산 (COG 기준) + const cog = Number(ship.cog) || 0; + const angleS = Math.sin(((360 - cog) * Math.PI) / 180.0); + const angleC = Math.cos(((360 - cog) * Math.PI) / 180.0); + + let path = null; + + // 케이스 1: dimABCD가 모두 있는 경우 (참조점 기준) + if (dimA > 0) { + path = calDimension(projX, projY, dimA, dimB, dimC, dimD, angleS, angleC); + } + // 케이스 2: dimABCD가 없지만 총 길이/너비 계산 가능한 경우 (중심 기준) + else { + const totalLength = dimA + dimB; + const totalWidth = dimC + dimD; + + // 길이 또는 너비가 있어야 폴리곤 생성 + if (totalLength > 0 || totalWidth > 0) { + // 길이만 있는 경우 기본 너비 할당 (시각화용) + const effectiveLength = totalLength > 0 ? totalLength : 10; + const effectiveWidth = totalWidth > 0 ? totalWidth : Math.max(3, effectiveLength * 0.15); + + path = calDimensionCentered(projX, projY, effectiveLength, effectiveWidth, angleS, angleC); + } + } + + if (path) { + result.push({ + path, + featureId: ship.featureId, + }); + } + } + + return result; +} + +/** + * 선박크기 PathLayer 생성 + * 줌 레벨 11 이상에서만 렌더링 + * + * @param {Array} ships - 뷰포트 내 선박 배열 + * @param {number} zoom - 현재 줌 레벨 + * @returns {PathLayer|null} Deck.gl PathLayer + */ +export function createShipDimLayer(ships, zoom) { + // 줌 레벨 11 미만이면 null 반환 + if (zoom < DIM_POLYGON_MIN_ZOOM) { + return null; + } + + const dimData = buildDimPolygonData(ships); + + if (dimData.length === 0) { + return null; + } + + return new PathLayer({ + id: 'ship-dim-layer', + data: dimData, + pickable: false, + widthScale: 1, + widthMinPixels: 1, + widthMaxPixels: 3, + getPath: (d) => d.path, + getColor: [0, 100, 200, 180], // 파란색 반투명 + getWidth: 2, + jointRounded: true, + capRounded: true, + }); +} + +// ===================== +// 신호상태 SVG 캐싱 시스템 +// 참조: mda-react-front/src/util/realTimeLayerUtil.ts (라인 778-876) +// ===================== + +// SVG 캐시 맵 +const flagSvgCache = new Map(); + +/** + * 통합선박 여부 판별 + * @param {string} targetId - TARGET_ID + * @returns {boolean} 통합선박 여부 + */ +function isIntegratedShip(targetId) { + return targetId && targetId.includes('_'); +} + +/** + * 신호 상태 배열 생성 (캐시 키 + SVG 생성용) + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts + * + * 선박통합 ON + 통합선박 (TARGET_ID에 '_' 포함): + * - '1' = 장비 존재 + 활성 (activeColor) + * - '0' = 장비 존재 + 비활성 (inactiveColor/회색) + * - '' = 장비 없음 (표시 안함) + * + * 선박통합 OFF 또는 단독선박: + * - 현재 signalSourceCode에 해당하는 장비만 표시 (항상 활성 색상) + * + * @param {Object} ship - 선박 데이터 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @returns {Object} { key, flagArray } + */ +function buildFlagStateArray(ship, isIntegrate) { + const keyParts = []; + const flagArray = []; + + // 선박통합 ON이고 통합선박인 경우에만 통합 모드로 처리 + const useIntegratedMode = isIntegrate && isIntegratedShip(ship.targetId); + + for (const config of SIGNAL_FLAG_CONFIGS) { + let isVisible = false; + let isActive = false; + let color = config.inactiveColor; + + if (useIntegratedMode) { + // 통합선박 + 선박통합 ON: 장비 값 확인 + const dataValue = ship[config.dataKey]; + + // '' 또는 undefined → 장비 없음 (표시 안함) + // '0' → 장비 존재, 비활성 (회색) + // '1' → 장비 존재, 활성 (색상) + if (dataValue === '1') { + isVisible = true; + isActive = true; + color = config.activeColor; + } else if (dataValue === '0') { + isVisible = true; + isActive = false; + color = config.inactiveColor; + } + // dataValue가 '' 또는 undefined면 isVisible = false 유지 + } else { + // 선박통합 OFF 또는 단독선박: 현재 신호원만 표시 (항상 활성 색상) + if (config.signalSourceCode === ship.signalSourceCode) { + isVisible = true; + isActive = true; + color = config.activeColor; + } + } + + // 캐시 키: I(통합모드)/S(단독모드) + X(미표시)/0(비활성)/1(활성) + const modePrefix = useIntegratedMode ? 'I' : 'S'; + keyParts.push(modePrefix + (isVisible ? (isActive ? '1' : '0') : 'X')); + + flagArray.push({ + name: config.key, + color: color, + flag: isVisible ? (isActive ? '1' : '0') : '', + }); + } + + return { + key: keyParts.join('_'), + flagArray, + }; +} + +/** + * 신호 플래그 SVG 생성 + * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - createFlagLabelSVG() + * @param {Array} arr - 플래그 배열 + * @returns {string} SVG 문자열 + */ +function createFlagLabelSVG(arr) { + const filteredArr = arr.filter((v) => v.flag !== ''); + if (filteredArr.length === 0) { + return ''; + } + + const rectSize = 20; + const gap = 1.1; + const svgSize = 128; + const totalRectWidth = filteredArr.length * rectSize + (filteredArr.length - 1) * gap; + const offsetX = (svgSize - totalRectWidth) / 2; + let rects = ''; + let currentIndex = 0; + + for (let i = 0; i < arr.length; i++) { + if (arr[i].flag === '') { + continue; + } + const x = offsetX + currentIndex * (rectSize + gap); + rects += ``; + rects += `${arr[i].name}`; + currentIndex++; + } + + const svg = `${rects}`; + return svg.replace(/\s+/g, ' '); +} + +/** + * SVG를 Data URI로 변환 + * @param {string} svgStr - SVG 문자열 + * @returns {string} Data URI + */ +function svgToDataURI(svgStr) { + return `data:image/svg+xml;charset=utf8,${encodeURIComponent(svgStr)}`; +} + +/** + * 캐시된 신호 플래그 SVG 가져오기 + * @param {Object} ship - 선박 데이터 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @returns {string} SVG 문자열 + */ +function getCachedFlagSVG(ship, isIntegrate) { + const { key, flagArray } = buildFlagStateArray(ship, isIntegrate); + + let svg = flagSvgCache.get(key); + if (!svg) { + svg = createFlagLabelSVG(flagArray); + if (svg) { + flagSvgCache.set(key, svg); + } + } + + return svg || ''; +} + +/** + * SVG 캐시 초기화 + */ +export function clearFlagSvgCache() { + flagSvgCache.clear(); +} + +/** + * 신호상태 IconLayer 생성 (SVG 캐싱 사용) + * 참조: mda-react-front/src/common/deck.ts (라인 757-771) + * @param {Array} ships - 선박 데이터 배열 + * @param {number} zoom - 현재 줌 레벨 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @returns {IconLayer|null} Deck.gl IconLayer + */ +export function createSignalStatusLayer(ships, zoom, isIntegrate) { + if (zoom < 9) { + return null; + } + + // 신호 플래그 데이터 생성 (SVG 캐싱 적용) + const flagData = ships + .map((ship) => { + const svg = getCachedFlagSVG(ship, isIntegrate); + if (!svg) return null; + + return { + longitude: Number(ship.longitude), + latitude: Number(ship.latitude), + url: svg, + }; + }) + .filter((d) => d !== null); + + if (flagData.length === 0) { + return null; + } + + return new IconLayer({ + id: 'signal-status-layer', + data: flagData, + pickable: false, + sizeScale: 15, + getPixelOffset: [20, 20], // 선박명 옆, 아래 배치 + getPosition: (d) => [d.longitude, d.latitude], + getIcon: (d) => ({ + url: svgToDataURI(d.url), + width: 128, + height: 128, + anchorY: 64, // 중앙 정렬 + }), + getSize: 10, + }); +} + +// ===================== +// 선박명 TextLayer +// ===================== + +/** + * 선박 라벨 텍스트 생성 + * @param {Object} ship - 선박 데이터 + * @param {Object} labelOptions - 라벨 옵션 + * @returns {string} 라벨 텍스트 + */ +function buildLabelText(ship, labelOptions) { + const parts = []; + + // 선박명 + if (labelOptions.showShipName && ship.shipName) { + parts.push(ship.shipName); + } + + // 선박크기 (DIM) + if (labelOptions.showShipSize) { + const length = (Number(ship.dimA) || 0) + (Number(ship.dimB) || 0); + const width = (Number(ship.dimC) || 0) + (Number(ship.dimD) || 0); + if (length > 0 || width > 0) { + parts.push(`${length}m x ${width}m`); + } + } + + return parts.join('\n'); +} + +/** + * 선박명 TextLayer 생성 + * @param {Array} ships - 선박 데이터 배열 + * @param {number} zoom - 현재 줌 레벨 + * @param {Object} labelOptions - 라벨 옵션 + * @returns {TextLayer|null} Deck.gl TextLayer + */ +export function createShipLabelLayer(ships, zoom, labelOptions = { showShipName: true }) { + if (zoom < 9) { + return null; + } + + const labelData = ships + .map((ship) => ({ + ...ship, + labelText: buildLabelText(ship, labelOptions), + })) + .filter((ship) => ship.labelText); + + if (labelData.length === 0) { + return null; + } + + const fontSize = zoom < 11 ? 10 : zoom < 13 ? 12 : 14; + + return new TextLayer({ + id: 'ship-label-layer', + data: labelData, + pickable: false, + getPosition: (d) => [d.longitude, d.latitude], + getText: (d) => d.labelText, + getSize: fontSize, + getColor: [30, 30, 30, 255], + getAngle: 0, + getTextAnchor: 'start', + getAlignmentBaseline: 'center', + getPixelOffset: [20, 0], + fontFamily: 'NanumSquare, Malgun Gothic, 맑은 고딕, Arial, sans-serif', + fontWeight: 'bold', + characterSet: 'auto', + outlineWidth: 2, + outlineColor: [255, 255, 255, 255], + billboard: true, + }); +} + +/** + * 선택된 선박 하이라이트 레이어 (다중 선박 지원) + * @param {Array} selectedShips - 선택된 선박 데이터 배열 + * @returns {ScatterplotLayer|null} Deck.gl 레이어 + */ +export function createSelectedShipLayer(selectedShips) { + if (!selectedShips || selectedShips.length === 0) return null; + + return new ScatterplotLayer({ + id: 'selected-ship-layer', + data: selectedShips, + pickable: false, + stroked: true, + filled: false, + radiusScale: 1, + radiusMinPixels: 18, + radiusMaxPixels: 35, + lineWidthMinPixels: 2, + getPosition: (d) => [d.longitude, d.latitude], + getRadius: 25, + getLineColor: [255, 215, 0, 255], + getLineWidth: 3, + }); +} + +/** + * 모든 선박 레이어 생성 (통합) + * 참조: mda-react-front/src/common/deck.ts + * + * 렌더링 파이프라인: + * 1. 밀도 제한 적용 (배치 렌더러) → 아이콘 표시 대상 결정 + * 2. 아이콘 레이어 생성 (밀도 제한된 ships 사용) + * 3. 라벨 클러스터링 (밀도 제한된 ships 대상) → 라벨 표시 대상 결정 + * 4. 라벨/신호상태 레이어 생성 + * + * @param {Array} ships - 선박 데이터 (밀도 제한 적용됨, 아이콘 + 라벨 공통) + * @param {Object|null} selectedShip - 선택된 선박 + * @param {number} zoom - 현재 줌 레벨 + * @param {boolean} showLabels - 선명표시 여부 + * @param {Object} labelOptions - 선명표시 옵션 + * @param {boolean} isIntegrate - 선박통합 모드 여부 + * @param {number} renderTrigger - 렌더링 트리거 (배치 렌더러에서 증가) + * @returns {Array} Deck.gl 레이어 배열 + */ +export function createShipLayers(ships, selectedShips, zoom, showLabels = false, labelOptions = { showShipName: true }, isIntegrate = true, renderTrigger = 0, darkSignalIds = null) { + const layers = []; + + // 1. 선택된 선박 하이라이트 (다중) + const selectedLayer = createSelectedShipLayer(selectedShips); + if (selectedLayer) { + layers.push(selectedLayer); + } + + // 2. 선박 아이콘 레이어 (밀도 제한 적용된 전체 선박) + layers.push(createShipIconLayer(ships, zoom, darkSignalIds)); + + // 3. 선명표시 레이어들 (밀도 제한된 선박 대상 → 자체 클러스터링) + // 아이콘이 표시되는 선박에만 라벨/신호상태 표시 + if (showLabels) { + // 라벨 클러스터링 (밀도 제한된 ships 대상) + const clusteredShips = getClusteredShips(ships, zoom, isIntegrate, renderTrigger); + + // 3-1. 속도벡터 레이어 + if (labelOptions.showSpeedVector) { + const vectorLayer = createSpeedVectorLayer(clusteredShips, zoom); + if (vectorLayer) { + layers.push(vectorLayer); + } + } + + // 3-2. 선박명 레이어 + const labelLayer = createShipLabelLayer(clusteredShips, zoom, labelOptions); + if (labelLayer) { + layers.push(labelLayer); + } + + // 3-3. 신호상태 레이어 (별도 클러스터링) + if (labelOptions.showSignalStatus) { + // 밀도 제한된 ships 대상으로 신호상태 클러스터링 + const signalClusteredShips = getSignalClusteredShips(ships, zoom, isIntegrate, renderTrigger); + const signalLayer = createSignalStatusLayer(signalClusteredShips, zoom, isIntegrate); + if (signalLayer) { + layers.push(signalLayer); + } + } + + // 3-4. 선박크기 폴리곤 레이어 (줌 11 이상에서만) + // 뷰포트 내 선박만 대상으로 렌더링 (성능 최적화) + if (labelOptions.showShipSize) { + const dimLayer = createShipDimLayer(clusteredShips, zoom); + if (dimLayer) { + layers.push(dimLayer); + } + } + } + + return layers; +} + +export default createShipLayers; diff --git a/src/map/measure/measure.js b/src/map/measure/measure.js new file mode 100644 index 00000000..8c6ebae1 --- /dev/null +++ b/src/map/measure/measure.js @@ -0,0 +1,392 @@ +/** + * 측정 도구 핵심 로직 + * - MeasureSession: OL 객체 생명주기 관리 + * - 거리/면적/거리환 설정 함수 + * - 포맷 유틸리티 + * + * 참조: mda-react-front/src/components/nav/rightNav/measure.ts + */ +import VectorSource from 'ol/source/Vector'; +import VectorLayer from 'ol/layer/Vector'; +import { Draw } from 'ol/interaction'; +import { Overlay } from 'ol'; +import { createBox } from 'ol/interaction/Draw'; +import { unByKey } from 'ol/Observable'; +import { getArea, getLength } from 'ol/sphere'; +import { LineString } from 'ol/geom'; + +// ===================== +// MeasureSession 클래스 +// ===================== + +/** + * 측정 세션: 생성한 OL 객체(레이어, 인터랙션, 오버레이, 리스너)를 + * 직접 추적하고, dispose() 한 번으로 일괄 정리. + */ +export class MeasureSession { + constructor(map) { + this.map = map; + this._layer = null; + this._interactions = []; + this._overlays = []; + this._listeners = []; + } + + /** VectorLayer 생성+등록, source 반환 */ + createLayer() { + const source = new VectorSource({ wrapX: false }); + this._layer = new VectorLayer({ source, zIndex: 54 }); + this.map.addLayer(this._layer); + return source; + } + + /** Draw 인터랙션 등록+추적 */ + addInteraction(draw) { + this.map.addInteraction(draw); + this._interactions.push(draw); + return draw; + } + + /** 측정 툴팁 Overlay 생성+등록+추적 */ + createTooltip() { + const el = document.createElement('div'); + el.className = 'ol-tooltip ol-tooltip-measure'; + const overlay = new Overlay({ + element: el, + offset: [0, -15], + positioning: 'bottom-center', + }); + this.map.addOverlay(overlay); + this._overlays.push(overlay); + return overlay; + } + + /** 리스너 키 추적 (dispose 시 일괄 해제) */ + addListener(key) { + if (key) this._listeners.push(key); + return key; + } + + /** 모든 추적 객체 일괄 제거 */ + dispose() { + this._listeners.forEach((key) => unByKey(key)); + this._listeners = []; + + this._interactions.forEach((i) => this.map.removeInteraction(i)); + this._interactions = []; + + this._overlays.forEach((o) => this.map.removeOverlay(o)); + this._overlays = []; + + if (this._layer) { + this.map.removeLayer(this._layer); + this._layer = null; + } + } +} + +// ===================== +// 포맷 유틸리티 +// ===================== + +/** + * 거리 포맷: NM (km) + * @param {number} meters + * @returns {string} e.g. "5.2 NM (9.63 km)" + */ +export function formatDistance(meters) { + const nm = ((meters / 1000) * 0.5399568035).toFixed(1); + let sub; + if (meters > 1000) { + sub = (Math.round((meters / 1000) * 100) / 100) + ' km'; + } else { + sub = (Math.round(meters * 100) / 100) + ' m'; + } + return `${nm} NM (${sub})`; +} + +/** + * 면적 포맷: km² 또는 m² + * @param {number} sqMeters + * @returns {string} + */ +export function formatArea(sqMeters) { + if (sqMeters > 10000) { + return (Math.round((sqMeters / 1000000) * 100) / 100) + ' km\u00B2'; + } + return (Math.round(sqMeters * 100) / 100) + ' m\u00B2'; +} + +/** + * 각도 계산 (북쪽 기준 시계방향) + * @param {number[]} start - [x, y] 맵 좌표 + * @param {number[]} end - [x, y] 맵 좌표 + * @param {number} [cog=0] - 선박 COG (도) + * @returns {string} 각도 (0-360, 소수점 1자리) + */ +export function getCircleDegree(start, end, cog = 0) { + const x = Number(end[0]) - Number(start[0]); + const y = Number(end[1]) - Number(start[1]); + + const radian = Math.atan2(y, x) * (180 / Math.PI); + let angle = 360 - (radian - 90); + angle = (angle - cog) % 360; + if (angle < 0) angle += 360; + + return angle.toFixed(1); +} + +/** + * 선분별 거리 툴팁 관리자 + * 좌표 배열이 변경될 때마다 선분 개수에 맞춰 툴팁을 생성/업데이트/제거 + */ +class SegmentTooltips { + constructor(session) { + this.session = session; + this.tooltips = []; // Overlay 배열 + } + + /** + * 좌표 배열을 받아 각 선분 중점에 거리 툴팁 배치 + * @param {Array} coords - 좌표 배열 + */ + update(coords) { + const segCount = coords.length - 1; + + // 부족하면 툴팁 추가 생성 + while (this.tooltips.length < segCount) { + const el = document.createElement('div'); + el.className = 'ol-tooltip ol-tooltip-segment'; + const overlay = new Overlay({ + element: el, + offset: [0, -10], + positioning: 'bottom-center', + }); + this.session.map.addOverlay(overlay); + this.session._overlays.push(overlay); + this.tooltips.push(overlay); + } + + // 남으면 숨기기 + for (let i = segCount; i < this.tooltips.length; i++) { + this.tooltips[i].setPosition(undefined); + } + + // 각 선분 거리 계산 및 표시 + for (let i = 0; i < segCount; i++) { + const segLine = new LineString([coords[i], coords[i + 1]]); + const length = getLength(segLine); + const mid = [ + (coords[i][0] + coords[i + 1][0]) / 2, + (coords[i][1] + coords[i + 1][1]) / 2, + ]; + this.tooltips[i].getElement().innerHTML = formatDistance(length); + this.tooltips[i].setPosition(mid); + } + } + + /** 모든 선분 툴팁을 static 스타일로 고정 */ + finalize() { + this.tooltips.forEach((overlay) => { + const el = overlay.getElement(); + if (el && overlay.getPosition()) { + el.className = 'ol-tooltip ol-tooltip-segment-static'; + } + }); + } +} + +// ===================== +// 도구 설정 함수 +// ===================== + +/** + * 거리 측정 설정 (LineString) + * @param {MeasureSession} session + * @param {VectorSource} source + */ +export function setupDistanceMeasure(session, source) { + const draw = new Draw({ source, type: 'LineString' }); + session.addInteraction(draw); + + let currentTooltip = null; + let segTooltips = null; + + draw.on('drawstart', (evt) => { + const tooltip = session.createTooltip(); + currentTooltip = tooltip; + segTooltips = new SegmentTooltips(session); + const geom = evt.feature.getGeometry(); + + const key = geom.on('change', (e) => { + const coords = e.target.getCoordinates(); + const length = getLength(e.target); + tooltip.getElement().innerHTML = formatDistance(length); + tooltip.setPosition(e.target.getLastCoordinate()); + + // 선분별 거리 표시 (2개 이상 좌표일 때) + if (coords.length >= 2) { + segTooltips.update(coords); + } + }); + session.addListener(key); + }); + + draw.on('drawend', () => { + if (currentTooltip) { + const el = currentTooltip.getElement(); + el.className = 'ol-tooltip ol-tooltip-static'; + currentTooltip.setOffset([0, -7]); + } + if (segTooltips) { + segTooltips.finalize(); + } + }); +} + +/** + * 면적 측정 설정 (Polygon / Box / Circle) + * @param {MeasureSession} session + * @param {VectorSource} source + * @param {'Polygon'|'Box'|'Circle'} shape + */ +export function setupAreaMeasure(session, source, shape) { + // 메인 Draw 생성 + let draw; + if (shape === 'Box') { + draw = new Draw({ source, type: 'Circle', geometryFunction: createBox() }); + } else if (shape === 'Circle') { + draw = new Draw({ source, type: 'Circle' }); + } else { + draw = new Draw({ source, type: 'Polygon' }); + } + session.addInteraction(draw); + + // Circle인 경우 반경 표시용 Line Draw 추가 + let lineDraw = null; + let lineTooltip = null; + if (shape === 'Circle') { + lineTooltip = session.createTooltip(); + lineDraw = new Draw({ source, type: 'LineString' }); + session.addInteraction(lineDraw); + + lineDraw.on('drawstart', (evt) => { + session.map.addOverlay(lineTooltip); + const geom = evt.feature.getGeometry(); + + const key = geom.on('change', (e) => { + const length = getLength(e.target); + const area = length * length * Math.PI; + lineTooltip.getElement().innerHTML = formatArea(area); + lineTooltip.setPosition(e.target.getFirstCoordinate()); + }); + session.addListener(key); + }); + } + + let currentTooltip = null; + let segTooltips = null; + + draw.on('drawstart', (evt) => { + if (shape === 'Polygon' || shape === 'Box') { + currentTooltip = session.createTooltip(); + segTooltips = new SegmentTooltips(session); + } + + const geom = evt.feature.getGeometry(); + const key = geom.on('change', (e) => { + if (shape === 'Polygon' || shape === 'Box') { + const areaValue = getArea(e.target); + currentTooltip.getElement().innerHTML = formatArea(areaValue); + currentTooltip.setPosition(e.target.getInteriorPoint().getCoordinates()); + + // 선분별 거리 표시 + const coords = e.target.getCoordinates()[0]; // 외부 링 + if (coords && coords.length >= 2) { + segTooltips.update(coords); + } + } + }); + session.addListener(key); + }); + + draw.on('drawend', () => { + if (shape === 'Polygon' || shape === 'Box') { + if (currentTooltip) { + const el = currentTooltip.getElement(); + el.className = 'ol-tooltip ol-tooltip-static'; + currentTooltip.setOffset([0, -7]); + } + if (segTooltips) { + segTooltips.finalize(); + } + } + if (shape === 'Circle' && lineDraw) { + lineDraw.finishDrawing(); + } + }); +} + +/** + * 거리환 측정 설정 (Circle + Line 이중 Draw) + * 참조: mda-react-front measure.ts getCircleMeasureInteraction + * + * @param {MeasureSession} session + * @param {VectorSource} source + */ +export function setupRangeRingMeasure(session, source) { + // Line Draw (반경 거리 표시) + const lineTooltip = session.createTooltip(); + const lineDraw = new Draw({ source, type: 'LineString' }); + + lineDraw.on('drawstart', (evt) => { + session.map.addOverlay(lineTooltip); + const geom = evt.feature.getGeometry(); + + const key = geom.on('change', (e) => { + const length = getLength(e.target); + lineTooltip.getElement().innerHTML = formatDistance(length); + lineTooltip.setPosition(e.target.getLastCoordinate()); + }); + session.addListener(key); + }); + + // Circle Draw (각도 표시) + const circleDraw = new Draw({ source, type: 'Circle' }); + let circleTooltip = null; + let degree = '0.0'; + + circleDraw.on('drawstart', (evt) => { + circleTooltip = session.createTooltip(); + + const geom = evt.feature.getGeometry(); + const key = geom.on('change', () => { + // sketchCoords_: [center, edge] — OL Draw 내부 좌표 + const coords = evt.target.sketchCoords_; + if (coords && coords[0] && coords[1]) { + degree = getCircleDegree(coords[0], coords[1]); + } + circleTooltip.getElement().innerHTML = `각도: ${degree}°`; + circleTooltip.setPosition(geom.getCenter()); + }); + session.addListener(key); + }); + + circleDraw.on('drawend', () => { + lineDraw.finishDrawing(); + + if (circleTooltip) { + const el = circleTooltip.getElement(); + el.className = 'ol-tooltip ol-tooltip-static'; + // 최종 툴팁: 거리 + 각도 + el.innerHTML = lineTooltip.getElement().innerHTML + ` 각도: ${degree}°`; + circleTooltip.setOffset([0, -7]); + } + + session.map.removeOverlay(lineTooltip); + }); + + // circle → line 순서로 인터랙션 등록 (OL 이벤트 처리 순서) + session.addInteraction(circleDraw); + session.addInteraction(lineDraw); +} diff --git a/src/map/measure/measure.scss b/src/map/measure/measure.scss new file mode 100644 index 00000000..f6c280ee --- /dev/null +++ b/src/map/measure/measure.scss @@ -0,0 +1,41 @@ +/* 측정 툴팁 스타일 */ +/* 참조: mda-react-front/src/map/control.css */ + +.ol-tooltip { + position: relative; + padding: 0.4rem 0.8rem; + border-radius: 0.4rem; + font-size: 1.2rem; + white-space: nowrap; + pointer-events: none; +} + +.ol-tooltip-measure { + background: rgba(255, 237, 169, 0.85); + color: #333; + font-weight: 600; + border: 0.1rem solid rgba(200, 180, 100, 0.5); +} + +.ol-tooltip-static { + background: rgba(255, 237, 169, 0.85); + color: #333; + font-weight: 600; + border: 0.1rem solid rgba(200, 180, 100, 0.5); +} + +.ol-tooltip-segment { + background: rgba(255, 255, 255, 0.8); + color: #555; + font-size: 1.1rem; + font-weight: 500; + border: 0.1rem solid rgba(180, 180, 180, 0.6); +} + +.ol-tooltip-segment-static { + background: rgba(255, 255, 255, 0.75); + color: #666; + font-size: 1.1rem; + font-weight: 500; + border: 0.1rem solid rgba(180, 180, 180, 0.5); +} diff --git a/src/map/measure/useMeasure.js b/src/map/measure/useMeasure.js new file mode 100644 index 00000000..197812cc --- /dev/null +++ b/src/map/measure/useMeasure.js @@ -0,0 +1,72 @@ +/** + * 측정 도구 React 훅 + * - Zustand 상태(activeMeasureTool, areaShape) ↔ OL 인터랙션 연결 + * - ESC 키로 측정 취소 + * - 도구 전환 시 이전 세션 자동 정리 + */ +import { useEffect, useRef } from 'react'; +import { useMapStore } from '../../stores/mapStore'; +import { + MeasureSession, + setupDistanceMeasure, + setupAreaMeasure, + setupRangeRingMeasure, +} from './measure'; + +export default function useMeasure() { + const map = useMapStore((s) => s.map); + const activeTool = useMapStore((s) => s.activeMeasureTool); + const areaShape = useMapStore((s) => s.areaShape); + const clearMeasure = useMapStore((s) => s.clearMeasure); + const sessionRef = useRef(null); + + // ESC 키로 측정 취소 + useEffect(() => { + const handleKeyDown = (e) => { + if (e.key === 'Escape' && activeTool) { + clearMeasure(); + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [activeTool, clearMeasure]); + + // 도구 활성화/비활성화 + useEffect(() => { + // 이전 세션 정리 + if (sessionRef.current) { + sessionRef.current.dispose(); + sessionRef.current = null; + } + + if (!map || !activeTool) return; + + // 면적 도구: 도형 선택 전까지 대기 + if (activeTool === 'area' && !areaShape) return; + + const session = new MeasureSession(map); + const source = session.createLayer(); + sessionRef.current = session; + + switch (activeTool) { + case 'distance': + setupDistanceMeasure(session, source); + break; + case 'area': + setupAreaMeasure(session, source, areaShape); + break; + case 'rangeRing': + setupRangeRingMeasure(session, source); + break; + default: + break; + } + + return () => { + if (sessionRef.current) { + sessionRef.current.dispose(); + sessionRef.current = null; + } + }; + }, [map, activeTool, areaShape]); +} diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx new file mode 100644 index 00000000..3f6c6139 --- /dev/null +++ b/src/pages/HomePage.jsx @@ -0,0 +1,13 @@ +/** + * 홈 페이지 (메인 지도 화면) + * - 지도는 MainLayout에서 렌더링 + * - 여기서는 추가 UI 요소만 관리 + */ +export default function HomePage() { + return ( + <> + {/* 메인 페이지 추가 컨텐츠 */} + {/* 선박 정보 팝업, 검색 결과 등 */} + + ); +} diff --git a/src/publish/PublishRoutes.jsx b/src/publish/PublishRoutes.jsx new file mode 100644 index 00000000..f65a738c --- /dev/null +++ b/src/publish/PublishRoutes.jsx @@ -0,0 +1,134 @@ +import { Route } from 'react-router-dom'; + +// 퍼블리시 레이아웃 컴포넌트 +import WrapComponent from './layouts/WrapComponent'; +import HeaderComponent from './layouts/HeaderComponent'; +import SideComponent from './layouts/SideComponent'; +import MainComponent from './layouts/MainComponent'; + +// 퍼블리시 페이지 컴포넌트 +import Panel1Component from './pages/Panel1Component'; +import Panel2Component from './pages/Panel2Component'; +import Panel3Component from './pages/Panel3Component'; +import Panel4Component from './pages/Panel4Component'; +import Panel5Component from './pages/Panel5Component'; +import Panel6Component from './pages/Panel6Component'; +import Panel7Component from './pages/Panel7Component'; +import Panel8Component from './pages/Panel8Component'; + +/** + * 퍼블리시 라우트 정의 + * - /publish/* 하위에서 퍼블리시 파일들을 미리볼 수 있음 + */ +const PublishRoutes = ( + <> + {/* 기본 페이지 - 전체 레이아웃 미리보기 */} + } /> + + {/* 개별 패널 미리보기 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + {/* 전체 레이아웃 (원본 구조 그대로) */} + } /> + +); + +// 퍼블리시 홈 +function PublishHome() { + return ( +
    +

    퍼블리시 미리보기

    +

    좌측 메뉴에서 확인할 페이지를 선택하세요.

    +
    +

    폴더 구조

    +
    +{`src/publish/
    +├── _incoming/     # 새 퍼블리시 파일 (원본)
    +├── layouts/       # 레이아웃 컴포넌트
    +├── pages/         # 페이지 컴포넌트
    +└── components/    # 공통 컴포넌트`}
    +        
    +

    병합 방법

    +
      +
    1. 새 퍼블리시 파일을 _incoming/ 폴더에 복사
    2. +
    3. Claude에게 병합 요청
    4. +
    5. 변경사항 확인 후 적용
    6. +
    +
    +
    + ); +} + +// 패널 래퍼 컴포넌트들 +function Panel1Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel2Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel3Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel4Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel5Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel6Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel7Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +function Panel8Wrapper() { + return ( +
    + {}} /> +
    + ); +} + +export default PublishRoutes; diff --git a/src/publish/components/FileUpload.jsx b/src/publish/components/FileUpload.jsx new file mode 100644 index 00000000..f55348c6 --- /dev/null +++ b/src/publish/components/FileUpload.jsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; + +export default function FileUpload({ label = "파일 선택", inputId, maxLength = 25, placeholder = "선택된 파일 없음" }) { + const [fileName, setFileName] = useState(''); + + // 중간 생략 함수 + const truncateMiddle = (str, maxLen) => { + if (!str) return ''; + if (str.length <= maxLen) return str; + const keep = Math.floor((maxLen - 3) / 2); + return str.slice(0, keep) + '...' + str.slice(str.length - keep); + }; + + const handleChange = (e) => { + const name = e.target.files[0]?.name || ''; + setFileName(name); + }; + + return ( +
    + + + + {fileName ? truncateMiddle(fileName, maxLength) : placeholder} + +
    + ); +} diff --git a/src/publish/components/Slider.jsx b/src/publish/components/Slider.jsx new file mode 100644 index 00000000..a8d7e9b2 --- /dev/null +++ b/src/publish/components/Slider.jsx @@ -0,0 +1,24 @@ +import { useState } from "react"; + +function Slider({ label = "", min = 0, max = 100, defaultValue = 50 }) { + const [value, setValue] = useState(defaultValue); + + const percent = ((value - min) / (max - min)) * 100; + + return ( + + ); +} + +export default Slider; diff --git a/src/publish/layouts/HeaderComponent.jsx b/src/publish/layouts/HeaderComponent.jsx new file mode 100644 index 00000000..ad1626d0 --- /dev/null +++ b/src/publish/layouts/HeaderComponent.jsx @@ -0,0 +1,36 @@ + +import { Link } from "react-router-dom"; + +export default function HeaderComponent() { + return( + + ) +} \ No newline at end of file diff --git a/src/publish/layouts/MainComponent.jsx b/src/publish/layouts/MainComponent.jsx new file mode 100644 index 00000000..147476e0 --- /dev/null +++ b/src/publish/layouts/MainComponent.jsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom"; +import TopComponent from "../pages/TopComponent"; + +export default function MainComponent() { + return ( +
    + + +
    + ); +} diff --git a/src/publish/layouts/PublishLayout.jsx b/src/publish/layouts/PublishLayout.jsx new file mode 100644 index 00000000..a7d5adca --- /dev/null +++ b/src/publish/layouts/PublishLayout.jsx @@ -0,0 +1,57 @@ +import { Outlet, Link, useLocation } from 'react-router-dom'; + +/** + * 퍼블리시 레이아웃 + * - 퍼블리시 파일들을 미리보기 위한 레이아웃 + * - 상단에 네비게이션 제공 + */ +export default function PublishLayout() { + const location = useLocation(); + const currentPath = location.pathname; + + const menuItems = [ + { path: '/publish', label: '메인', exact: true }, + { path: '/publish/panel1', label: 'Panel1 (선박)' }, + { path: '/publish/panel2', label: 'Panel2 (위성)' }, + { path: '/publish/panel3', label: 'Panel3 (기상)' }, + { path: '/publish/panel4', label: 'Panel4 (분석)' }, + { path: '/publish/panel5', label: 'Panel5 (타임라인)' }, + { path: '/publish/panel6', label: 'Panel6 (AI모드)' }, + { path: '/publish/panel7', label: 'Panel7 (리플레이)' }, + { path: '/publish/panel8', label: 'Panel8 (항적조회)' }, + ]; + + const isActive = (path, exact) => { + if (exact) return currentPath === path; + return currentPath.startsWith(path); + }; + + return ( +
    + {/* 퍼블리시 네비게이션 */} + + + {/* 퍼블리시 콘텐츠 */} +
    + +
    +
    + ); +} diff --git a/src/publish/layouts/SideComponent.jsx b/src/publish/layouts/SideComponent.jsx new file mode 100644 index 00000000..40be2dba --- /dev/null +++ b/src/publish/layouts/SideComponent.jsx @@ -0,0 +1,94 @@ +import { useState } from "react"; +import { useNavigate, useLocation } from "react-router-dom"; + +import NavComponent from "../pages/NavComponent"; +import Panel1Component from "../pages/Panel1Component"; +import Panel2Component from "../pages/Panel2Component"; +import Panel3Component from "../pages/Panel3Component"; +import Panel4Component from "../pages/Panel4Component"; +import Panel5Component from "../pages/Panel5Component"; +import Panel6Component from "../pages/Panel6Component"; +import Panel7Component from "../pages/Panel7Component"; +import Panel8Component from "../pages/Panel8Component"; +import DisplayComponent from "../pages/DisplayComponent"; + +export default function SideComponent() { + const navigate = useNavigate(); + //const location = useLocation(); + + // 현재열린패널 + const [activePanel, setActivePanel] = useState("gnb1"); + + // 패널열린상태 + const [isPanelOpen, setIsPanelOpen] = useState(true); + const handleTogglePanel = () => setIsPanelOpen(prev => !prev); + + // Display 탭상태 + const [displayTab, setDisplayTab] = useState("filter"); + + /* ========================= + Nav 클릭 → 패널 + 라우팅 + ========================= */ + const handleChangePanel = (key) => { + setIsPanelOpen(true); + //setActivePanel(key); // navigate 없음 + + switch (key) { + case "gnb8": //항적조회 + setActivePanel("gnb8"); + navigate("/track"); + break; + + case "gnb7": // 리플레이 + setActivePanel("gnb7"); + navigate("/replay"); + break; + + case "filter": // 필터 + case "layer": // 레이어 + setActivePanel(key); + setDisplayTab(key); + + // 항적조회/리플레이에서 넘어올 경우 메인 초기화 + navigate("/main"); + break; + + default: + setActivePanel(key); + navigate("/main"); + break; + } + }; + + /* ========================= + 공통 props + ========================= */ + const panelProps = { + isOpen: isPanelOpen, + onToggle: handleTogglePanel, + }; + + return ( +
    + + +
    + {activePanel === "gnb1" && } + {activePanel === "gnb2" && } + {activePanel === "gnb3" && } + {activePanel === "gnb4" && } + {activePanel === "gnb5" && } + {activePanel === "gnb6" && } + {activePanel === "gnb7" && } + {activePanel === "gnb8" && } + {(activePanel === "filter" || activePanel === "layer") && ( + + )} +
    +
    + ); +} diff --git a/src/publish/layouts/ToolComponent.jsx b/src/publish/layouts/ToolComponent.jsx new file mode 100644 index 00000000..99ac99cf --- /dev/null +++ b/src/publish/layouts/ToolComponent.jsx @@ -0,0 +1,131 @@ +import { useState } from "react" +export default function ToolComponent() { + const [isLegendOpen, setIsLegendOpen] = useState(false); + + return( +
    + {/* 툴바 */} +
    +
      +
    • +
    • +
    • +
    +
      +
    • +
    • +
    • +
    +
      +
    • +
    • +
    +
    + {/* 맵컨트롤 툴바 */} +
    +
      +
    • +
    • 7
    • +
    • +
    +
      +
    • +
    • +
    • +
    +
    + {/* 범례 */} + {isLegendOpen && ( +
    +
      +
    • + 통합통합 + 0 +
    • +
    • + 중국어선중국어선 + 0 +
    • +
    • + 중국어선허가중국어선허가 + 0 +
    • +
    • + 일본어선일본어선 + 0 +
    • +
    • + 위험물위험물 + 0 +
    • +
    • + 여객선여객선 + 0 +
    • +
    • + 함정함정 + 0 +
    • +
    • + 함정-RADAR함정-RADAR + 0 +
    • +
    • + 일반일반 + 0 +
    • +
    • + VTS-일반VTS-일반 + 0 +
    • +
    • + VTS-RADARVTS-RADAR + 0 +
    • +
    • + VPASS일반VPASS일반 + 0 +
    • +
    • + ENAV어선ENAV어선 + 0 +
    • +
    • + ENAV위험물ENAV위험물 + 0 +
    • +
    • + ENAV화물선ENAV화물선 + 0 +
    • +
    • + ENAV관공선ENAV관공선 + 0 +
    • +
    • + ENAV일반ENAV일반 + 0 +
    • +
    • + D-MF/HFD-MF/HF + 0 +
    • +
    • + 항공기항공기 + 0 +
    • +
    • + NLLNLL + 0 +
    • +
    +
    + )} +
    + ) +} \ No newline at end of file diff --git a/src/publish/layouts/WrapComponent.jsx b/src/publish/layouts/WrapComponent.jsx new file mode 100644 index 00000000..f4030866 --- /dev/null +++ b/src/publish/layouts/WrapComponent.jsx @@ -0,0 +1,15 @@ +import { Outlet } from "react-router-dom"; +import HeaderComponent from "./HeaderComponent"; +import SideComponent from "./SideComponent"; +import ToolComponent from "./ToolComponent"; + +export default function WrapComponent() { + return ( +
    + + + {/* Main 영역 */} + +
    + ); +} diff --git a/src/publish/pages/Analysis1Component.jsx b/src/publish/pages/Analysis1Component.jsx new file mode 100644 index 00000000..9c09260b --- /dev/null +++ b/src/publish/pages/Analysis1Component.jsx @@ -0,0 +1,48 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Analysis1Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 위성 영상 등록 팝업 */} +
    +
    +
    + 관심 해역 설정 +
    + +
    +
    + + + +
    +
    + +
    +
    + +
    + ); +} diff --git a/src/publish/pages/Analysis2Component.jsx b/src/publish/pages/Analysis2Component.jsx new file mode 100644 index 00000000..b0684528 --- /dev/null +++ b/src/publish/pages/Analysis2Component.jsx @@ -0,0 +1,129 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Analysis2Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 위성 영상 등록 팝업 */} +
    +
    +
    + 관심 해역 설정 +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    관심 해역 설정 - 해상영역명, 설정 옵션, 좌표,영역 옵션,해상영역명 크기, 해상영역명 색상,윤곽선 굵기,윤곽선 종류,윤곽선 색상,채우기 색상 에 대한 내용을 등록하는 표입니다.
    해상영역명
    설정 옵션 +
    + + + +
    +
    좌표[124,96891368166156, 36.37855817450263]
    + [125,25105622872591, 36.37855817450263]
    + [125,25105622872591, 36.37855817450263]
    + [125,25105622872591, 36.37855817450263]
    + [125,25105622872591, 36.37855817450263] +
    영역 옵션 +
    + + +
    +
    해상영역명 크기 +
    + +
    + + +
    +
    +
    해상영역명 색상
    윤곽선 굵기 +
    + +
    + + +
    +
    +
    윤곽선 종류 + +
    윤곽선 색상 채우기 색상
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/src/publish/pages/Analysis3Component.jsx b/src/publish/pages/Analysis3Component.jsx new file mode 100644 index 00000000..0ccc457a --- /dev/null +++ b/src/publish/pages/Analysis3Component.jsx @@ -0,0 +1,198 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Analysis3Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 위성 영상 등록 팝업 */} +
    +
    +
    + 관심 해역 분석 등록 +
    + +
    + +
    + {/* 지도캡쳐/테이블 영역 */} +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    관심 해역 분석 등록 - 제목, 상세 내역, 공유 여부,공유 그룹 에 대한 내용을 등록하는 표입니다.
    제목
    상세 내역 + +
    공유 여부 +
    + + +
    +
    공유 그룹 + +
    +
    + {/* 관심영역 체크박스 목록 -스크롤됨 */} +
    +
    관심영역 목록
    +
      +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    • + + +
    • +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/src/publish/pages/Analysis4Component.jsx b/src/publish/pages/Analysis4Component.jsx new file mode 100644 index 00000000..efdc3c4d --- /dev/null +++ b/src/publish/pages/Analysis4Component.jsx @@ -0,0 +1,235 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Analysis4Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 위성 영상 등록 팝업 */} +
    +
    +
    +
    + 350 대해구도 + 조회시간: 2026-07-00 17:15:13 +
    + +
    + +
    + +
    + {/* 지도캡쳐/테이블 영역 */} +
    +
    통항 선박
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    통항 선박 - 선박 종류, 승선원, 위험물 운반, 공유 여부 및 그룹 에 대한 표입니다
    카고(척)0
    카고 승성원(명)-
    탱커수(척)
    탱커 승선원(명)
    위험물 운반석(척)
    위험물 운반선 승선원(명)
    위험물 양(톤)
    어선(척)
    어선 승선원(명)
    기타 어선(척)
    기타 어선 승선원(명)
    여객선(척)
    유도선(척)
    유도선 승선원(명)
    기타 선박(척)
    기타 선박 승선원(명)
    함정수(척)
    +
    + {/* 관심영역 체크박스 목록 -스크롤됨 */} +
    +
    신호별
    + + + + + + + + + + + + + + + + + + + + + + + + +
    신호별 - 제목, 상세 내역, 공유 여부,공유 그룹 에 대한 내용을 등록하는 표입니다.
    AIS0
    V-PASS-
    VHF
    MFHF
    + + +
    E-NAV
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    E-NAV - 여객선, 어선, 카고, 관공선, 기타 선박과 공유 정보 에 대한 표입니다.
    E-NAV 여객선(척)0
    E-NAV 어선(척)-
    E-NAV 카고(척)
    E-NAV 관공선(척)
    E-NAV 기타(척)
    + + +
    기상정보
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    기상정보 - 유향, 유속, 유의 파고, 파향, 파주기, 풍속, 풍향 을 나타내는 표입니다
    유향0
    유속-
    유의 파고0.5(m)
    파향 +
    + 파향 + 350(°) +
    +
    파주기3.7(s)
    풍속9.2(m/s)
    풍향 +
    + 풍향 + 45(°) +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + ); +} diff --git a/src/publish/pages/DisplayComponent.jsx b/src/publish/pages/DisplayComponent.jsx new file mode 100644 index 00000000..bf57d9e0 --- /dev/null +++ b/src/publish/pages/DisplayComponent.jsx @@ -0,0 +1,355 @@ +import { useEffect, useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import Slider from '../components/Slider'; + +export default function DisplayComponent({ isOpen, onToggle, activeTab: externalTab }) { + const navigate = useNavigate(); + + // 투명도 + const [opacity, setOpacity] = useState(70); + + // 아코디언 + const [isAccordionOpen1, setIsAccordionOpen1] = useState(true); // 기존 + const [isAccordionOpen2, setIsAccordionOpen2] = useState(true); // + const [isAccordionOpen3, setIsAccordionOpen3] = useState(true); // + const [isAccordionOpen4, setIsAccordionOpen4] = useState(false); // + + const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev); + const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev); + const toggleAccordion3 = () => setIsAccordionOpen3(prev => !prev); + const toggleAccordion4 = () => setIsAccordionOpen4(prev => !prev); + + // 탭이동 + const [activeTab, setActiveTab] = useState('filter'); + + useEffect(() => { + if (externalTab) { + setActiveTab(externalTab); + } + }, [externalTab]); + + const tabs = [ + { id: 'filter', label: '필터' }, + { id: 'layer', label: '레이어' }, + ]; + return ( + + ); +} diff --git a/src/publish/pages/EmptyMain.jsx b/src/publish/pages/EmptyMain.jsx new file mode 100644 index 00000000..2550b09a --- /dev/null +++ b/src/publish/pages/EmptyMain.jsx @@ -0,0 +1,4 @@ + +export default function EmptyMain() { + return null; // 또는 지도만 보여주는 영역 +} diff --git a/src/publish/pages/LayerComponent.jsx b/src/publish/pages/LayerComponent.jsx new file mode 100644 index 00000000..1117ff3f --- /dev/null +++ b/src/publish/pages/LayerComponent.jsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; +import FileUpload from '../components/FileUpload'; + +export default function LayerComponent() { + const navigate = useNavigate(); + + return ( +
    + + {/* 레이어등록 팝업 */} +
    +
    +
    + 레이어 등록 +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    레이어등록 - 레이어명, 첨부파일, 공유설정 에 대한 내용을 나타내는 표입니다.
    레이어명 *
    첨부파일 * +
    + + geojson 파일을 첨부해 주세요. +
    +
    공유설정 +
    + + + +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + ); +} diff --git a/src/publish/pages/MyPageComponent.jsx b/src/publish/pages/MyPageComponent.jsx new file mode 100644 index 00000000..c8824c42 --- /dev/null +++ b/src/publish/pages/MyPageComponent.jsx @@ -0,0 +1,208 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function MyPageComponent() { + const navigate = useNavigate(); + + // 서브 팝업 상태 + // null | "password" | "cert" + const [subPopup, setSubPopup] = useState(null); + + return ( +
    + + {/* 내 정보 조회 */} +
    +
    +
    + 내 정보 조회 +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + 내 정보 조회 - 아이디, 비밀번호, 이름, 이메일, 직급, 상세소속, 공인인증서 삭제 +
    아이디admin222
    비밀번호 + +
    이름ADMIN
    이메일123@korea.kr
    직급경감
    상세소속
    공인인증서 삭제 + +
    +
    + +
    +
    + + +
    +
    +
    +
    + + {/* 딤 + 서브 팝업 */} + {subPopup && ( +
    + + {/* 비밀번호 변경 */} + {subPopup === "password" && ( +
    +
    + 비밀번호 수정 +
    + +
    + + + + + + + + + + + + + + + + + + + + +
    + 비밀번호 수정 - 현재 비밀번호, 새 비밀번호, 새 비밀번호 확인 +
    현재 비밀번호 + +
    새 비밀번호 + +
    새 비밀번호 확인 + +
    +
    + +
    +
    + + +
    +
    +
    + )} + + {/* 공인인증서 삭제 */} + {subPopup === "cert" && ( +
    +
    + 공인인증서 삭제 +
    + +
    +
    + 공인인증서를 삭제 하시겠습니까? +
    +
    + +
    +
    + + +
    +
    +
    + )} + +
    + )} +
    + ); +} diff --git a/src/publish/pages/NavComponent.jsx b/src/publish/pages/NavComponent.jsx new file mode 100644 index 00000000..bf44b8d7 --- /dev/null +++ b/src/publish/pages/NavComponent.jsx @@ -0,0 +1,71 @@ +export default function NavComponent({ activeKey, onChange }) { + const gnbList = [ + { key: 'gnb1', class: 'gnb1', label: '선박' }, + { key: 'gnb2', class: 'gnb2', label: '위성' }, + { key: 'gnb3', class: 'gnb3', label: '기상' }, + { key: 'gnb4', class: 'gnb4', label: '분석' }, + { key: 'gnb5', class: 'gnb5', label: '타임라인' }, + { key: 'gnb6', class: 'gnb6', label: 'AI모드' }, + { key: 'gnb7', class: 'gnb7', label: '리플레이' }, + { key: 'gnb8', class: 'gnb8', label: '항적조회' }, + ]; + + const sideList = [ + { key: 'filter', class: 'filter', label: '필터' }, + { key: 'layer', class: 'layer', label: '레이어' }, + ]; + + return( + + ) +} diff --git a/src/publish/pages/Panel1Component.jsx b/src/publish/pages/Panel1Component.jsx new file mode 100644 index 00000000..8912f09c --- /dev/null +++ b/src/publish/pages/Panel1Component.jsx @@ -0,0 +1,727 @@ +import { useState, useEffect } from 'react'; +import { Link } from "react-router-dom"; +import Panel1DetailComponent from './Panel1DetailComponent'; + +export default function Panel1Component({ isOpen, onToggle }) { + // 내부 뷰 상태 + const [view, setView] = useState('list'); // list | detail + + // 아코디언 + const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); // 기존 + const [isAccordionOpen2, setIsAccordionOpen2] = useState(false); // 새 아코디언 + + const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev); + const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev); + + // 탭이동 + const [activeTab, setActiveTab] = useState('ship01'); + + const tabs = [ + { id: 'ship01', label: '선박검색' }, + { id: 'ship02', label: '허가선박' }, + { id: 'ship03', label: '제재단속' }, + { id: 'ship04', label: '침몰선박' }, + { id: 'ship05', label: '선박입출항' }, + { id: 'ship06', label: '관심선박' } + ]; + return ( + + ); +} diff --git a/src/publish/pages/Panel1DetailComponent.jsx b/src/publish/pages/Panel1DetailComponent.jsx new file mode 100644 index 00000000..f1c74977 --- /dev/null +++ b/src/publish/pages/Panel1DetailComponent.jsx @@ -0,0 +1,112 @@ +import { useState, useEffect } from 'react'; +import { Link } from "react-router-dom"; + +export default function Panel1DetailComponent({ isOpen, onToggle, onBack }) { + + // 탭이동 + const [activeTab, setActiveTab] = useState('ship02'); + + const tabs = [ + { id: 'ship01', label: '선박검색' }, + { id: 'ship02', label: '허가선박' }, + { id: 'ship03', label: '제재단속' }, + { id: 'ship04', label: '침몰선박' }, + { id: 'ship05', label: '선박입출항' }, + { id: 'ship06', label: '관심선박' } + ]; + return ( + <> + {/* */} + + ); +} diff --git a/src/publish/pages/Panel2Component.jsx b/src/publish/pages/Panel2Component.jsx new file mode 100644 index 00000000..39ae3930 --- /dev/null +++ b/src/publish/pages/Panel2Component.jsx @@ -0,0 +1,420 @@ +import { useState } from 'react'; +import { Link, useNavigate } from "react-router-dom"; +import Slider from '../components/Slider'; + +export default function Panel2Component({ isOpen, onToggle }) { + const navigate = useNavigate(); + + // 아코디언 + const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); // 기존 + const [isAccordionOpen2, setIsAccordionOpen2] = useState(false); // 새 아코디언 + + const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev); + const toggleAccordion2 = () => setIsAccordionOpen2(prev => !prev); + + // 탭이동 + const [activeTab, setActiveTab] = useState('ship01'); + + const tabs = [ + { id: 'ship01', label: '위성영상 관리' }, + { id: 'ship02', label: '위성사업자 관리' }, + { id: 'ship03', label: '위성 관리' }, + ]; + return ( + + ); +} diff --git a/src/publish/pages/Panel3Component.jsx b/src/publish/pages/Panel3Component.jsx new file mode 100644 index 00000000..55780771 --- /dev/null +++ b/src/publish/pages/Panel3Component.jsx @@ -0,0 +1,322 @@ +import { useState } from 'react'; +import { Link } from "react-router-dom"; +export default function Panel3Component({ isOpen, onToggle }) { + + // 탭이동 + const [activeTab, setActiveTab] = useState('weather01'); + + const tabs = [ + { id: 'weather01', label: '기상특보' }, + { id: 'weather02', label: '태풍정보' }, + { id: 'weather03', label: '조위관측' }, + { id: 'weather04', label: '조석정보' }, + { id: 'weather05', label: '항공기상' }, + ]; + return ( + + ); +} diff --git a/src/publish/pages/Panel4Component.jsx b/src/publish/pages/Panel4Component.jsx new file mode 100644 index 00000000..1d5bfec2 --- /dev/null +++ b/src/publish/pages/Panel4Component.jsx @@ -0,0 +1,521 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; +export default function Panel4Component({ isOpen, onToggle }) { + const navigate = useNavigate(); + + // 아코디언 + const [isAccordionOpen1, setIsAccordionOpen1] = useState(false); // 기존 + const toggleAccordion1 = () => setIsAccordionOpen1(prev => !prev); + + // 탭이동 + const [activeTab, setActiveTab] = useState('analysis01'); + + const tabs = [ + { id: 'analysis01', label: '관심 해역' }, + { id: 'analysis02', label: '해역 분석' }, + { id: 'analysis03', label: '해역 진입 선박' }, + { id: 'analysis04', label: '해구 분석' }, + ]; + return ( + + ); +} diff --git a/src/publish/pages/Panel5Component.jsx b/src/publish/pages/Panel5Component.jsx new file mode 100644 index 00000000..a469706d --- /dev/null +++ b/src/publish/pages/Panel5Component.jsx @@ -0,0 +1,7 @@ +import { useState } from "react"; +export default function Panel5Component() { + + return ( +
    + ); +} diff --git a/src/publish/pages/Panel6Component.jsx b/src/publish/pages/Panel6Component.jsx new file mode 100644 index 00000000..513549d1 --- /dev/null +++ b/src/publish/pages/Panel6Component.jsx @@ -0,0 +1,77 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +export default function Panel6Component({ isOpen, onToggle }) { + + return ( + + ); +} diff --git a/src/publish/pages/Panel7Component.jsx b/src/publish/pages/Panel7Component.jsx new file mode 100644 index 00000000..3389c12a --- /dev/null +++ b/src/publish/pages/Panel7Component.jsx @@ -0,0 +1,7 @@ +import { useState } from "react"; +export default function Panel7Component() { + + return ( +
    + ); +} diff --git a/src/publish/pages/Panel8Component.jsx b/src/publish/pages/Panel8Component.jsx new file mode 100644 index 00000000..868a706e --- /dev/null +++ b/src/publish/pages/Panel8Component.jsx @@ -0,0 +1,7 @@ +import { useState } from "react"; +export default function Panel8Component() { + + return ( +
    + ); +} diff --git a/src/publish/pages/ReplayComponent.jsx b/src/publish/pages/ReplayComponent.jsx new file mode 100644 index 00000000..7f734dcd --- /dev/null +++ b/src/publish/pages/ReplayComponent.jsx @@ -0,0 +1,100 @@ + +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function ReplayComponent() { + const navigate = useNavigate(); + + const max = 100; + const [value, setValue] = useState(30); + + const percent = (value / max) * 100; + + return( +
    + +
    + + +
    + {/* 재생상태 컨트롤 */} +
    +
    + + +
    + +
    + setValue(Number(e.target.value))} + style={{ + background: `linear-gradient( + to right, + #FF0000 0%, + #FF0000 ${percent}%, + #D7DBEC ${percent}%, + #D7DBEC 100% + )` + }} + /> +
    + 2023-08-20  10:15:30 +
    +
    + +
    + + +
    + +
    + {/* 재생옵션 영역 */} +
    + + +
    + +
    +
    + +
    + ) +} \ No newline at end of file diff --git a/src/publish/pages/Satellite1Component.jsx b/src/publish/pages/Satellite1Component.jsx new file mode 100644 index 00000000..d9ad226a --- /dev/null +++ b/src/publish/pages/Satellite1Component.jsx @@ -0,0 +1,189 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; +import FileUpload from '../components/FileUpload'; + +export default function Satellite1Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 위성 영상 등록 팝업 */} +
    +
    +
    + 위성 영상 등록 +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    위성 영상 등록 - 사업자명/위성명, 영상 촬영일, 위성영상파일,CSV 파일,위성영상명, 영상전송 주기,영상 종류,위성 궤도,영상 출처,촬영 목적,촬영 모드,취득방법,구매가격, 에 대한 내용을 등록하는 표입니다.
    사업자명/위성명 * +
    + + +
    +
    영상 촬영일 *
    위성영상파일 * +
    + +
    +
    CSV 파일 * +
    + +
    +
    위성영상명 *
    영상전송 주기 + +
    영상 종류 +
    + + + + + +
    +
    위성 궤도 + + 영상 출처 + +
    촬영 목적 + + 촬영 모드 + +
    취득방법 + + 구매가격 +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + ); +} diff --git a/src/publish/pages/Satellite2Component.jsx b/src/publish/pages/Satellite2Component.jsx new file mode 100644 index 00000000..5d604e62 --- /dev/null +++ b/src/publish/pages/Satellite2Component.jsx @@ -0,0 +1,87 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Satellite2Component() { + const navigate = useNavigate(); + return ( +
    + + {/* 위성 사업자 등록 팝업 */} +
    +
    +
    + 위성 사업자 등록 +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    위성 사업자 등록 - 사업자 분류, 사업자명, 국가, 소재지, 상세내역 에 대한 내용을 등록하는 표입니다.
    사업자 분류 * + +
    사업자명
    국가 * + +
    소재지
    상세내역
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + ); +} diff --git a/src/publish/pages/Satellite3Component.jsx b/src/publish/pages/Satellite3Component.jsx new file mode 100644 index 00000000..f24eec73 --- /dev/null +++ b/src/publish/pages/Satellite3Component.jsx @@ -0,0 +1,94 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Satellite3Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 위성 관리 등록 팝업 */} +
    +
    +
    + 위성 관리 등록 +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    위성 관리 등록 - 사업자명, 위성명, 센서 타입, 촬영 해상도, 주파수, 상세내역 에 대한 내용을 등록하는 표입니다.
    사업자명 * + +
    위성명 *
    센서 타입 + +
    촬영 해상도
    주파수
    상세내역
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + ); +} diff --git a/src/publish/pages/Satellite4Component.jsx b/src/publish/pages/Satellite4Component.jsx new file mode 100644 index 00000000..05c705a7 --- /dev/null +++ b/src/publish/pages/Satellite4Component.jsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Satellite4Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 삭제 팝업 */} +
    +
    +
    + 삭제 +
    + +
    +
    삭제 하시겠습니까?
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + ); +} diff --git a/src/publish/pages/ShipComponent.jsx b/src/publish/pages/ShipComponent.jsx new file mode 100644 index 00000000..bc54d6fa --- /dev/null +++ b/src/publish/pages/ShipComponent.jsx @@ -0,0 +1,169 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function ShipComponent() { + const navigate = useNavigate(); + + //progress bar value 선언 + const [value, setValue] = useState(60); + + // 갤러리 이미지 + const images = [ + { src: "/images/photo_ship_001.png", alt: "1511함A-05" }, + { src: "/images/photo_ship_002.png", alt: "1511함A-05" }, + ]; + + const [currentIndex, setCurrentIndex] = useState(0); + + const handlePrev = () => { + if (currentIndex === 0) return; + setCurrentIndex(prev => prev - 1); + }; + + const handleNext = () => { + if (currentIndex === images.length - 1) return; + setCurrentIndex(prev => prev + 1); + }; + + return( +
    + + {/* 배정보 팝업 */} +
    + {/* header */} +
    +
    + + 대한민국 + 1511함A-05 + 13450135 +
    +
    + +
    + + + + {/* 이미지 영역 */} +
    + {images[currentIndex].alt} +
    +
    + {/* body */} +
    +
    +
    + +
      +
    • A
    • +
    • V
    • +
    • E
    • +
    • T
    • +
    • D
    • +
    • R
    • +
    +
    + +
    + +
    +
    + {value}% + +
    +
    + +
      +
    • +
      + 출항지 + 서귀포해양경찰서 +
      +
      + 입항지 + 하태도 +
      +
    • +
    • +
      + 출항일시 + 2024-11-23 11:23:00 +
      +
      + 입항일시 + 2024-11-23 11:23:00 +
      +
    • +
    • +
      + 선박상태 + 정박 +
      +
      + 속도/항로 + 4.2 kn / 13.3˚ +
      +
      + 흘수 + 1.1m +
      +
    • +
    + + {/*
      +
    • + AIS + 정상 +
    • +
    • + RF + 정상 +
    • +
    • + EO + 정상 +
    • +
    • + SAR + 비활성 +
    • +
    */} +
    + + +
    +
    + {/* footer */} +
    데이터 수신시간 : 2024-11-23 11:23:00
    +
    + +
    + ) +} \ No newline at end of file diff --git a/src/publish/pages/Signal1Component.jsx b/src/publish/pages/Signal1Component.jsx new file mode 100644 index 00000000..f64b90fa --- /dev/null +++ b/src/publish/pages/Signal1Component.jsx @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Signal1Component() { + const navigate = useNavigate(); + + return ( +
    + + {/* 신호설정 팝업 */} +
    +
    +
    + 신호설정 +
    + +
    + + + + + + + + + + + + + + + + +
    신호설정 - 신호표출반경, 수신수기 설정 에 대한 내용을 나타내는 표입니다.
    신호표출반경 + +
    수신수기 설정
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + ); +} diff --git a/src/publish/pages/Signal2Component.jsx b/src/publish/pages/Signal2Component.jsx new file mode 100644 index 00000000..99f4e694 --- /dev/null +++ b/src/publish/pages/Signal2Component.jsx @@ -0,0 +1,149 @@ +import { useState } from 'react'; +import { useNavigate } from "react-router-dom"; + +export default function Signal2Component() { + const navigate = useNavigate(); + + // 아코디언 상태 (3개, 초기 모두 열림) + const [accordionOpen, setAccordionOpen] = useState({ + signal1: true, + signal2: true, + signal3: true, + }); + + const toggleAccordion = (key) => { + setAccordionOpen((prev) => ({ ...prev, [key]: !prev[key] })); + }; + + return ( +
    + + {/* 신호설정 팝업 */} +
    +
    +
    + 맞춤 설정 +
    + +
    + {/* 아코디언그룹 01 */} +
    +
    + NLL 고속 선박 탐지 + +
    + {/* 여기서부터 아코디언 */} +
    +
      +
    • + + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    + {/* 여기까지 */} +
    + + {/* 아코디언그룹 02 */} +
    +
    + 특정 어업수역 탐지 + +
    + {/* 여기서부터 아코디언 */} +
    +
      +
    • + + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    • + +
    • +
    +
    + {/* 여기까지 */} +
    + + {/* 아코디언그룹 03 */} +
    +
    + 위험화물 식별 + +
    + {/* 여기서부터 아코디언 */} +
    +
      +
    • + + +
    • +
    +
    + {/* 여기까지 */} +
    +
    + +
    +
    + + +
    +
    +
    +
    + +
    + ); +} diff --git a/src/publish/pages/ToastComponent.jsx b/src/publish/pages/ToastComponent.jsx new file mode 100644 index 00000000..1748fe4b --- /dev/null +++ b/src/publish/pages/ToastComponent.jsx @@ -0,0 +1,59 @@ +import { Link } from "react-router-dom"; + +export default function ToastComponent() { + return( +
    + + {/* 지도상 배표식 */} +
    +
    + + 1511함A-05 + 12.5 kts | 45° + +
    + +
    + + 1511함A-05 + 12.5 kts | 45° + +
    + +
    + + 1511함A-05 + 12.5 kts | 45° + +
    +
    + + {/* 토스트팝업 */} +
    +
    + 104 어업구역 비인가 선박 + + + + +
    + +
    + 104 어업구역 비인가 선박 + + + + +
    + +
    + 저속 이동 의심 선박 + + + + +
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/publish/pages/TopComponent.jsx b/src/publish/pages/TopComponent.jsx new file mode 100644 index 00000000..f1ffa98a --- /dev/null +++ b/src/publish/pages/TopComponent.jsx @@ -0,0 +1,21 @@ +export default function TopComponent() { + return( +
    +
    +
      +
    • +
    • 경도129° 38’31.071”E
    • +
    • 위도35° 21’24.580”N
    • +
    • KST2024-07-01(화) 12:00:00
    • +
    • +
    • +
    +
    + +
    + + +
    +
    + ) +} \ No newline at end of file diff --git a/src/publish/pages/TrackComponent.jsx b/src/publish/pages/TrackComponent.jsx new file mode 100644 index 00000000..9216aa70 --- /dev/null +++ b/src/publish/pages/TrackComponent.jsx @@ -0,0 +1,41 @@ + +import { useNavigate } from "react-router-dom"; + +export default function TrackComponent() { + const navigate = useNavigate(); + + return( +
    + +
    + + + {/* 항적조회 검색바 */} +
    + + + +
    + +
    + +
    + ) +} \ No newline at end of file diff --git a/src/publish/pages/WeatherComponent.jsx b/src/publish/pages/WeatherComponent.jsx new file mode 100644 index 00000000..eb664225 --- /dev/null +++ b/src/publish/pages/WeatherComponent.jsx @@ -0,0 +1,71 @@ +import { useNavigate } from "react-router-dom"; + +export default function WeatherComponent() { + const navigate = useNavigate(); + + return( +
    + +
    + {/* header */} +
    +
    + 해양관측소 +
    + +
    + {/* body */} +
    + +
      +
    • + 2023.10.16 20:54 +
    • +
    • + 조위 + 251(cm) +
    • +
    • + 수온 + 19.6(°C) +
    • +
    • + 염분 + 31.8(PSU) +
    • +
    • + 기온 + 16.9(°C) +
    • +
    • + 기압 + 1016.6(hPa) +
    • +
    • + 풍향 + 315(deg) +
    • +
    • + 풍속 + 7.1(m/s) +
    • +
    • + 유속방향 + -(deg) +
    • +
    • + 유속 + -(m/s) +
    • +
    +
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/publish/scss/HeaderComponent.scss b/src/publish/scss/HeaderComponent.scss new file mode 100644 index 00000000..d033824c --- /dev/null +++ b/src/publish/scss/HeaderComponent.scss @@ -0,0 +1,109 @@ + +@charset "utf-8"; + +#wrap { + + /* header */ + #header { + width: 100%; + height: 4.4rem; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + background: var(--secondary5); + + .logoArea { + display: flex; + align-items: center; + + .logo { + width: 4.3rem; + height: 4.4rem; + display: flex; + align-items: center; + justify-content: center; + background: url(../assets/images/logo.svg) no-repeat center / contain; + } + + .logoTxt { + color: var(--white); + font-weight: var(--fw-bold); + } + } + + aside { + ul { + display: flex; + + li { + position: relative; + width: 3rem; + height: 3rem; + margin-right: .9rem; + + &.setWrap:hover .setMenu { + display: block; + } + + a { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + + &.alram { + background-image: url(../assets/images/ico_alarm.svg); + } + + &.set { + background-image: url(../assets/images/ico_set.svg); + } + + &.user { + background-image: url(../assets/images/ico_user.svg); + } + } + } + } + } + .setMenu { + display: none; + position: absolute; + top: 2.8rem; + right: -1rem; + min-width: 22rem; + background-color:var(--secondary2) ; + padding: .6rem 0; + z-index: 100; + pointer-events: auto; + + a { + display: block; + padding: .8rem 1.5rem; + font-size: var(--fs-m); + font-weight: var(--fw-bold); + + &:hover { + background-color:var(--primary1); + } + } + } + + .badge { + position: absolute; + top: .6rem; + right: .8rem; + display: block; + width: .5rem; + height: .6rem; + border-radius: 50%; + background-color: var(--alert); + } + } +} \ No newline at end of file diff --git a/src/publish/scss/Layout.scss b/src/publish/scss/Layout.scss new file mode 100644 index 00000000..81b9c753 --- /dev/null +++ b/src/publish/scss/Layout.scss @@ -0,0 +1,557 @@ + +@charset "utf-8"; + +#wrap { + width: 100%; + height: 100%; + + /* header */ + #header { + width: 100%; + height: 4.4rem; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + background: var(--secondary5); + + .logoArea { + display: flex; + align-items: center; + + .logo { + width: 4.3rem; + height: 4.4rem; + display: flex; + align-items: center; + justify-content: center; + background: url(../assets/images/logo.svg) no-repeat center / contain; + } + + .logoTxt { + color: var(--white); + font-weight: var(--fw-bold); + } + } + + aside { + ul { + display: flex; + + li { + width: 3rem; + height: 3rem; + margin-right: .9rem; + + a { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + + &.alram { + background-image: url(../assets/images/ico_alarm.svg); + } + + &.set { + background-image: url(../assets/images/ico_set.svg); + } + + &.user { + background-image: url(../assets/images/ico_user.svg); + } + } + } + } + } + + .badge { + position: absolute; + top: .6rem; + right: .8rem; + display: block; + width: .5rem; + height: .6rem; + border-radius: 50%; + background-color: var(--alert); + } + } + + #sidePanel { + /* gnb */ + #nav { + position: fixed; + top: 4.4rem; + left: 0; + z-index: 100; + width: 4.3rem; + height: calc(100% - 4.4rem); + display: flex; + flex-direction: column; + justify-content: space-between; + background: var(--secondary1); + border-right: 1px solid var(--tertiary2); + + .gnb { + width: 4.2rem; + display: flex; + flex-direction: column; + + li { + width: 100%; + height: 4.4rem; + display: flex; + align-items: center; + justify-content: center; + + a { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + + /* 공통 상태 */ + &:hover, + &:active, + &.active { + background-color: var(--primary1); + } + + /* gnb icons */ + &.gnb1 { + background-image: url(../assets/images/ico_gnb01.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb01_on.svg); + } + } + + &.gnb2 { + background-image: url(../assets/images/ico_gnb02.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb02_on.svg); + } + } + + &.gnb3 { + background-image: url(../assets/images/ico_gnb03.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb03_on.svg); + } + } + + &.gnb4 { + background-image: url(../assets/images/ico_gnb04.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb04_on.svg); + } + } + + &.gnb5 { + background-image: url(../assets/images/ico_gnb05.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb05_on.svg); + } + } + + &.gnb6 { + background-image: url(../assets/images/ico_gnb06.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb06_on.svg); + } + } + } + } + } + } + + + /* side-pannel */ + .slidePanel { + position: fixed; + top: 4.4rem; + left: 4.3rem; + width: 54rem; + height: calc(100% - 4.4rem); + display: flex; + flex-direction: column; + background-color: var(--secondary2); + z-index: 99; + transform: translateX(0); + transition: transform .3s ease; + + /* 닫힘 상태 */ + &.is-closed { + transform: translateX(-100%); + + .toogle::after { + transform: translate(-50%, -50%) rotate(180deg); + } + } + + .tabBox { + flex-shrink: 0; + width: 100%; + height: 4.8rem; + padding: 0 1.9rem; + margin-top: 1.5rem; + } + + .tabWrap { + flex: 1; + min-height: 0; + display: flex; + flex-direction: column; + + &.is-active { + display: flex; + } + + .tabTop { + flex-shrink: 0; + width: 100%; + padding: 0 2rem; + border-bottom: 1px solid var(--secondary1); + + .title { + height: 6.4rem; + font-size: var(--fs-ml); + color: var(--white); + padding: 2rem 0; + } + } + + .tabBtm { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 3rem 1.9rem; + } + } + + .toogle { + position: absolute; + overflow: hidden; + z-index: 10; + top: 50%; + left: 100%; + width: 2.4rem; + height: 5rem; + border-radius: 0 1rem 1rem 0; + transform: translateY(-50%); + background-color: var(--secondary2); + border: solid 1px var(--secondary3); + + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 2.4rem; + height: 2.4rem; + transform: translate(-50%, -50%); + background: url(../assets/images/ico_toggle.svg) no-repeat center / 2.4rem; + transition: transform .3s ease; + } + } + } + + } + /* main */ + #main { + width: 100%; + height: calc(100% - 4.4rem); + position: relative; + + /* top-bar */ + .topBar { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + padding: 1.5rem 5.4rem; + } + + /* top-location */ + .locationInfo { + grid-column: 2; + width: 71rem; + max-width: 100%; + height: 3.8rem; + background-color: rgba(var(--secondary6-rgb), .9); + + ul { + display: flex; + align-items: center; + width: 100%; + height: 100%; + + li { + display: flex; + align-items: center; + height: 100%; + padding: 0 1rem; + color: var(--white); + font-weight: var(--fw-bold); + font-size: 1.3rem; + + &:first-child, + &:last-child { + padding: 0; + } + + span + span { + padding-right: .5rem; + } + + &.divider { + position: relative; + + &::after { + content: ""; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 1.8rem; + background-color: rgba(255, 255, 255, .3); + } + } + + .wgs { + display: flex; + align-items: center; + padding-right: .5rem; + + &::before { + content: ""; + display: block; + width: 2rem; + height: 2rem; + margin-right: .5rem; + background: url(../assets/images/ico_globe.svg) no-repeat center / 2rem; + } + } + + .kst { + display: flex; + align-items: center; + justify-content: center; + width: 3.1rem; + height: 1.8rem; + margin-right: .5rem; + border-radius: .3rem; + background-color: var(--tertiary3); + color: var(--white); + font-size: var(--fs-s); + } + } + } + + button { + width: 4rem; + height: 3.8rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + box-sizing: border-box; + background-color: var(--secondary3); + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + + &:hover, + &.active { + background-color: rgba(var(--primary1-rgb), .8); + } + + &.map { + background-image: url(../assets/images/ico_map.svg); + } + + &.ship { + background-image: url(../assets/images/ico_ship.svg); + } + + &.set { + width: 2rem; + height: 2rem; + background-color: transparent; + background-image: url(../assets/images/ico_set_s.svg); + background-size: 2rem; + + &:hover, + &.active { + background-color: transparent; + } + } + } + } + + /* top-search */ + .schBox { + position: relative; + grid-column: 3; + justify-self: end; + width: 20rem; + height: 3.8rem; + + .sch { + width: 100%; + height: 100%; + padding-right: 4.4rem; + border-radius: 2rem; + background-color: rgba(var(--secondary6-rgb), .9); + + &::placeholder { + color: var(--white); + } + } + + .mainSchBtn { + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + width: 4rem; + height: 3.8rem; + font-size: 0; + text-indent: -999999em; + border: none; + cursor: pointer; + background: url(../assets/images/ico_search_main.svg) no-repeat center left / 2.4rem; + } + } + } + + /* tool-bar */ + #tool { + .toolBar { + position: absolute; + top: 5.9rem; + right: .9rem; + width: 3rem; + height: 41rem; + } + + .control { + position: absolute; + bottom: 1.8rem; + right: .9rem; + width: 3rem; + height: 20rem; + } + + .toolItem { + width: 100%; + display: flex; + flex-direction: column; + + &.space { + li { + margin-bottom: .5rem; + } + } + + &.zoom { + li { + height: 3rem; + + &.num { + background-color: var(--white); + color: var(--gray-scale2); + border-left: 1px solid rgba(var(--secondary6-rgb), .8); + border-right: 1px solid rgba(var(--secondary6-rgb), .8); + font-size: var(--fs-m); + } + + button { + padding-bottom: 0; + } + } + } + + li { + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 4.2rem; + background-color: rgba(var(--secondary6-rgb), .8); + + &:hover, + &.active { + background-color: rgba(var(--primary1-rgb), .8); + } + + button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding-bottom: .2rem; + cursor: pointer; + text-align: center; + font-size: var(--fs-xxs); + color: var(--white); + background-repeat: no-repeat; + background-position: top left; + background-size: 3rem; + + &::before { + content: ""; + flex-shrink: 0; + width: 3rem; + height: 3rem; + } + + &.tool01 { background-image: url(../assets/images/ico_tool01.svg); } + &.tool02 { background-image: url(../assets/images/ico_tool02.svg); } + &.tool03 { background-image: url(../assets/images/ico_tool03.svg); } + &.tool04 { background-image: url(../assets/images/ico_tool04.svg); } + &.tool05 { background-image: url(../assets/images/ico_tool05.svg); } + &.tool06 { background-image: url(../assets/images/ico_tool06.svg); } + &.tool07 { background-image: url(../assets/images/ico_tool07.svg); } + &.tool08 { background-image: url(../assets/images/ico_tool08.svg); } + + &.zoomin { background-image: url(../assets/images/ico_plus.svg); } + &.zoomout { background-image: url(../assets/images/ico_minus.svg); } + &.legend { background-image: url(../assets/images/ico_legend.svg); } + &.minimap { background-image: url(../assets/images/ico_minimap.svg); } + } + } + } + } + +} \ No newline at end of file diff --git a/src/publish/scss/MainComponent.scss b/src/publish/scss/MainComponent.scss new file mode 100644 index 00000000..f9a1e878 --- /dev/null +++ b/src/publish/scss/MainComponent.scss @@ -0,0 +1,188 @@ + +@charset "utf-8"; + +#wrap { + //* main */ + #main { + width: 100%; + min-height: calc(100vh - 4.4rem); + position: relative; + overscroll-behavior: none; + overflow: hidden; + + //* top-bar */ + .topBar { + position: absolute; + top: 1.5rem; + left: 0; + right: 0; + height: 4.4rem; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 5.4rem; + box-sizing: border-box; + z-index: 95; + } + + //* top-location */ + .locationInfo { + flex: 0 0 auto; + min-width: 71rem; + height: 3.8rem; + margin: 0 auto; + display: flex; + align-items: center; + background-color: rgba(var(--secondary6-rgb), .9); + overflow: hidden; + + ul { + display: flex; + align-items: center; + width: 100%; + height: 100%; + + li { + display: flex; + align-items: center; + height: 100%; + padding: 0 1rem; + color: var(--white); + font-weight: var(--fw-bold); + font-size: 1.3rem; + + &:first-child, + &:last-child { + padding: 0; + } + + span + span { + padding-right: .5rem; + } + + &.divider { + position: relative; + + &::after { + content: ""; + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 1.8rem; + background-color: rgba(255, 255, 255, .3); + } + } + + .wgs { + display: flex; + align-items: center; + padding-right: .5rem; + + &::before { + content: ""; + display: block; + width: 2rem; + height: 2rem; + margin-right: .5rem; + background: url(../assets/images/ico_globe.svg) no-repeat center / 2rem; + } + } + + .kst { + display: flex; + align-items: center; + justify-content: center; + width: 3.1rem; + height: 1.8rem; + margin-right: .5rem; + border-radius: .3rem; + background-color: var(--tertiary3); + color: var(--white); + font-size: var(--fs-s); + } + } + } + + button { + width: 4rem; + height: 3.8rem; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + cursor: pointer; + box-sizing: border-box; + background-color: var(--secondary3); + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + + &:hover, + &.active { + background-color: rgba(var(--primary1-rgb), .8); + } + + &.map { + background-image: url(../assets/images/ico_map.svg); + } + + &.ship { + background-image: url(../assets/images/ico_ship.svg); + } + + &.set { + width: 2rem; + height: 2rem; + background-color: transparent; + background-image: url(../assets/images/ico_set_s.svg); + background-size: 2rem; + + &:hover, + &.active { + background-color: transparent; + } + } + } + } + + //* top-search */ + .topSchBox { + flex: 0 0 auto; + width: 20rem; + height: 3.8rem; + display: flex; + justify-content: flex-end; + position: relative; + + .tschInput { + width: 100%; + height: 100%; + padding-right: 4.4rem; + border-radius: 2rem; + background-color: rgba(var(--secondary6-rgb), .9); + border: 0; + + &::placeholder { + color: var(--white); + } + } + + .mainSchBtn { + position: absolute; + top: 50%; + right: 0; + transform: translateY(-50%); + width: 4rem; + height: 3.8rem; + font-size: 0; + text-indent: -999999em; + border: none; + cursor: pointer; + background: url(../assets/images/ico_search_main.svg) no-repeat center left / 2.4rem; + } + } + + } +} \ No newline at end of file diff --git a/src/publish/scss/SideComponent.scss b/src/publish/scss/SideComponent.scss new file mode 100644 index 00000000..7dc48fe2 --- /dev/null +++ b/src/publish/scss/SideComponent.scss @@ -0,0 +1,539 @@ + +@charset "utf-8"; + +#wrap { + + #sidePanel { + /* gnb */ + #nav { + position: fixed; + top: 4.4rem; + left: 0; + z-index: 100; + width: 4.3rem; + height: calc(100% - 4.4rem); + display: flex; + flex-direction: column; + justify-content: space-between; + background: var(--secondary1); + border-right: 1px solid var(--tertiary2); + + .gnb { + width: 4.2rem; + display: flex; + flex-direction: column; + + li { + width: 100%; + height: 4.4rem; + display: flex; + align-items: center; + justify-content: center; + + button { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + + /* 공통 상태 */ + &:hover, + &:active, + &.active { + background-color: var(--primary1); + } + + /* gnb icons */ + &.gnb1 { + background-image: url(../assets/images/ico_gnb01.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb01_on.svg); + } + } + + &.gnb2 { + background-image: url(../assets/images/ico_gnb02.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb02_on.svg); + } + } + + &.gnb3 { + background-image: url(../assets/images/ico_gnb03.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb03_on.svg); + } + } + + &.gnb4 { + background-image: url(../assets/images/ico_gnb04.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb04_on.svg); + } + } + + &.gnb5 { + background-image: url(../assets/images/ico_gnb05.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb05_on.svg); + } + } + + &.gnb6 { + background-image: url(../assets/images/ico_gnb06.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb06_on.svg); + } + } + + &.gnb7 { + background-image: url(../assets/images/ico_gnb07.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb07_on.svg); + } + } + + &.gnb8 { + background-image: url(../assets/images/ico_gnb08.svg); + + &:hover, + &:active, + &.active { + background-image: url(../assets/images/ico_gnb08_on.svg); + } + } + } + } + } + .side { + width: 4.2rem; + display: flex; + flex-direction: column; + + li { + width: 100%; + height: 4.4rem; + display: flex; + align-items: center; + justify-content: center; + + button { + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + background-repeat: no-repeat; + background-position: center; + background-size: 3rem; + background-color: var(--primary3); + + &.filter { + background-image: url('../assets/images/ico_side01.svg'); + } + + &.layer { + background-image: url('../assets/images/ico_side02.svg'); + } + } + } + } + + } + + + /* side-pannel */ + .slidePanel { + position: fixed; + top: 4.4rem; + left: 4.3rem; + width: 54rem; + height: calc(100% - 4.4rem); + min-height: 0; + display: flex; + flex-direction: column; + background-color: var(--secondary2); + border-right: .1rem solid var(--secondary3); + z-index: 99; + transform: translateX(0); + transition: transform .3s ease; + + /* 닫힘 상태 */ + &.is-closed { + transform: translateX(-100%); + + .toogle::after { + transform: translate(-50%, -50%) rotate(180deg); + } + } + + .tabBox { + flex-shrink: 0; + width: 100%; + padding: 1.5rem 2rem 0 2rem; + } + + .tabWrap { + flex: 1; + min-height: 0; + display: none; + flex-direction: column; + + &.is-active { + display: flex; + } + + .tabWrapInner { + display: flex; + flex-direction: column; + flex: 1; + min-height: 100%; + + .tabWrapCnt { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + padding-bottom: 3rem; + + // 필터 스위치그룹 + .switchGroup { + display: flex; + flex-direction: column; + width: 100%; + + .sgHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.2rem 1rem; + background-color: var(--secondary1); + border-bottom: .1rem solid var(--secondary3); + + .colL { + display: flex; + align-items: center; + gap: 1rem; + font-weight: var(--fw-bold); + + .favship { + width: 2rem; + height: 2rem; + background: url(../assets/images/ico_favship.svg) no-repeat center / contain; + } + } + + .toggleBtn { + display: flex; + align-items: center; + width: 2.4rem; + height: 2.4rem; + background: url(../assets/images/ico_arrow_toggle_down.svg) no-repeat center / contain; + transition: transform 0.3s ease; + + &.is-open { + transform: rotate(180deg); + } + } + } + + .switchBox { + overflow: hidden; + max-height: 0; + transition: max-height 0.3s ease; + + &.is-open { + max-height: 500rem; + } + + .switchList { + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; + background-color: var(--tertiary1); + padding: 1rem; + + li { + display: flex; + justify-content: space-between; + padding: .1rem 0; + + span { + flex: 1; + min-width: 0; + word-break: break-word; + overflow-wrap: break-word; + white-space: normal; + } + } + } + } + } + } + + .btnBox { + flex: 0 0 auto; + margin-top: auto; + display: flex; + justify-content: flex-end; + padding: 2rem; + } + } + + .tabTop { + flex-shrink: 0; + width: 100%; + padding: 0 2rem; + + .title { + font-size: var(--fs-ml); + color: var(--white); + font-weight: var(--fw-bold); + padding: 1.7rem 0; + + .prevBtn { + width: 2rem; + height: 2rem; + background: url(../assets/images/ico_tit_prev.svg) no-repeat center /contain; + margin-right: .5rem; + } + } + // 기상패널 범례 + .legend { + display: flex; + align-items: flex-start; + flex-direction: column; + gap: .4rem; + margin-bottom: 2.5rem; + + .legendTitle { + font-size: var(--fs-m); + } + + .legendList { + width: 100%; + border-radius: .6rem; + padding: 1.5rem; + background-color: rgba(var(--secondary4-rgb), .2); + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + + li { + display: flex; + align-items: center; + justify-content: flex-start; + font-size: var(--fs-ml); + + img { + width: 2.6rem; + height: 2.6rem; + margin-right: .5rem; + } + } + } + } + } + + .tabBtm { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + border-top: 1px solid var(--secondary1); + padding: 3rem 2rem; + + &.noLine { + overflow-y: visible; + border-top: 0; + padding: 0 2rem; + } + + &.noSc { + overflow-y: visible; + padding-bottom: 0; + } + .tabBtmInner { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + + .tabBtmCnt { + flex: 1 1 auto; + min-height: 0 ; + overflow-y: auto; + padding-bottom: 2rem; + } + + .btnBox { + flex: 0 0 auto; + margin-top: auto; + display: flex; + justify-content: flex-end; + gap: 1rem; + padding: 2rem 0; + + &.rowSB { + justify-content: space-between; + } + + button { + flex: initial; + min-width: 12rem; + } + } + } + } + + } + + .toogle { + position: absolute; + overflow: hidden; + z-index: 10; + top: 50%; + left: 100%; + width: 2.4rem; + height: 5rem; + border-radius: 0 1rem 1rem 0; + transform: translateY(-50%); + background-color: var(--secondary2); + border: solid 1px var(--secondary3); + + &::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 2.4rem; + height: 2.4rem; + transform: translate(-50%, -50%); + background: url(../assets/images/ico_toggle.svg) no-repeat center / 2.4rem; + transition: transform .3s ease; + } + } + // ai모드 + .panelHeader { + display: flex; + align-items: center; + padding: 0 2rem; + + .panelTitle { + padding: 1.7rem 0; + font-size: var(--fs-ml); + font-weight: var(--fw-bold); + } + } + + .panelBody { + display: flex; + flex: 1; + min-height: 0; + padding: 0 2rem; + + .ai { + display: grid; + grid-template-columns: repeat(2, 1fr); + align-items: start; + gap: 1rem; + row-gap: 1rem; + grid-auto-rows: min-content; + width: 100%; + + li { + display: flex; + align-items: center; + justify-content: flex-start; + + a { + position: relative; + display: flex; + flex-direction: column; + gap: .6rem; + width: 100%; + height: auto; + padding: 2rem 1.2rem; + background-color: var(--secondary1); + border: 2px solid transparent; + border-radius: .6rem; + + &.on { + background-color: var(--secondary5); + border-color: var(--primary1); + } + + &:not(.on) > * { + opacity: .5; + } + + .title { + display: flex; + align-items: center; + font-weight: var(--fw-bold); + + img { + width: 2.2rem; + height: 2.2rem; + margin-right: .5rem; + } + } + + .keyword { + font-weight: var(--fw-bold); + } + + .control { + position: absolute; + top: 2rem; + right: 1.5rem; + display: flex; + align-items: center; + + i { + width: .8rem; + height: .8rem; + border-radius: 50%; + background-color: var(--white); + margin-right: .5rem; + } + } + } + } + } + } + + .panelFooter { + border-top: 1px solid var(--secondary1); + padding: 1rem 2rem; + } + + } + + } +} \ No newline at end of file diff --git a/src/publish/scss/ToolComponent.scss b/src/publish/scss/ToolComponent.scss new file mode 100644 index 00000000..626898a9 --- /dev/null +++ b/src/publish/scss/ToolComponent.scss @@ -0,0 +1,153 @@ + +@charset "utf-8"; + +#wrap { + //* tool-bar */ + #tool { + .toolBar { + position: absolute; + top: 5.9rem; + right: .9rem; + width: 3rem; + height: 41rem; + z-index: 95; + } + + .control { + position: absolute; + bottom: 1.8rem; + right: .9rem; + width: 3rem; + height: 20rem; + } + + .toolItem { + width: 100%; + display: flex; + flex-direction: column; + + &.space { + li { + margin-bottom: .5rem; + } + } + + &.zoom { + li { + height: 3rem; + + &.num { + background-color: var(--white); + color: var(--gray-scale1); + border-left: 1px solid rgba(var(--secondary6-rgb), .8); + border-right: 1px solid rgba(var(--secondary6-rgb), .8); + font-size: var(--fs-m); + } + + button { + padding-bottom: 0; + } + } + } + + li { + display: flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 4.2rem; + background-color: rgba(var(--secondary6-rgb), .8); + + button { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + padding-bottom: .2rem; + cursor: pointer; + text-align: center; + font-size: var(--fs-xxs); + color: var(--white); + background-repeat: no-repeat; + background-position: top left; + background-size: 3rem; + + &:hover, + &.active { + background-color: rgba(var(--primary1-rgb), .8); + } + + &::before { + content: ""; + flex-shrink: 0; + width: 3rem; + height: 3rem; + } + + &.tool01 { background-image: url(../assets/images/ico_tool01.svg); } + &.tool02 { background-image: url(../assets/images/ico_tool02.svg); } + &.tool03 { background-image: url(../assets/images/ico_tool03.svg); } + &.tool04 { background-image: url(../assets/images/ico_tool04.svg); } + &.tool05 { background-image: url(../assets/images/ico_tool05.svg); } + &.tool06 { background-image: url(../assets/images/ico_tool06.svg); } + &.tool07 { background-image: url(../assets/images/ico_tool07.svg); } + &.tool08 { background-image: url(../assets/images/ico_tool08.svg); } + + &.zoomin { background-image: url(../assets/images/ico_plus.svg); } + &.zoomout { background-image: url(../assets/images/ico_minus.svg); } + &.legend { background-image: url(../assets/images/ico_legend.svg); } + &.minimap { background-image: url(../assets/images/ico_minimap.svg); } + } + } + } + // 범례 + .legendWrap { + position: fixed; + right: 5rem; + bottom: 5rem; + + .legendList { + display: flex; + flex-direction: column; + width: 14rem; + height: auto; + border-radius: .5rem; + background-color: var(--gray-scale3); + padding: .7rem; + gap: .4rem; + + li { + display: flex; + justify-content: space-between; + align-items: center; + + &.legendItem { + padding: .5rem 0; + border-bottom: 1px solid var(--gray-scale7); + } + + .legendLabel { + display: flex; + align-items: center; + gap: .2rem; + font-size: var(--fs-s); + font-weight: var(--fw-bold); + + img { + width: 1.6rem; + height: 1.6rem; + } + } + + .legendValue { + font-size: var(--fs-s); + font-weight: var(--fw-heavy); + } + } + } + } + + } +} \ No newline at end of file diff --git a/src/publish/scss/WrapComponent.scss b/src/publish/scss/WrapComponent.scss new file mode 100644 index 00000000..fa86d43f --- /dev/null +++ b/src/publish/scss/WrapComponent.scss @@ -0,0 +1,7 @@ + +@charset "utf-8"; + +#wrap { + width: 100%; + min-height: 100vh; +} \ No newline at end of file diff --git a/src/scss/SideComponent.scss b/src/scss/SideComponent.scss index 5754b8f7..4cd604f3 100644 --- a/src/scss/SideComponent.scss +++ b/src/scss/SideComponent.scss @@ -244,6 +244,23 @@ height: 2rem; background: url(../assets/images/ico_favship.svg) no-repeat center / contain; } + + .btnDelDark { + padding: 0.2rem 0.6rem; + font-size: 1.1rem; + font-weight: var(--fw-regular); + color: var(--danger, #ff4d4f); + border: .1rem solid var(--danger, #ff4d4f); + border-radius: 0.4rem; + background: transparent; + cursor: pointer; + line-height: 1.4; + + &:hover { + background: var(--danger, #ff4d4f); + color: #fff; + } + } } .toggleBtn { diff --git a/src/scss/ToolComponent.scss b/src/scss/ToolComponent.scss index 626898a9..19de52ba 100644 --- a/src/scss/ToolComponent.scss +++ b/src/scss/ToolComponent.scss @@ -102,6 +102,65 @@ } } } + // 선명표시 호버 패널 + .label-panel-wrap { + position: relative; + } + + .labelPanel { + position: absolute; + right: 100%; + top: 50%; + transform: translateY(-50%); + padding-right: 12px; + + .label-panel-inner { + background-color: rgba(30, 35, 50, 0.95); + border-radius: 6px; + padding: 6px 12px; + min-width: 120px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + } + + .label-panel-title { + color: #fff; + font-size: 12px; + font-weight: bold; + margin-bottom: 4px; + padding-bottom: 4px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + } + + .label-panel-list { + list-style: none; + margin: 0; + padding: 0; + } + + .label-panel-item { + display: flex; + align-items: center; + justify-content: space-between; + width: auto; + height: auto; + padding: 2px 0; + background-color: transparent; + color: #ddd; + font-size: 12px; + + &.area-shape-item { + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + + &:hover { + background-color: rgba(255, 255, 255, 0.15); + color: #fff; + } + } + } + } + // 범례 .legendWrap { position: fixed; diff --git a/src/scss/global.scss b/src/scss/global.scss new file mode 100644 index 00000000..c8c4a946 --- /dev/null +++ b/src/scss/global.scss @@ -0,0 +1,135 @@ +/** + * 글로벌 스타일 + */ + +// 기존 SCSS 임포트 +@import './WrapComponent.scss'; +@import './HeaderComponent.scss'; +@import './SideComponent.scss'; +@import './MainComponent.scss'; +@import './ToolComponent.scss'; + +// 퍼블리시 미리보기 스타일 +.publish-wrapper { + display: flex; + height: 100vh; + background: #1a1a2e; +} + +.publish-nav { + width: 250px; + background: #16213e; + border-right: 1px solid #0f3460; + display: flex; + flex-direction: column; + + .publish-nav-header { + padding: 16px; + border-bottom: 1px solid #0f3460; + display: flex; + flex-direction: column; + gap: 8px; + + a { + color: #e94560; + text-decoration: none; + font-size: 14px; + + &:hover { + text-decoration: underline; + } + } + + .publish-title { + color: #fff; + font-size: 18px; + font-weight: bold; + } + } + + .publish-menu { + list-style: none; + margin: 0; + padding: 0; + flex: 1; + overflow-y: auto; + + li { + a { + display: block; + padding: 12px 16px; + color: #aaa; + text-decoration: none; + border-bottom: 1px solid #0f3460; + transition: all 0.2s; + + &:hover { + background: #0f3460; + color: #fff; + } + + &.active { + background: #e94560; + color: #fff; + } + } + } + } +} + +.publish-content { + flex: 1; + overflow: auto; + padding: 20px; + + .publish-home { + max-width: 800px; + margin: 0 auto; + color: #fff; + + h1 { + margin-bottom: 20px; + color: #e94560; + } + + h2 { + margin-top: 30px; + margin-bottom: 10px; + color: #fff; + font-size: 18px; + } + + p { + color: #aaa; + } + + pre { + background: #0f3460; + padding: 16px; + border-radius: 8px; + overflow-x: auto; + font-size: 14px; + } + + ol { + color: #aaa; + padding-left: 20px; + + li { + margin-bottom: 8px; + } + + code { + background: #0f3460; + padding: 2px 6px; + border-radius: 4px; + color: #e94560; + } + } + } + + .panel-wrapper { + background: #1a1a2e; + min-height: 100%; + } +} diff --git a/src/stores/mapStore.js b/src/stores/mapStore.js new file mode 100644 index 00000000..e8eaffa1 --- /dev/null +++ b/src/stores/mapStore.js @@ -0,0 +1,61 @@ +import { create } from 'zustand'; + +/** + * 지도 상태 관리 스토어 + */ +export const useMapStore = create((set, get) => ({ + // 지도 인스턴스 + map: null, + setMap: (map) => set({ map }), + + // 줌 레벨 + zoom: 7, + setZoom: (zoom) => set({ zoom }), + + zoomIn: () => { + const { map, zoom } = get(); + if (map && zoom < 17) { + const newZoom = zoom + 1; + map.getView().setZoom(newZoom); + set({ zoom: newZoom }); + } + }, + + zoomOut: () => { + const { map, zoom } = get(); + if (map && zoom > 0) { + const newZoom = zoom - 1; + map.getView().setZoom(newZoom); + set({ zoom: newZoom }); + } + }, + + // 중심 좌표 [lon, lat] + center: [127.1388684, 37.4449168], + setCenter: (center) => set({ center }), + + // 측정 도구 + activeMeasureTool: null, // 'distance' | 'area' | 'rangeRing' | null + areaShape: null, // 'Polygon' | 'Box' | 'Circle' | null + + setMeasureTool: (tool) => set((state) => ({ + activeMeasureTool: state.activeMeasureTool === tool ? null : tool, + areaShape: null, + })), + setAreaShape: (shape) => set({ areaShape: shape }), + clearMeasure: () => set({ activeMeasureTool: null, areaShape: null }), + + // 레이어 가시성 + layerVisibility: { + baseMap: true, + ships: true, + weather: false, + satellite: false, + }, + toggleLayer: (layerName) => set((state) => ({ + layerVisibility: { + ...state.layerVisibility, + [layerName]: !state.layerVisibility[layerName], + }, + })), +})); diff --git a/src/stores/shipStore.js b/src/stores/shipStore.js new file mode 100644 index 00000000..0fe6508d --- /dev/null +++ b/src/stores/shipStore.js @@ -0,0 +1,997 @@ +/** + * 선박 데이터 Zustand 스토어 + * 참조: mda-react-front/src/shared/model/deckStore.ts + * 참조: mda-react-front/src/common/deck.ts (카운트 쓰로틀링) + */ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; +import { + SIGNAL_KIND_CODE_FISHING, + SIGNAL_KIND_CODE_KCGV, + SIGNAL_KIND_CODE_PASSENGER, + SIGNAL_KIND_CODE_CARGO, + SIGNAL_KIND_CODE_TANKER, + SIGNAL_KIND_CODE_GOV, + SIGNAL_KIND_CODE_NORMAL, + SIGNAL_KIND_CODE_BUOY, + SIGNAL_SOURCE_CODE_AIS, + SIGNAL_SOURCE_CODE_VPASS, + SIGNAL_SOURCE_CODE_ENAV, + SIGNAL_SOURCE_CODE_VTS_AIS, + SIGNAL_SOURCE_CODE_D_MF_HF, + SIGNAL_SOURCE_CODE_RADAR, + NATIONAL_CODE_KR, + NATIONAL_CODE_CN, + NATIONAL_CODE_JP, + NATIONAL_CODE_KP, + NATIONAL_CODE_OTHER, +} from '../types/constants'; + +// ===================== +// 국적 코드 매핑 (ShipBatchRenderer.js와 동일) +// ===================== +function mapNationalCode(nationalCode) { + if (!nationalCode) return 'OTHER'; + const code = nationalCode.toUpperCase(); + if (code === 'KR' || code === 'KOR' || code === '440') return 'KR'; + if (code === 'CN' || code === 'CHN' || code === '412' || code === '413' || code === '414') return 'CN'; + if (code === 'JP' || code === 'JPN' || code === '431' || code === '432') return 'JP'; + if (code === 'KP' || code === 'PRK' || code === '445') return 'KP'; + return 'OTHER'; +} + +// ===================== +// 서버 수신시간 파싱 (receivedTime → ms timestamp) +// 형식: "YYYYMMDDHHmmss" +// ===================== +function parseReceivedTime(receivedTime) { + if (!receivedTime || receivedTime.length < 14) return Date.now(); + const y = receivedTime.slice(0, 4); + const M = receivedTime.slice(4, 6); + const d = receivedTime.slice(6, 8); + const h = receivedTime.slice(8, 10); + const m = receivedTime.slice(10, 12); + const s = receivedTime.slice(12, 14); + const ts = new Date(`${y}-${M}-${d}T${h}:${m}:${s}`).getTime(); + return isNaN(ts) ? Date.now() : ts; +} + +// ===================== +// 타임아웃 상수 (카운트 사이클에서 상태 전환/삭제 판정) +// ===================== +// +// ■ 영해안 (LOST=0, Inshore) +// 국내 직접 수집수단(AIS 기지국, VTS 등)이 커버하는 해역. +// 수신 주기가 짧으므로(수 초~수 분) 12분 무수신 시 정상 이탈로 판단하여 삭제. +// +// ■ 영해밖 (LOST=1, Offshore) +// 직접 수집수단이 닿지 않아 위성 AIS(S-AIS) 등 간접 수단에 의존. +// 위성 AIS는 선박 위치·궤도에 따라 수신 간격이 30분~최대 1시간까지 벌어질 수 있어, +// 유효한 항해 중인 선박이 다크시그널로 오판되지 않도록 65분(3900초)으로 설정. +// +// ■ 레이더 (단독, 비통합) +// 레이더 신호는 실시간 회전 주기(수 초)에 맞춰 갱신되므로 타임아웃을 짧게 유지. +// 함정용은 /topic/ship-throttled-60s 채널 기반이므로 90초로 설정. +// +// 참조: mda-react-front/src/common/deck.ts +// 추후 사용자 설정 화면에서 커스텀 가능하도록 상수로 분리. +// ===================== +const INSHORE_TIMEOUT_MS = 12 * 60 * 1000; // 720초 (12분) — 영해안: LOST=0, 무수신 시 삭제 +const OFFSHORE_TIMEOUT_MS = 65 * 60 * 1000; // 3900초 (65분) — 영해밖: LOST=1, 무수신 시 다크시그널 전환 +const RADAR_TIMEOUT_MS = 90 * 1000; // 90초 — 단독 레이더 비통합, 무수신 시 삭제 +const SIGNAL_SOURCE_RADAR = '000005'; + +// ===================== +// 장비 활성 상태 판단 +// 참조: mda-react-front/src/common/deck.ts - isAnyEquipmentActive +// AVETDR 6개 장비 중 하나라도 '1'(활성)이면 true +// ===================== +const EQUIPMENT_KEYS = ['ais', 'vpass', 'enav', 'vtsAis', 'dMfHf', 'vtsRadar']; + +function isAnyEquipmentActive(ship) { + return EQUIPMENT_KEYS.some(key => ship[key] === '1'); +} + +// ===================== +// 카운트 쓰로틀링 상수 및 캐시 +// 참조: mda-react-front/src/common/deck.ts (271-331) +// ===================== +const LIVE_COUNT_THROTTLE_MS = 5000; // 5초 + +/** + * 카운트 캐시 (스토어 외부에 저장) + * - counts: 마지막으로 계산된 종류별 카운트 + * - lastCalcTime: 마지막 계산 시간 + * - lastFilterHash: 마지막 필터 해시 (필터 변경 감지용) + */ +const countCache = { + counts: null, + lastCalcTime: 0, + lastFilterHash: '', +}; + +/** + * 필터 해시 생성 (필터 변경 감지용) + * @param {Object} kindVisibility - 선박 종류별 표시 여부 + * @param {Object} sourceVisibility - 신호원별 표시 여부 + * @returns {string} 필터 해시 문자열 + */ +function generateFilterHash(kindVisibility, sourceVisibility, nationalVisibility, darkSignalVisible) { + const kindKeys = Object.keys(kindVisibility).sort(); + const sourceKeys = Object.keys(sourceVisibility).sort(); + const nationalKeys = Object.keys(nationalVisibility).sort(); + + const kindHash = kindKeys.map(k => kindVisibility[k] ? '1' : '0').join(''); + const sourceHash = sourceKeys.map(k => sourceVisibility[k] ? '1' : '0').join(''); + const nationalHash = nationalKeys.map(k => nationalVisibility[k] ? '1' : '0').join(''); + + return `${kindHash}_${sourceHash}_${nationalHash}_${darkSignalVisible ? '1' : '0'}`; +} + +/** + * 초기 선박 종류별 카운트 + */ +const initialKindCounts = { + [SIGNAL_KIND_CODE_FISHING]: 0, + [SIGNAL_KIND_CODE_KCGV]: 0, + [SIGNAL_KIND_CODE_PASSENGER]: 0, + [SIGNAL_KIND_CODE_CARGO]: 0, + [SIGNAL_KIND_CODE_TANKER]: 0, + [SIGNAL_KIND_CODE_GOV]: 0, + [SIGNAL_KIND_CODE_NORMAL]: 0, + [SIGNAL_KIND_CODE_BUOY]: 0, +}; + +/** + * 초기 선박 종류별 표시 설정 + */ +const initialKindVisibility = { + [SIGNAL_KIND_CODE_FISHING]: true, + [SIGNAL_KIND_CODE_KCGV]: true, + [SIGNAL_KIND_CODE_PASSENGER]: true, + [SIGNAL_KIND_CODE_CARGO]: true, + [SIGNAL_KIND_CODE_TANKER]: true, + [SIGNAL_KIND_CODE_GOV]: true, + [SIGNAL_KIND_CODE_NORMAL]: true, + [SIGNAL_KIND_CODE_BUOY]: true, +}; + +/** + * 초기 신호원별 표시 설정 + */ +const initialSourceVisibility = { + [SIGNAL_SOURCE_CODE_AIS]: true, + [SIGNAL_SOURCE_CODE_VPASS]: true, + [SIGNAL_SOURCE_CODE_ENAV]: true, + [SIGNAL_SOURCE_CODE_VTS_AIS]: true, + [SIGNAL_SOURCE_CODE_D_MF_HF]: true, + [SIGNAL_SOURCE_CODE_RADAR]: true, +}; + +/** + * 초기 국적별 표시 설정 + */ +const initialNationalVisibility = { + [NATIONAL_CODE_KR]: true, + [NATIONAL_CODE_CN]: true, + [NATIONAL_CODE_JP]: true, + [NATIONAL_CODE_KP]: true, + [NATIONAL_CODE_OTHER]: true, +}; + +/** + * 선박 스토어 + */ +const useShipStore = create(subscribeWithSelector((set, get) => ({ + // ===================== + // 상태 (State) + // ===================== + + /** 선박 데이터 맵 (featureId -> shipData), featureId = signalSourceCode + targetId */ + features: new Map(), + + /** 다크시그널 선박 ID Set (features와 별도 관리, 메인 프로젝트 동일 구조) + * 참조: mda-react-front/src/shared/model/deckStore.ts - darkSignalIds */ + darkSignalIds: new Set(), + + /** 선박 종류별 카운트 */ + kindCounts: { ...initialKindCounts }, + + /** 선박 종류별 표시 여부 */ + kindVisibility: { ...initialKindVisibility }, + + /** 신호원별 표시 여부 */ + sourceVisibility: { ...initialSourceVisibility }, + + /** 국적별 표시 여부 */ + nationalVisibility: { ...initialNationalVisibility }, + + /** 선택된 선박 ID (단일 클릭용, 레거시) */ + selectedShipId: null, + + /** Ctrl+Drag 다중 선택된 featureId 배열 (제한 없음) */ + selectedShipIds: [], + + /** 컨텍스트 메뉴 상태 { x, y, ships: [] } | null */ + contextMenu: null, + + /** 호버 중인 선박 정보 { ship, x, y } | null */ + hoverInfo: null, + + /** 상세 모달 배열 (최대 3개) [{ ship, id, initialPos }] */ + detailModals: [], + + /** 마지막 모달 위치 (새 모달 초기 위치 계산용) */ + lastModalPos: null, + + /** 다크시그널(소실신호) 표시 여부 */ + darkSignalVisible: true, + + /** 다크시그널 선박 수 */ + darkSignalCount: 0, + + /** 선박 표시 On/Off */ + isShipVisible: true, + + /** 선박 통합 모드 (통합선박에서 isPriority만 표시) */ + isIntegrate: true, + + /** 선명표시 여부 (개발 중 기본 활성화) */ + showLabels: true, + + /** 선명표시 옵션 (개발 중 기본 모두 활성화) */ + labelOptions: { + showShipName: true, // 선박명 + showSpeedVector: true, // 속도벡터 + showShipSize: true, // 선박크기 + showSignalStatus: true, // 신호상태 + }, + + /** STOMP 연결 상태 */ + isConnected: false, + + /** 범례 표시 여부 */ + showLegend: true, + + /** 변경된 선박 ID 추적 (렌더링 최적화용) */ + changedIds: new Set(), + + /** 총 선박 수 */ + totalCount: 0, + + // ===================== + // 액션 (Actions) + // ===================== + + /** + * 여러 선박 데이터 병합 (bulk update) + * 카운트는 5초 쓰로틀로 업데이트 (메인 프로젝트 동일) + * @param {Array} ships - 선박 데이터 배열 + */ + mergeFeatures: (ships) => { + set((state) => { + const newFeatures = new Map(state.features); + const newDarkSignalIds = new Set(state.darkSignalIds); + const newChangedIds = new Set(); + + ships.forEach((ship) => { + const featureId = ship.featureId; + if (!featureId) return; + + // 좌표가 없으면 스킵 + if (!ship.longitude || !ship.latitude) { + return; + } + + const hasActive = isAnyEquipmentActive(ship); + + // 규칙 1: LOST=0(영해 내) + 모든 장비 비활성 → 저장하지 않음 (완전 삭제) + if (!ship.lost && !hasActive) { + newFeatures.delete(featureId); + newDarkSignalIds.delete(featureId); + return; + } + + // 다크시그널 상태 판정 + if (hasActive) { + // 장비 활성 → 다크시그널 해제 (회복) + newDarkSignalIds.delete(featureId); + } else { + // 모든 장비 비활성 (상태 전환용 신호) + // → 이미 다크시그널이면 유지, 아니면 등록 + // → 상태 플래그(위치/시간 등)는 갱신하되 카운트에 반영하지 않음 + newDarkSignalIds.add(featureId); + } + + // receivedTimestamp 부여: 서버 수신시간 기반 (초기 로드 시 정확한 경과 시간 판단) + // 장비 비활성 신호도 상태 플래그(위치, 시간 등)는 항상 갱신 + newFeatures.set(featureId, { ...ship, receivedTimestamp: parseReceivedTime(ship.receivedTime) }); + newChangedIds.add(featureId); + }); + + return { + features: newFeatures, + darkSignalIds: newDarkSignalIds, + changedIds: newChangedIds, + }; + }); + + // 카운트 재계산 (다크시그널 전환, 레이더 삭제도 여기서 처리) + // 쓰로틀 카운트 업데이트 + get().updateCountsThrottled(); + }, + + /** + * 쓰로틀 카운트 업데이트 + * 5초 이내에는 캐시된 값 사용, 필터 변경 시 즉시 재계산 + * 참조: mda-react-front/src/common/deck.ts - updateLayerData() + */ + updateCountsThrottled: () => { + const state = get(); + const { features, kindVisibility, sourceVisibility, nationalVisibility, isIntegrate, darkSignalVisible } = state; + + const now = Date.now(); + const currentFilterHash = generateFilterHash(kindVisibility, sourceVisibility, nationalVisibility, darkSignalVisible) + `_${isIntegrate}`; + + // 5초 이내이고 필터 변경 없으면 스킵 + const elapsed = now - countCache.lastCalcTime; + const filterChanged = currentFilterHash !== countCache.lastFilterHash; + + if (elapsed < LIVE_COUNT_THROTTLE_MS && !filterChanged && countCache.counts) { + return; // 캐시 사용 (업데이트 스킵) + } + + // 카운트 재계산 + 상태 전환 (다크시그널/레이더 삭제) + // 참조: mda-react-front/src/common/deck.ts - updateLayerData() + // + // 처리 순서 (메인 프로젝트 동일): + // ① darkSignalIds.has(featureId) → darkCount++, return + // ② 레이더(000005)+비통합 → timeout? delete : return + // ③ LOST=0 + INSHORE timeout → delete, return + // ④ LOST=1 + OFFSHORE timeout → darkSignal 전환, return + // ⑤ !isAnyEquipmentActive → darkSignal 전환, return + // ⑥ isPriority 필터 + 필터 + 카운트 + const { darkSignalIds } = state; + const newKindCounts = { ...initialKindCounts }; + let newDarkSignalCount = 0; + const seenTargetIds = new Set(); + const newDarkIds = []; // 다크시그널 전환 대상 (④⑤) + const deleteIds = []; // 삭제 대상 (②③) + + features.forEach((ship, featureId) => { + // ① 이미 다크시그널 → 카운트만 (비용 최소: Set.has O(1)) + if (darkSignalIds.has(featureId)) { + newDarkSignalCount++; + return; + } + + // ② 단독 레이더(비통합) → 타임아웃이면 삭제, 카운트 제외 + if (ship.signalSourceCode === SIGNAL_SOURCE_RADAR && !ship.integrate) { + if (now - ship.receivedTimestamp > RADAR_TIMEOUT_MS) { + deleteIds.push(featureId); + } + return; + } + + const elapsed = now - ship.receivedTimestamp; + + // ③ 영해안 (LOST≠1) + INSHORE 타임아웃 → 삭제 + if (!ship.lost && elapsed > INSHORE_TIMEOUT_MS) { + deleteIds.push(featureId); + return; + } + + // ④ 영해밖 (LOST=1) + OFFSHORE 타임아웃 → 다크시그널 전환 + if (ship.lost && elapsed > OFFSHORE_TIMEOUT_MS) { + newDarkIds.push(featureId); + newDarkSignalCount++; + return; + } + + // ⑤ 모든 장비 비활성 → 즉시 다크시그널 전환 + if (!isAnyEquipmentActive(ship)) { + newDarkIds.push(featureId); + newDarkSignalCount++; + return; + } + + // ⑥ 통합 모드: isPriority가 아니면 카운트에서 제외 (삭제/전환은 위에서 처리 완료) + if (isIntegrate && ship.integrate && !ship.isPriority) return; + + // 필터 적용된 선박만 카운트 + if (!kindVisibility[ship.signalKindCode]) return; + if (!sourceVisibility[ship.signalSourceCode]) return; + const mappedNational = mapNationalCode(ship.nationalCode); + if (!nationalVisibility[mappedNational]) return; + + // targetId(통합ID) 기준 중복 제거 + if (ship.targetId && seenTargetIds.has(ship.targetId)) return; + if (ship.targetId) seenTargetIds.add(ship.targetId); + + if (ship.signalKindCode && newKindCounts[ship.signalKindCode] !== undefined) { + newKindCounts[ship.signalKindCode]++; + } + }); + + const totalCount = Object.values(newKindCounts).reduce((sum, count) => sum + count, 0); + + // 캐시 업데이트 + countCache.counts = newKindCounts; + countCache.lastCalcTime = now; + countCache.lastFilterHash = currentFilterHash; + + // features/darkSignalIds 변경이 필요한 경우 + if (newDarkIds.length > 0 || deleteIds.length > 0) { + const newFeatures = new Map(features); + const newDarkSignalIds = new Set(darkSignalIds); + + newDarkIds.forEach((fid) => newDarkSignalIds.add(fid)); + deleteIds.forEach((fid) => { + newFeatures.delete(fid); + newDarkSignalIds.delete(fid); + }); + + set({ + features: newFeatures, + darkSignalIds: newDarkSignalIds, + kindCounts: newKindCounts, + totalCount, + darkSignalCount: newDarkSignalCount, + }); + } else { + set({ + kindCounts: newKindCounts, + totalCount, + darkSignalCount: newDarkSignalCount, + }); + } + }, + + /** + * 카운트 즉시 재계산 (필터 변경 시 호출) + */ + recalculateCounts: () => { + // 캐시 무효화하여 즉시 재계산 + countCache.lastCalcTime = 0; + get().updateCountsThrottled(); + }, + + /** + /** + * 단일 선박 추가/업데이트 + * @param {Object} ship - 선박 데이터 + */ + addOrUpdateFeature: (ship) => { + get().mergeFeatures([ship]); + }, + + /** + * 선박 삭제 + * @param {string} featureId - 삭제할 선박 ID (signalSourceCode + targetId) + */ + deleteFeatureById: (featureId) => { + set((state) => { + const newFeatures = new Map(state.features); + newFeatures.delete(featureId); + + const newDarkSignalIds = new Set(state.darkSignalIds); + newDarkSignalIds.delete(featureId); + + return { + features: newFeatures, + darkSignalIds: newDarkSignalIds, + selectedShipId: state.selectedShipId === featureId ? null : state.selectedShipId, + }; + }); + + // 쓰로틀 카운트 업데이트 + get().updateCountsThrottled(); + }, + + /** + * 여러 선박 삭제 + * @param {Array} featureIds - 삭제할 선박 ID 배열 (signalSourceCode + targetId) + */ + deleteFeaturesByIds: (featureIds) => { + set((state) => { + const newFeatures = new Map(state.features); + const newDarkSignalIds = new Set(state.darkSignalIds); + featureIds.forEach((featureId) => { + newFeatures.delete(featureId); + newDarkSignalIds.delete(featureId); + }); + + return { + features: newFeatures, + darkSignalIds: newDarkSignalIds, + selectedShipId: featureIds.includes(state.selectedShipId) ? null : state.selectedShipId, + }; + }); + + // 쓰로틀 카운트 업데이트 + get().updateCountsThrottled(); + }, + + /** + * 선박 종류별 표시 토글 + * 필터 변경 시 카운트 즉시 재계산 + * @param {string} kindCode - 선박 종류 코드 + */ + toggleKindVisibility: (kindCode) => { + set((state) => ({ + kindVisibility: { + ...state.kindVisibility, + [kindCode]: !state.kindVisibility[kindCode], + }, + })); + // 필터 변경 시 즉시 카운트 재계산 + get().recalculateCounts(); + }, + + /** + * 신호원별 표시 토글 + * 필터 변경 시 카운트 즉시 재계산 + * @param {string} sourceCode - 신호원 코드 + */ + toggleSourceVisibility: (sourceCode) => { + set((state) => ({ + sourceVisibility: { + ...state.sourceVisibility, + [sourceCode]: !state.sourceVisibility[sourceCode], + }, + })); + // 필터 변경 시 즉시 카운트 재계산 + get().recalculateCounts(); + }, + + /** + * 국적별 표시 토글 + * 필터 변경 시 카운트 즉시 재계산 + * @param {string} nationalCode - 국적 코드 + */ + toggleNationalVisibility: (nationalCode) => { + set((state) => ({ + nationalVisibility: { + ...state.nationalVisibility, + [nationalCode]: !state.nationalVisibility[nationalCode], + }, + })); + // 필터 변경 시 즉시 카운트 재계산 + get().recalculateCounts(); + }, + + /** + * 다크시그널 표시 토글 + */ + toggleDarkSignalVisible: () => { + set((state) => ({ + darkSignalVisible: !state.darkSignalVisible, + })); + get().recalculateCounts(); + }, + + /** + * 다크시그널 선박 일괄 삭제 + */ + clearDarkSignals: () => { + set((state) => { + const newFeatures = new Map(state.features); + state.darkSignalIds.forEach((fid) => { + newFeatures.delete(fid); + }); + return { + features: newFeatures, + darkSignalIds: new Set(), + }; + }); + get().recalculateCounts(); + }, + + /** + * 선박 표시 전체 On/Off + */ + toggleShipVisible: () => { + set((state) => ({ + isShipVisible: !state.isShipVisible, + })); + }, + + /** + * 선명표시 On/Off + */ + toggleShowLabels: () => { + set((state) => ({ + showLabels: !state.showLabels, + })); + }, + + /** + * 선명표시 옵션 설정 + * @param {string} optionKey - 옵션 키 (showShipName, showSpeedVector, showShipSize, showSignalStatus) + */ + toggleLabelOption: (optionKey) => { + set((state) => ({ + labelOptions: { + ...state.labelOptions, + [optionKey]: !state.labelOptions[optionKey], + }, + })); + }, + + /** + * 선명표시 옵션 직접 설정 + * @param {Object} options - 옵션 객체 + */ + setLabelOptions: (options) => { + set((state) => ({ + labelOptions: { + ...state.labelOptions, + ...options, + }, + })); + }, + + /** + * 선박 통합 모드 토글 + * 통합 모드 On: isPriority=1인 선박만 표시 + * 통합 모드 Off: 모든 선박 표시 + */ + toggleIntegrate: () => { + const newMode = !get().isIntegrate; + get().syncSelectedWithIntegrateMode(newMode); + set({ isIntegrate: newMode }); + // 필터 변경 시 즉시 카운트 재계산 + get().recalculateCounts(); + }, + + /** + * 선박 선택 + * @param {string|null} featureId - 선택할 선박 ID (null이면 선택 해제, signalSourceCode + targetId) + */ + selectShip: (featureId) => { + set({ selectedShipId: featureId }); + }, + + /** + * Ctrl+Drag 다중 선택 설정 + * @param {Array} ids - featureId 배열 + */ + setSelectedShipIds: (ids) => set({ selectedShipIds: ids }), + + /** + * 다중 선택 해제 + */ + clearSelectedShips: () => set({ selectedShipIds: [] }), + + /** + * 컨텍스트 메뉴 열기 + * @param {{ x: number, y: number, ships: Array }} info + */ + openContextMenu: (info) => set({ contextMenu: info }), + + /** + * 컨텍스트 메뉴 닫기 + */ + closeContextMenu: () => set({ contextMenu: null }), + + /** + * 통합모드 전환 시 selectedShipIds 동기화 + * 참조: mda-react-front/src/shared/model/deckStore.ts - syncSelectedFeaturesWithIntegrateMode + * @param {boolean} toIntegrateMode - 전환 후 통합모드 ON 여부 + */ + syncSelectedWithIntegrateMode: (toIntegrateMode) => { + const { selectedShipIds, features } = get(); + if (selectedShipIds.length === 0) return; + + const EQUIPMENT_MAP = [ + { index: 0, signalSourceCode: '000001', dataKey: 'ais' }, + { index: 1, signalSourceCode: '000003', dataKey: 'vpass' }, + { index: 2, signalSourceCode: '000002', dataKey: 'enav' }, + { index: 3, signalSourceCode: '000004', dataKey: 'vtsAis' }, + { index: 4, signalSourceCode: '000016', dataKey: 'dMfHf' }, + // index 5 = VTS-Radar → 확장 시 제외 + ]; + + if (toIntegrateMode) { + // OFF → ON: 개별 장비 → 대표(isPriority) 선박으로 축소 + const newIds = []; + const seenTargetIds = new Set(); + + selectedShipIds.forEach((fid) => { + const ship = features.get(fid); + if (!ship) return; + + if (!ship.integrate) { + newIds.push(fid); + return; + } + + const tid = ship.targetId; + if (seenTargetIds.has(tid)) return; + seenTargetIds.add(tid); + + let priorityFid = null; + features.forEach((s, id) => { + if (s.targetId === tid && s.isPriority) priorityFid = id; + }); + newIds.push(priorityFid || fid); + }); + + set({ selectedShipIds: newIds }); + } else { + // ON → OFF: 대표 선박 → isActive인 개별 장비로 확장 + const newIds = []; + + selectedShipIds.forEach((fid) => { + const ship = features.get(fid); + if (!ship) return; + + if (!ship.integrate || !ship.isPriority) { + newIds.push(fid); + return; + } + + const parts = ship.targetId.split('_'); + let expanded = false; + + EQUIPMENT_MAP.forEach(({ index, signalSourceCode, dataKey }) => { + const equipTargetId = parts[index]; + if (!equipTargetId) return; + if (ship[dataKey] !== '1') return; + + const equipFeatureId = signalSourceCode + equipTargetId; + if (features.has(equipFeatureId)) { + newIds.push(equipFeatureId); + expanded = true; + } + }); + + if (!expanded) newIds.push(fid); + }); + + set({ selectedShipIds: newIds }); + } + }, + + /** + * 호버 정보 설정 + * @param {Object|null} info - { ship, x, y } 또는 null + */ + setHoverInfo: (info) => { + set({ hoverInfo: info }); + }, + + /** + * 상세 모달 열기 (최대 3개, 4번째부터 FIFO 제거) + * 새 모달은 마지막 모달의 현재 위치 기준 우측 140px 오프셋으로 생성 + * 참조: mda-react-front/src/shared/model/deckStore.ts - setAddDetailModal + * @param {Object} ship - 선박 데이터 + */ + openDetailModal: (ship) => { + set((state) => { + // 이미 열린 동일 선박 모달이면 무시 + if (state.detailModals.some((m) => m.id === ship.featureId)) { + return state; + } + + // 새 모달 초기 위치: 마지막 모달 위치 + 140px 우측 + const basePos = state.lastModalPos || { x: 0, y: 100 }; + const initialPos = { x: basePos.x + 140, y: basePos.y }; + + const newModal = { ship, id: ship.featureId, initialPos }; + let modals = [...state.detailModals, newModal]; + + // 3개 초과 시 가장 오래된 모달 제거 + if (modals.length > 3) { + modals = modals.slice(modals.length - 3); + } + + return { + detailModals: modals, + lastModalPos: initialPos, + }; + }); + }, + + /** + * 모달 위치 업데이트 (드래그 후 호출) + * @param {string} modalId - 모달 ID + * @param {{ x: number, y: number }} pos - 현재 위치 + */ + updateModalPos: (modalId, pos) => { + set({ lastModalPos: pos }); + }, + + /** + * 특정 상세 모달 닫기 + * @param {string} modalId - 모달 ID (featureId) + */ + closeDetailModal: (modalId) => { + set((state) => ({ + detailModals: state.detailModals.filter((m) => m.id !== modalId), + })); + }, + + /** + * 모든 상세 모달 닫기 + */ + closeAllDetailModals: () => { + set({ detailModals: [], lastModalPos: null }); + }, + + /** + * STOMP 연결 상태 설정 + * @param {boolean} connected - 연결 상태 + */ + setConnected: (connected) => { + set({ isConnected: connected }); + }, + + /** + * 범례 표시 토글 + */ + toggleShowLegend: () => { + set((state) => ({ showLegend: !state.showLegend })); + }, + + /** + * 모든 선박 데이터 초기화 + */ + clearFeatures: () => { + // 캐시도 초기화 + countCache.counts = null; + countCache.lastCalcTime = 0; + countCache.lastFilterHash = ''; + + set({ + features: new Map(), + darkSignalIds: new Set(), + kindCounts: { ...initialKindCounts }, + changedIds: new Set(), + selectedShipId: null, + selectedShipIds: [], + contextMenu: null, + totalCount: 0, + darkSignalCount: 0, + }); + }, + + /** + * 변경 ID 초기화 (렌더링 후 호출) + */ + clearChangedIds: () => { + set({ changedIds: new Set() }); + }, + + /** + * 선박 종류별 카운트 직접 설정 (서버 count 토픽용) + * @param {Object} counts - 종류별 카운트 객체 + */ + setKindCounts: (counts) => { + const totalCount = Object.values(counts).reduce((sum, count) => sum + count, 0); + set({ + kindCounts: { ...initialKindCounts, ...counts }, + totalCount, + }); + }, + + // ===================== + // 셀렉터 (Selectors) + // ===================== + + /** + * 표시 가능한 선박 목록 (필터 적용) + * @returns {Array} 필터링된 선박 배열 + */ + getVisibleShips: () => { + const state = get(); + if (!state.isShipVisible) return []; + + const { features, darkSignalIds, kindVisibility, sourceVisibility, darkSignalVisible } = state; + const result = []; + + features.forEach((ship, featureId) => { + // 다크시그널은 독립 필터 (선종/신호원/국적 필터 무시) + if (darkSignalIds.has(featureId)) { + if (darkSignalVisible) result.push(ship); + return; + } + + // 선박 종류 필터 + if (!kindVisibility[ship.signalKindCode]) return; + + // 신호원 필터 + if (!sourceVisibility[ship.signalSourceCode]) return; + + result.push(ship); + }); + + return result; + }, + + /** + * 선택된 선박 정보 + * @returns {Object|null} 선박 데이터 또는 null + */ + getSelectedShip: () => { + const { features, selectedShipId } = get(); + return selectedShipId ? features.get(selectedShipId) : null; + }, + + /** + * 선택된 모든 선박 정보 (하이라이트 표시용) + * selectedShipIds(박스선택) + detailModals(상세모달) 통합 + * @returns {Array} 선박 데이터 배열 + */ + getSelectedShips: () => { + const { features, selectedShipIds, detailModals } = get(); + const result = []; + const seen = new Set(); + + selectedShipIds.forEach((fid) => { + const ship = features.get(fid); + if (ship && !seen.has(fid)) { + result.push(ship); + seen.add(fid); + } + }); + + detailModals.forEach((m) => { + if (m.ship && !seen.has(m.id)) { + result.push(m.ship); + seen.add(m.id); + } + }); + + return result; + }, + + /** + * CSV 다운로드용 선박 목록 (필터 적용) + * - 레이더 항상 제외 + * - 통합 모드: isPriority만 포함 + * - 다크시그널: 독립 필터 적용 + * - 일반: 선종/신호원/국적 필터 적용 + * @returns {Array} 다운로드용 선박 배열 (downloadTargetId 포함) + */ + getDownloadShips: () => { + const state = get(); + const { + features, darkSignalIds, kindVisibility, sourceVisibility, nationalVisibility, + isIntegrate, darkSignalVisible, + } = state; + const result = []; + + features.forEach((ship, featureId) => { + // 레이더 항상 제외 + if (ship.signalSourceCode === '000005') return; + + // 통합 모드: isPriority만 포함 + if (isIntegrate && ship.integrate && !ship.isPriority) return; + + const downloadTargetId = isIntegrate ? ship.targetId : ship.originalTargetId; + + // 다크시그널: 독립 필터 + if (darkSignalIds.has(featureId)) { + if (darkSignalVisible) { + result.push({ ...ship, downloadTargetId }); + } + return; + } + + // 선종 필터 + if (!kindVisibility[ship.signalKindCode]) return; + // 신호원 필터 + if (!sourceVisibility[ship.signalSourceCode]) return; + // 국적 필터 + const mapped = mapNationalCode(ship.nationalCode); + if (!nationalVisibility[mapped]) return; + + result.push({ ...ship, downloadTargetId }); + }); + + return result; + }, +}))); + +export default useShipStore; diff --git a/src/stores/uiStore.js b/src/stores/uiStore.js new file mode 100644 index 00000000..52211765 --- /dev/null +++ b/src/stores/uiStore.js @@ -0,0 +1,28 @@ +import { create } from 'zustand'; + +/** + * UI 상태 관리 스토어 + */ +export const useUIStore = create((set) => ({ + // 사이드패널 열림 상태 + isPanelOpen: true, + togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })), + setPanel: (isOpen) => set({ isPanelOpen: isOpen }), + + // 활성 메뉴 + activeMenu: 'ship', + setActiveMenu: (menu) => set({ activeMenu: menu }), + + // 로딩 상태 + isLoading: false, + setLoading: (loading) => set({ isLoading: loading }), + + // 모달 상태 + modals: {}, + openModal: (modalId, data = null) => set((state) => ({ + modals: { ...state.modals, [modalId]: { isOpen: true, data } }, + })), + closeModal: (modalId) => set((state) => ({ + modals: { ...state.modals, [modalId]: { isOpen: false, data: null } }, + })), +})); diff --git a/src/types/constants.js b/src/types/constants.js new file mode 100644 index 00000000..37e02d2e --- /dev/null +++ b/src/types/constants.js @@ -0,0 +1,300 @@ +/** + * 선박 신호원 및 종류 상수 정의 + * 참조: mda-react-front/src/types/constants.ts + * 참조: mda-stomp-bridge/docs/guide/frontend-guide.html + */ + +// ===================== +// 신호원 코드 (Signal Source Code) +// ===================== +export const SIGNAL_SOURCE_CODE_AIS = '000001'; +export const SIGNAL_SOURCE_CODE_ENAV = '000002'; +export const SIGNAL_SOURCE_CODE_VPASS = '000003'; +export const SIGNAL_SOURCE_CODE_VTS_AIS = '000004'; +export const SIGNAL_SOURCE_CODE_RADAR = '000005'; +export const SIGNAL_SOURCE_CODE_D_MF_HF = '000016'; + +// ===================== +// 선박 종류 코드 (Ship Kind Code) +// ===================== +export const SIGNAL_KIND_CODE_FISHING = '000020'; // 어선 +export const SIGNAL_KIND_CODE_KCGV = '000021'; // 경비함정 +export const SIGNAL_KIND_CODE_PASSENGER = '000022'; // 여객선 +export const SIGNAL_KIND_CODE_CARGO = '000023'; // 화물선 +export const SIGNAL_KIND_CODE_TANKER = '000024'; // 유조선 +export const SIGNAL_KIND_CODE_GOV = '000025'; // 관공선 +export const SIGNAL_KIND_CODE_NORMAL = '000027'; // 일반 +export const SIGNAL_KIND_CODE_BUOY = '000028'; // 부이 + +// ===================== +// STOMP 메시지 배열 인덱스 +// 참조: mda-react-front/src/feature/commonFeature.ts - deckSplitCommonTarget() +// ===================== +export const SHIP_MSG_INDEX = { + TARGET_ID: 0, // 통합 TARGET_ID (AIS_VPASS_ENAV_VTSAIS_DMFHF 형식) + RECV_DATE_TIME: 1, // 수신 시간 + SIGNAL_SOURCE_CODE: 2, // 신호원 코드 + COG: 3, // 침로 (degrees) + SOG: 4, // 속력 (knots) + LONGITUDE: 5, // 경도 + LATITUDE: 6, // 위도 + SHIP_NAME: 7, // 선박명 + SHIP_TYPE: 8, // 선박 타입 + DIM_A: 9, + DIM_B: 10, + DIM_C: 11, + DIM_D: 12, + DEPARTMENT: 13, + ALTITUDE: 14, + VTS_CODE: 15, + LOST: 16, // 1=lost, 0=정상 + HAZARDOUS_CATEGORY: 17, // 위험물 카테고리 + PERMISSION: 18, // 허가 여부 + AIS: 19, // AIS 통합 플래그 + VPASS: 20, // VPASS 통합 플래그 + ENAV: 21, // ENAV 통합 플래그 + VTS_AIS: 22, // VTS_AIS 통합 플래그 + D_MF_HF: 23, // D_MF_HF 통합 플래그 + VTS_RADAR: 24, // RADAR 통합 플래그 + NEAR_VTS_CODE: 25, + INTEGRATE: 26, // 통합 여부 + SIGNAL_KIND_CODE: 27, // 선종 코드 + DRAUGHT: 28, // 흘수 + IMO: 29, // IMO 번호 + IS_NON_PERMISSION: 30, + IS_ABROAD_GOV: 31, + IS_MMSI_CHANGED: 32, + IS_EEZ_CONTACTED: 33, + IS_NORTH_KOREA: 34, + NATIONAL_CODE: 35, // 국적 코드 + IS_PRIORITY: 36, // 1=priority (통합선박에서 대표 신호원) + ORIGINAL_TARGET_ID: 37, // 개별 장비 고유 TARGET_ID +}; + +// ===================== +// 선박 종류별 한글 라벨 +// ===================== +export const SHIP_KIND_LABELS = { + [SIGNAL_KIND_CODE_FISHING]: '어선', + [SIGNAL_KIND_CODE_KCGV]: '경비함정', + [SIGNAL_KIND_CODE_PASSENGER]: '여객선', + [SIGNAL_KIND_CODE_CARGO]: '화물선', + [SIGNAL_KIND_CODE_TANKER]: '유조선', + [SIGNAL_KIND_CODE_GOV]: '관공선', + [SIGNAL_KIND_CODE_NORMAL]: '일반', + [SIGNAL_KIND_CODE_BUOY]: '부이', +}; + +// ===================== +// 신호원별 한글 라벨 +// ===================== +export const SIGNAL_SOURCE_LABELS = { + [SIGNAL_SOURCE_CODE_AIS]: 'AIS', + [SIGNAL_SOURCE_CODE_ENAV]: 'e-Nav', + [SIGNAL_SOURCE_CODE_VPASS]: 'V-PASS', + [SIGNAL_SOURCE_CODE_VTS_AIS]: 'VTS AIS', + [SIGNAL_SOURCE_CODE_RADAR]: 'RADAR', + [SIGNAL_SOURCE_CODE_D_MF_HF]: 'D MF/HF', +}; + +// ===================== +// AVETDR 플래그 설정 +// A=AIS, V=VPASS, E=ENAV, T=VTS_AIS, D=D_MF_HF, R=RADAR +// 참조: mda-react-front/src/util/realTimeLayerUtil.ts (라인 733-776) +// ===================== +export const SIGNAL_FLAG_CONFIGS = [ + { + key: 'A', + name: 'AIS', + activeColor: '#C2A7DC', + inactiveColor: '#CFCFCF', + signalSourceCode: SIGNAL_SOURCE_CODE_AIS, + dataKey: 'ais', + }, + { + key: 'V', + name: 'V-Pass', + activeColor: '#8FAEFC', + inactiveColor: '#CFCFCF', + signalSourceCode: SIGNAL_SOURCE_CODE_VPASS, + dataKey: 'vpass', + }, + { + key: 'E', + name: 'E-Nav', + activeColor: '#74B2F0', + inactiveColor: '#CFCFCF', + signalSourceCode: SIGNAL_SOURCE_CODE_ENAV, + dataKey: 'enav', + }, + { + key: 'T', + name: 'VTS-AIS', + activeColor: '#4190DF', + inactiveColor: '#CFCFCF', + signalSourceCode: SIGNAL_SOURCE_CODE_VTS_AIS, + dataKey: 'vtsAis', + }, + { + key: 'D', + name: 'D-MF/HF', + activeColor: '#459EF6', + inactiveColor: '#CFCFCF', + signalSourceCode: SIGNAL_SOURCE_CODE_D_MF_HF, + dataKey: 'dMfHf', + }, + { + key: 'R', + name: 'VTS-RT', + activeColor: '#4577F6', + inactiveColor: '#CFCFCF', + signalSourceCode: SIGNAL_SOURCE_CODE_RADAR, + dataKey: 'vtsRadar', + }, +]; + +// 레거시 호환 +export const AVETDR_COLORS = { + A: { active: '#C2A7DC', inactive: '#444' }, + V: { active: '#8FAEFC', inactive: '#444' }, + E: { active: '#74B2F0', inactive: '#444' }, + T: { active: '#4190DF', inactive: '#444' }, + D: { active: '#459EF6', inactive: '#444' }, + R: { active: '#4577F6', inactive: '#444' }, +}; + +// ===================== +// 선박 종류별 색상 (범례용) +// ===================== +export const SHIP_KIND_COLORS = { + [SIGNAL_KIND_CODE_FISHING]: '#00C853', // 녹색 - 어선 + [SIGNAL_KIND_CODE_KCGV]: '#FF5722', // 주황 - 경비함정 + [SIGNAL_KIND_CODE_PASSENGER]: '#2196F3', // 파랑 - 여객선 + [SIGNAL_KIND_CODE_CARGO]: '#9C27B0', // 보라 - 화물선 + [SIGNAL_KIND_CODE_TANKER]: '#F44336', // 빨강 - 유조선 + [SIGNAL_KIND_CODE_GOV]: '#FF9800', // 오렌지 - 관공선 + [SIGNAL_KIND_CODE_NORMAL]: '#607D8B', // 회색 - 일반 + [SIGNAL_KIND_CODE_BUOY]: '#795548', // 갈색 - 부이 +}; + +// ===================== +// 속력 임계값 (정지/이동 판단) +// ===================== +export const SPEED_THRESHOLD = 1; // knots (메인 프로젝트 기준) + +// ===================== +// 아이콘 아틀라스 매핑 (atlas.png 스프라이트 시트) +// 참조: mda-react-front/src/types/constants.ts +// ===================== +export const ICON_ATLAS_MAPPING = { + // 이동 중인 선박 아이콘 (화살표 형태) + fishingImg: { x: 1, y: 518, width: 16, height: 27 }, + kcgvImg: { x: 45, y: 115, width: 17, height: 27 }, + passImg: { x: 24, y: 486, width: 17, height: 27 }, + cargoImg: { x: 44, y: 144, width: 17, height: 27 }, + hazardImg: { x: 44, y: 173, width: 17, height: 27 }, // 유조선 + govImg: { x: 43, y: 486, width: 17, height: 27 }, + etcImg: { x: 24, y: 515, width: 17, height: 27 }, + bouyImg: { x: 1, y: 485, width: 21, height: 31 }, + + // 정지 선박 아이콘 (원형) + fishingStopImg: { x: 51, y: 51, width: 8, height: 8 }, + kcgvStopImg: { x: 51, y: 41, width: 8, height: 8 }, + passStopImg: { x: 51, y: 21, width: 8, height: 8 }, + cargoStopImg: { x: 51, y: 1, width: 8, height: 8 }, + hazardStopImg: { x: 51, y: 11, width: 8, height: 8 }, + govStopImg: { x: 51, y: 31, width: 8, height: 8 }, + etcStopImg: { x: 51, y: 71, width: 8, height: 8 }, + + // 위험물 선박 아이콘 + fishingHazardImg: { x: 33, y: 415, width: 30, height: 69 }, + kcgvHazardImg: { x: 1, y: 414, width: 30, height: 69 }, + passHazardImg: { x: 1, y: 343, width: 30, height: 69 }, + cargoHazardImg: { x: 33, y: 273, width: 30, height: 69 }, + tankerHazardImg: { x: 1, y: 272, width: 30, height: 69 }, + govHazardImg: { x: 33, y: 202, width: 30, height: 69 }, + etcHazardImg: { x: 33, y: 344, width: 30, height: 69 }, + + // 소실 신호 아이콘 + lostShipImg: { x: 43, y: 515, width: 16, height: 28 }, + + // 기타 아이콘 + fixedAssetImg: { x: 1, y: 59, width: 48, height: 54 }, + airCraftImg: { x: 1, y: 1, width: 48, height: 56 }, + kcgvAssetImg: { x: 1, y: 115, width: 42, height: 26 }, + rotationAssetImg: { x: 1, y: 143, width: 41, height: 26 }, + helicopterImg: { x: 1, y: 171, width: 41, height: 26 }, +}; + +// ===================== +// 선종별 이동 아이콘 매핑 +// ===================== +export const ICON_MAPPING_KIND_MOVING = { + [SIGNAL_KIND_CODE_FISHING]: 'fishingImg', + [SIGNAL_KIND_CODE_KCGV]: 'kcgvImg', + [SIGNAL_KIND_CODE_PASSENGER]: 'passImg', + [SIGNAL_KIND_CODE_CARGO]: 'cargoImg', + [SIGNAL_KIND_CODE_TANKER]: 'hazardImg', + [SIGNAL_KIND_CODE_GOV]: 'govImg', + [SIGNAL_KIND_CODE_NORMAL]: 'etcImg', + [SIGNAL_KIND_CODE_BUOY]: 'bouyImg', +}; + +// ===================== +// 선종별 정지 아이콘 매핑 +// ===================== +export const ICON_MAPPING_KIND_STOPPING = { + [SIGNAL_KIND_CODE_FISHING]: 'fishingStopImg', + [SIGNAL_KIND_CODE_KCGV]: 'kcgvStopImg', + [SIGNAL_KIND_CODE_PASSENGER]: 'passStopImg', + [SIGNAL_KIND_CODE_CARGO]: 'cargoStopImg', + [SIGNAL_KIND_CODE_TANKER]: 'hazardStopImg', + [SIGNAL_KIND_CODE_GOV]: 'govStopImg', + [SIGNAL_KIND_CODE_NORMAL]: 'etcStopImg', + [SIGNAL_KIND_CODE_BUOY]: 'bouyImg', +}; + +// ===================== +// 국적 코드 (National Code) +// ===================== +export const NATIONAL_CODE_KR = 'KR'; // 한국 +export const NATIONAL_CODE_CN = 'CN'; // 중국 +export const NATIONAL_CODE_JP = 'JP'; // 일본 +export const NATIONAL_CODE_KP = 'KP'; // 북한 +export const NATIONAL_CODE_OTHER = 'OTHER'; // 기타 + +// ===================== +// STOMP 토픽 +// ===================== +export const STOMP_TOPICS = { + SHIP: '/topic/ship', + SHIP_THROTTLED: '/topic/ship-throttled-', // + {N}s + COUNT: '/topic/count', + SHIP_DELETE: '/topic/ship-delete', +}; + +// ===================== +// 기본 선박 종류 목록 (필터/범례용) +// ===================== +export const SHIP_KIND_LIST = [ + { code: SIGNAL_KIND_CODE_FISHING, label: '어선' }, + { code: SIGNAL_KIND_CODE_KCGV, label: '경비함정' }, + { code: SIGNAL_KIND_CODE_PASSENGER, label: '여객선' }, + { code: SIGNAL_KIND_CODE_CARGO, label: '화물선' }, + { code: SIGNAL_KIND_CODE_TANKER, label: '유조선' }, + { code: SIGNAL_KIND_CODE_GOV, label: '관공선' }, + { code: SIGNAL_KIND_CODE_NORMAL, label: '일반' }, + { code: SIGNAL_KIND_CODE_BUOY, label: '부이' }, +]; + +// ===================== +// 신호원 목록 (필터용) +// ===================== +export const SIGNAL_SOURCE_LIST = [ + { code: SIGNAL_SOURCE_CODE_AIS, label: 'AIS' }, + { code: SIGNAL_SOURCE_CODE_VPASS, label: 'V-PASS' }, + { code: SIGNAL_SOURCE_CODE_ENAV, label: 'e-Nav' }, + { code: SIGNAL_SOURCE_CODE_VTS_AIS, label: 'VTS AIS' }, + { code: SIGNAL_SOURCE_CODE_D_MF_HF, label: 'D MF/HF' }, + { code: SIGNAL_SOURCE_CODE_RADAR, label: 'RADAR' }, +]; diff --git a/src/utils/csvDownload.js b/src/utils/csvDownload.js new file mode 100644 index 00000000..69f6acf8 --- /dev/null +++ b/src/utils/csvDownload.js @@ -0,0 +1,132 @@ +/** + * CSV 다운로드 유틸리티 + * 참조: mda-react-front/src/widgets/rightMenu/ui/RightMenu.tsx (512-579) + */ +import { Polygon } from 'ol/geom'; +import { shipTypeMap } from '../assets/data/shiptype'; +import { SHIP_KIND_LABELS, SIGNAL_SOURCE_LABELS } from '../types/constants'; + +// 해구도 데이터 캐시 (첫 호출 시만 로딩) +let trenchCache = null; + +/** + * 해구도 폴리곤 데이터 로딩 (동적 import, 캐시) + */ +async function loadTrenchData() { + if (trenchCache) return trenchCache; + const data = await import('../assets/data/largeTrench.json'); + const geojson = data.default || data; + trenchCache = geojson.features.map((f) => ({ + zoneName: f.properties.zone_name, + polygon: new Polygon(f.geometry.coordinates), + })); + return trenchCache; +} + +/** + * 선박 좌표 → 해구도 번호 일괄 조회 + * @param {Array} ships - 선박 배열 (longitude, latitude 필드 필요) + * @returns {Map} index → zone_name 매핑 + */ +async function lookupTrenchNumbers(ships) { + const trenchData = await loadTrenchData(); + const result = new Map(); + + ships.forEach((ship, idx) => { + const lon = parseFloat(ship.longitude); + const lat = parseFloat(ship.latitude); + if (isNaN(lon) || isNaN(lat)) { + result.set(idx, 'X'); + return; + } + + let found = false; + for (const { zoneName, polygon } of trenchData) { + if (polygon.intersectsCoordinate([lon, lat])) { + result.set(idx, zoneName); + found = true; + break; + } + } + if (!found) { + result.set(idx, 'X'); + } + }); + + return result; +} + +/** + * 수신시간 포맷 변환 + * "YYYYMMDDHHmmss" → "YYYY-MM-DD HH:mm:ss" + */ +function formatRecvDateTime(raw) { + if (!raw || raw.length < 14) return raw || ''; + return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)} ${raw.slice(8, 10)}:${raw.slice(10, 12)}:${raw.slice(12, 14)}`; +} + +/** + * CSV 안전 필드 (쌍따옴표 감싸기) + */ +function csvField(val) { + const str = val == null ? '' : String(val); + return `"${str.replace(/"/g, '""')}"`; +} + +/** + * CSV 문자열 생성 + * @param {Array} ships - 선박 배열 + * @param {Map} trenchMap - index → zone_name 매핑 + * @returns {string} CSV 문자열 (BOM 포함) + */ +function buildCsvString(ships, trenchMap) { + const BOM = '\uFEFF'; + const headers = [ + '타겟ID', '선박명', '선종/기종', '선종/기종-유형', '신호', + 'SOG', 'COG', '경도', '위도', '흘수', '수신시간', '해구도', + ]; + + const rows = ships.map((ship, idx) => { + const fields = [ + csvField(ship.downloadTargetId), + csvField(ship.shipName), + csvField(SHIP_KIND_LABELS[ship.signalKindCode] || ship.signalKindCode), + csvField(shipTypeMap.get(String(ship.shipType)) || ship.shipType), + csvField(SIGNAL_SOURCE_LABELS[ship.signalSourceCode] || ship.signalSourceCode), + csvField(ship.sog), + csvField(ship.cog), + csvField(ship.longitude), + csvField(ship.latitude), + csvField(ship.draught), + csvField(formatRecvDateTime(ship.receivedTime)), + csvField(trenchMap.get(idx) || 'X'), + ]; + return fields.join(','); + }); + + return BOM + headers.map(csvField).join(',') + '\n' + rows.join('\n'); +} + +/** + * CSV 다운로드 트리거 + * @param {Array} ships - getDownloadShips()에서 반환된 선박 배열 + */ +export async function downloadShipCsv(ships) { + const trenchMap = await lookupTrenchNumbers(ships); + const csvString = buildCsvString(ships, trenchMap); + + const blob = new Blob([csvString], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + + const now = new Date(); + const pad = (n) => String(n).padStart(2, '0'); + const fileName = `ship_download_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}.csv`; + + const link = document.createElement('a'); + link.href = url; + link.download = fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 00000000..1ec9d56f --- /dev/null +++ b/vite.config.js @@ -0,0 +1,60 @@ +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default ({ mode }) => { + const env = loadEnv(mode, process.cwd(), ''); + + return defineConfig({ + server: { + host: true, + port: 3000, + proxy: { + // 지도 타일 서버 + '/MAPS': { + target: env.VITE_MAP_TILE_URL || 'http://10.26.252.39:9090', + changeOrigin: true, + secure: false, + }, + // GeoJSON 데이터 + '/geo': { + target: env.VITE_MAP_TILE_URL || 'http://10.26.252.39:9090', + changeOrigin: true, + secure: false, + }, + // 선박 신호 API (signal-api) + // 참조: mda-react-front/vite.config.ts + '/signal-api': { + target: env.VITE_SIGNAL_API || 'http://10.26.252.39:9090/signal-api', + changeOrigin: true, + secure: false, + rewrite: (path) => path.replace(/^\/signal-api/, ''), + }, + // API 서버 + '/api': { + target: env.VITE_API_URL || 'http://localhost:8080', + changeOrigin: true, + secure: false, + }, + }, + }, + plugins: [react()], + resolve: { + alias: { + '@': '/src', + }, + }, + build: { + outDir: 'dist', + cssCodeSplit: true, + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom', 'react-router-dom'], + map: ['ol', 'ol-ext'], + state: ['zustand'], + }, + }, + }, + }, + }); +};