obcon-scada / app /include /SharpFile.js
chanmin0723's picture
Initial obcon SCADA deploy
e4bf523
Raw
History Blame Contribute Delete
22.3 kB
'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('<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;