'use strict' /** * Copyright (c) 2017~2022, OBCon Inc. * All rights reserved. */ /** * @file * @copyright 2017~2022, OBCon Inc. * @author gye hyun james kim [pnuskgh@gmail.com] */ //-------------------------------------------------------------------------------------------------- //--- 사용하는 좌표계 : https://hmjkor.tistory.com/483 //--- OBCon SCADA : EPSG:4326 : WGS84 좌표계. 위/경도 좌표계 //--- vWorld : EPSG:3857 (EPSG:900913) : 구글 Mercator(메카르도) 좌표계 //--- KakaoMap : WGS84 좌표계 //--- OpenLayers : EPSG:4326 : WGS84 좌표계. 위/경도 좌표계 //--- OpenLayers 3 : EPSG:3857 //--- //--- Korea에서 사용하는 좌표계 (https://www.osgeo.kr/17) //--- Korean 2000 : GRS80 경위도, EPSG:4019, EPSG:4737 (WGS84와 거의 유사) //--- GRS80 UTM-K : EPSG:5179, 네이버지도에서 사용중인 좌표계 //--- Bessel UTM-K : EPSG:5178, 새 주소지도에서 사용중 //--- Korean 1995 : WGS84 경위도, EPSG:4326, EPSG:4166 //--- Korean 1985 : Bessel 1841 경위도, EPSG:4004, EPSG:4162 //--- //--- 국토지리정보원(www.ngii.go.kr) 등에서 사용하는 좌표계 (http://www.juso.go.kr/) //--- GRS80 UTM-K(한국형 단일평면직각좌표계) : EPSG:5179 //--- 국토정보플랫폼 표준 좌표계 //--- 서부원점 : EPSG:5185 //--- 중부원점 : EPSG:5186 //--- 동부원점 : EPSG:5187 //--- 동해(울륭)원점 : EPSG:5188 //--- 좌표계 변환 프로그램 : https://wwhitelife.tistory.com/55 //--- //--- 좌표계 (https://www.slideshare.net/ybh0616/gis-2-93095088) //--- EPSG (European Petroleum Survey Group, 유럽석유조사그룹), https://epsg.io/ //--- https://www.google.com/maps/dir/37.575996,126.9769286//@37.5757392,126.9758296,18z 에서 위도, 경도 확인 가능 //--- CRS (Coordinate Reference System, 좌표참조체계) //--- GCS (Geographic Coordinate System, 지리좌표계) //--- 위도 (latitude) : 적도(0)가 기준, 북극(90), 남극(-90) //--- 경도 (longitude) : 본초 자오선(Prime Meridian)이 기준. 서경 -180 ~ 동경 180 //--- Prime Meridian : 영국 런던 그리니치 구 왕립 천문대를 지나는 자오선 //--- 대한민국 : 북위 33 ~ 43, 동경 124 ~ 132 //--- Bessel 1841, WGS84 (World Geodetic System 1984) //--- GRS80 (Geodetic Reference System 1980) //--- PCS (Projected Coordinate System, 투영좌표계) //--- SRS (Spatial Reference System, 공간참조체계) //-------------------------------------------------------------------------------------------------- //--- 지도 라이브러리 //--- vWorld //--- https://www.vworld.kr/dev/v4api.do //--- KakaoMap //--- //--- SGIS //--- https://sgis.kostat.go.kr/developer/html/newOpenApi/api/mapApi/ready.html //--- QGIS //--- https://snugis.tistory.com/category/ArcMap%20%26%20QGIS%20%EA%B0%95%EC%A2%8C //--- Proj4 : 좌표계 변환 프로그램 //--- https://wwhitelife.tistory.com/55 //-------------------------------------------------------------------------------------------------- /* - SharpFile 구조 * ~.shp : binary : 모양 형식 * ~.shx : binary : 모양 형식 인덱스 * ~.dbf : binary : 속성 형식 * ~.prj : string : 좌표 참조 시스템 "PROJCS > GEOGCS > DATUM > SPHEROID"의 첫번째 항목 : 좌표계 (예, GRS_1980) * ~.csf : binary : ??? * ~.cpg : string : 문자셋 * ~.sbn, sbx : binary : 기능의 공간 인덱스 x ~.fbn, fbx : 읽기 전용 기능의 공간 인덱스 x ~.ain, aih : 테이블에서 활성 필드의 속성 인덱스 * ~.shp.xml : string : 지리 공간 메타 데이터 (ISO 19115 또는 기타 XML 스키마 형식) x ~.ixs : 읽기/쓰기 데이터 세트에 대한 지오 코딩 색인 (ODB 형식) x ~.atx : ArcGIS 8 이상에서 .dbf 파일 속성 색인 x ~.qix : MapServer 및 GDAL/OGR 소프트웨어에서 사용하는 대체 쿼드 트리 공간 인덱스 - SharpFile - SharpFile To GeoJson - https://github.com/mbostock/shapefile - https://www.npmjs.com/package/shapefile - GeoJson - https://junghan92.medium.com/d3-geo-topojson-canvas%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A7%B5-%EC%B0%A8%ED%8A%B8-%EA%B7%B8%EB%A6%AC%EA%B8%B0-ffab54ec23bf - GeoJson to TopoJson - https://www.npmjs.com/package/topojson-server - https://github.com/topojson/topojson - TopoJson : GeoJson의 확장 - GeoJson에 비해 1/10 크기 - KakaoMap - WGS84 좌표계 사용 - https://apis.map.kakao.com/web/sample/drawShape/ - https://apis.map.kakao.com/web/documentation/#Polygon */ const fs = require('fs'); const path = require('path'); const iconv = require('iconv-lite'); const proj4 = require('proj4'); const shapefile = require("shapefile"); class FileInfo { constructor(folderRoot, folder=null, filename=null, ext=null) { this._folderRoot = folderRoot; this._folder = folder; this._ext = ext this.filename = filename; } get folderRoot() { return this._folderRoot; } set folderRoot(newValue) { this._folderRoot = newValue; } get folder() { return this._folder; } set folder(newValue) { this._folder = newValue; } get filename() { return this._filename; } set filename(newValue) { if (newValue == null) { this._filename = newValue; } else { let tmpStrings = newValue.split('.'); if (tmpStrings.length == 2) { this._filename = tmpStrings[0]; this._ext = tmpStrings[1]; } else { this._filename = newValue; } } } get ext() { return this._ext; } set ext(newValue) { this._ext = newValue; } _getFile() { if (this._filename == null) { return null; } else { return (this._ext == null) ? this._filename:`${this._filename}.${this._ext}`; } } getFilename() { let filename = this._getFile(); if (filename == null) { return this._folder; } else { return (this._folder == null) ? filename:path.join(this._folder, filename); } } getFilenameFull() { let filename = this.getFilename(); if (filename == null) { return this._folderRoot; } else { return (this._folderRoot == null) ? this.getFilename():path.join(this._folderRoot, this.getFilename()); } } getFolderFull() { if (this._folderRoot == null) { return this._folder; } else { return (this._folder == null) ? this._folderRoot:path.join(this._folderRoot, this._folder); } } makeFolders() { if (this._folder == null) { return; } else { let folder = path.join(this._folderRoot); this._folder.split(path.sep).forEach(name => { folder = path.join(folder, name); if (!fs.existsSync(folder)) { fs.mkdirSync(folder); } }); } } deleteAllFile() { let folder = this.getFolderFull(); if (folder != null) { let files = fs.readdirSync(folder); files.forEach(function(file) { if (fs.statSync(path.join(folder, file)).isDirectory()) { fs.rmSync(path.join(folder, file), { recursive: true, force: true }) } else { fs.unlinkSync(path.join(folder, file)); } }.bind(this)); } } } class SharpFile { constructor() { //--- SharpFile : files/map/${serviceName}/site_${siteID}/SharpFile/${folder}/~.shp //--- GeoJson : files/map/${serviceName}/site_${siteID}/GeoJson/${folder}/~.geojson //--- KakaoMap : themes/${service}/${theme}/devices/KakaoMap/site_${siteID}/~.js this._folder = 'files/map'; //--- 지도 관련 파일이 저장되는 폴더 this._coordinates = []; //--- 좌표 변환에 사용되는 좌표계 this._messages = []; this.initialize(); } initialize() { this.init_proj4(); } //--- Proj4 라이브러리를 위한 설정 //--- https://wwhitelife.tistory.com/55 //--- 카카오맵에서 좌표 변환 : https://apis.map.kakao.com/web/sample/transCoord/ init_proj4() { this._coordinates = [ 'EPSG_3857', 'WGS84', 'GRS80', 'GRS_1980' ]; //--- OBCon과 vWorld에서 사용 //--- Google Mercator: EPSG:3857(공식), EPSG:900913(통칭) proj4.defs('EPSG_3857', '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs'); //--- KakaoMap에서 사용 //--- WGS84 (World Geodetic System 1984) : EPSG:4326, EPSG:4166 (Korean 1995) proj4.defs('WGS84', '+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs'); // proj4.defs('WGS84', '+proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees'); //--- GRS80 (Geodetic Reference System 1980) : EPSG:4019, EPSG:4737 (Korean 2000) // proj4.defs('GRS80', '+proj=longlat +ellps=GRS80 +no_defs'); proj4.defs('GRS80', '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m +no_defs'); // proj4.defs('GRS_1980', '+proj=longlat +ellps=GRS80 +no_defs'); // proj4.defs('GRS_1980', '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m +no_defs'); // proj4.defs('GRS_1980', '+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=500000 +ellps=GRS80 +units=m +no_defs'); proj4.defs('GRS_1980', '+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs'); // proj4.defs('TM128', '+proj=tmerc +lat_0=38 +lon_0=128 +k=0.9999 +x_0=400000 +y_0=600000 +ellps=bessel +units=m +no_defs +towgs84=-115.80,474.99,674.11,1.16,-2.31,-1.63,6.43'); // proj4.defs('UTM-K', '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m +no_defs'); // //--- Bessel 1841 : EPSG:4004, EPSG:4162 (Korean 1985) // proj4.defs('Bessel_1841', '+proj=longlat +ellps=bessel +no_defs +towgs84=-115.80,474.99,674.11,1.16,-2.31,-1.63,6.43'); // //--- UTM52N (WGS84): EPSG:32652 // proj4.defs('UTM52N', '+proj=utm +zone=52 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'); // //--- UTM51N (WGS84) : EPSG:32651 proj4.defs('UTM51N', '+proj=utm +zone=51 +ellps=WGS84 +datum=WGS84 +units=m +no_defs'); } getMessages(delimiter='') { return this._messages.join(delimiter); } //--- 서울도시가스 //--- MP: 적색, 확대시에만 표시 (LP: 청색, RP: 녹색) async sharpFileToGeoJson(service, site) { try { this._messages = []; this._messages.push('

SharpFile을 변환 한다.


'); let folderSharpFile = new FileInfo(path.join(appl.root, this._folder, service, site, 'SharpFile')); if (fs.existsSync(folderSharpFile.getFilenameFull()) == false) { //--- SharpFile에 해당하는 폴더가 없으면 종료 한다. return; } this._messages.push('

SharpFile 목록

'); this._messages.push('
'); let geoJsons = []; this._messages.push('

GeoJson 파일 목록

'); this._messages.push('
'); this._messages.push('

KakaoMap 파일 목록

'); this._messages.push('
'); } catch(err) { console.error(err); utils.obj.loggerError(err); } } _findSharpFile(fileInfo) { let fileInfos = []; let folderCurrent = fileInfo.getFolderFull(); let files = fs.readdirSync(folderCurrent); for (let idx = 0; idx < files.length; idx++) { let filename = files[idx]; let folder = path.join(folderCurrent, filename); if (fs.statSync(folder).isDirectory()) { if (filename.startsWith('zz') == false) { let folderInfo = new FileInfo(fileInfo.folderRoot, (fileInfo.folder == null) ? filename:path.join(fileInfo.folder, filename)); let tmpFileInfos = this._findSharpFile(folderInfo); fileInfos = fileInfos.concat(tmpFileInfos); } } else { if (filename.endsWith('.shp')) { fileInfos.push(new FileInfo(fileInfo.folderRoot, fileInfo.folder, filename)); } } } return fileInfos; } //--- GeoJson 파일을 사용하지 않음 async _sharpFileToGeoJson(fileInfoSharpFile, fileInfoGeoJson) { try { let source = await shapefile.open(fileInfoSharpFile.getFilenameFull()); let content = []; let geoJsons = []; while (true) { let result = await source.read(); if (result.done) { break; } //--- 한글 변환 //--- To-Do : sharpFile의 인코딩 정보를 읽어서 한글 변환에 사용 한다. //--- To-Do : properties의 속성이 문자열인 경우 한글 변환 // console.log('string', typeof(result.value.properties['GU_NM'])); result.value.properties['GU_NM'] = iconv.decode(result.value.properties['GU_NM'], 'euc-kr'); result.value.properties['DONG_NM_H'] = iconv.decode(result.value.properties['DONG_NM_H'], 'euc-kr'); //--- 좌표계 변환 for (let idx = 0; idx < result.value.geometry.coordinates.length; idx++) { //--- To-Do : 향후 sharpFile에 있는 좌표계를 읽어서 처리 한다. //--- 위도와 경도 위치 조정 let item = proj4('GRS_1980', 'WGS84', result.value.geometry.coordinates[idx]); result.value.geometry.coordinates[idx] = [ item[1], item[0] ]; } //--- https://apis.map.kakao.com/web/sample/drawShape/ content.push(JSON.stringify(result.value)); geoJsons.push(result.value); } let contents = [ 'if (typeof(geoJsons) == "undefined") {', ' var geoJsons = [];', '}', '', 'geoJsons.push([', content.join(',\n'), ']);' ]; logger.info(`GeoJson file : ${fileInfoGeoJson.getFilenameFull()}`); fs.writeFileSync(fileInfoGeoJson.getFilenameFull(), contents.join('\n'), { encoding: 'UTF-8' }); return geoJsons; } catch(err) { console.error(err); utils.obj.loggerError(err); return []; } } async _geoJsonToKakaoMap(geoJsons, fileInfo, service, site) { let content = []; try { content.push('nsOBCon.data = nsOBCon.data || {};'); content.push('nsOBCon.data.funcPolylines = nsOBCon.data.funcPolylines || [];'); geoJsons.forEach(feature => { if (feature.geometry.type == 'LineString') { let lngMin = 99999; let lngMax = -99999; let latMin = 99999; let latMax = -99999; content.push('nsOBCon.data.funcPolylines.push(function() {'); //--- https://apis.map.kakao.com/web/documentation/#Polyline content.push(' let polyline = new kakao.maps.Polyline({'); content.push(' map: null,'); content.push(' path: ['); feature.geometry.coordinates.forEach(coordinate => { lngMin = Math.min(lngMin, coordinate[1]); lngMax = Math.max(lngMax, coordinate[1]); latMin = Math.min(latMin, coordinate[0]); latMax = Math.max(latMax, coordinate[0]); content.push(` new kakao.maps.LatLng(${coordinate[0]}, ${coordinate[1]}),`); }); content.push(' ],'); content.push(' strokeWeight: 2,'); let filetype = ''; if ((service == 'service_scada_techpalm') && ((site == 'site_44') || (site == 'site_1'))) { //--- 서울도시가스 let filename = fileInfo.getFilename(); if (filename.endsWith('_MP.js')) { content.push(" strokeColor: 'red',"); filetype = 'MP'; // content.push(` filetype: 'MP',`); } else if (filename.endsWith('_LP.js')) { content.push(" strokeColor: 'blue',"); filetype = 'LP'; // content.push(` filetype: 'LP',`); } else if (filename.endsWith('_RP.js')) { content.push(" strokeColor: 'green',"); filetype = 'RP'; // content.push(` filetype: 'RP',`); } else { content.push(" strokeColor: '#FF00FF',"); } } else { content.push(" strokeColor: '#FF00FF',"); } content.push(' strokeOpacity: 0.8,'); content.push(" strokeStyle: 'solid',"); content.push(' });'); content.push(` polyline.filetype = '${filetype}';`); content.push(` polyline.lngMin = ${lngMin};`); content.push(` polyline.lngMax = ${lngMax};`); content.push(` polyline.latMin = ${latMin};`); content.push(` polyline.latMax = ${latMax};`); content.push(' nsOBCon.data.polylines.push(polyline);'); content.push('});'); } else { logger.info('Error : not supported type : ', JSON.stringify(feature)); } }); logger.info(`KakaoMap file : ${fileInfo.getFilenameFull()}`); fs.writeFileSync(fileInfo.getFilenameFull(), content.join('\n'), { encoding: 'UTF-8' }); } catch(err) { console.error(err); utils.obj.loggerError(err); } } } module.exports = SharpFile;