Spaces:
Sleeping
Sleeping
| /** | |
| * 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('<h3>SharpFile을 변환 한다.</h3><br/>'); | |
| let folderSharpFile = new FileInfo(path.join(appl.root, this._folder, service, site, 'SharpFile')); | |
| if (fs.existsSync(folderSharpFile.getFilenameFull()) == false) { //--- SharpFile에 해당하는 폴더가 없으면 종료 한다. | |
| return; | |
| } | |
| this._messages.push('<h4>SharpFile 목록</h4>'); | |
| this._messages.push('<ul>'); | |
| let fileInfos = this._findSharpFile(folderSharpFile); | |
| for (let idx = 0; idx < fileInfos.length; idx++) { | |
| this._messages.push(`<li>${fileInfos[idx].getFilename()}</li>`); | |
| } | |
| this._messages.push('</ul><br/>'); | |
| let geoJsons = []; | |
| this._messages.push('<h4>GeoJson 파일 목록</h4>'); | |
| this._messages.push('<ul>'); | |
| let fileInfoGeoJson = new FileInfo(path.join(appl.root, this._folder, service, site, 'GeoJson'), null, null, 'geojson'); | |
| fileInfoGeoJson.makeFolders(); | |
| fileInfoGeoJson.deleteAllFile(); | |
| for (let idx = 0; idx < fileInfos.length; idx++) { | |
| let fileInfo = fileInfos[idx]; | |
| fileInfoGeoJson.folder = fileInfo.folder; | |
| fileInfoGeoJson.filename = fileInfo.filename; | |
| if (fs.existsSync(fileInfoGeoJson.getFilenameFull())) { | |
| fs.unlinkSync(fileInfoGeoJson.getFilenameFull()); | |
| } | |
| fileInfoGeoJson.makeFolders(); | |
| let geoJson = await this._sharpFileToGeoJson(fileInfo, fileInfoGeoJson); | |
| geoJsons.push({ | |
| fileInfoSharpFile: fileInfo, | |
| geoJson: geoJson | |
| }); | |
| this._messages.push(`<li>${fileInfoGeoJson.getFilename()}</li>`); | |
| } | |
| this._messages.push('</ul><br/>'); | |
| this._messages.push('<h4>KakaoMap 파일 목록</h4>'); | |
| this._messages.push('<ul>'); | |
| let folderKakaoMap = new FileInfo(appl.root, path.join('themes', service, config.theme.name, 'devices', 'KakaoMap', site)); | |
| folderKakaoMap.makeFolders(); | |
| folderKakaoMap.deleteAllFile(); | |
| geoJsons.forEach(async function(geoJson) { | |
| let fileInfoKakaoMap = new FileInfo(folderKakaoMap.getFolderFull(), null, null, 'js'); | |
| if (geoJson.fileInfoSharpFile.folder == null) { | |
| fileInfoKakaoMap.filename = geoJson.fileInfoSharpFile.filename; | |
| } else { | |
| fileInfoKakaoMap.filename = `${geoJson.fileInfoSharpFile.folder.replace('/', '_').replace('\\', '_')}_${geoJson.fileInfoSharpFile.filename}`; | |
| } | |
| this._geoJsonToKakaoMap(geoJson.geoJson, fileInfoKakaoMap, service, site); | |
| this._messages.push(`<li>${fileInfoKakaoMap.getFilename()}</li>`); | |
| }.bind(this)); | |
| this._messages.push('</ul><br/>'); | |
| } 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; | |