Skip to content

Commit 91ecd04

Browse files
committed
initial commit
1 parent a6318c6 commit 91ecd04

File tree

6 files changed

+531
-79
lines changed

6 files changed

+531
-79
lines changed

service/npm-shrinkwrap.json

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"wms-capabilities": "0.6.0",
7979
"xmlbuilder2": "3.1.1",
8080
"xpath": "0.0.34",
81+
"xslt-processor": "^3.3.1",
8182
"yaml": "1.10.2"
8283
},
8384
"devDependencies": {

service/src/routes/imports.ts

Lines changed: 85 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import fs from 'fs-extra';
66
import Zip from 'adm-zip';
77
import { defaultHandler as upload } from '../upload';
88
import { DOMParser, Document } from '@xmldom/xmldom';
9-
import toGeoJson from '../utilities/togeojson';
9+
import kml from '../utilities/transformKML'
10+
import { Xslt, XmlParser } from 'xslt-processor';
1011

1112
interface SecurityConfig {
1213
authentication: {
@@ -17,7 +18,7 @@ interface LayerRequest extends Request {
1718
layer: {
1819
type: string;
1920
};
20-
kml?: Document;
21+
features?: any[];
2122
file?: Express.Multer.File;
2223
}
2324

@@ -29,76 +30,116 @@ interface ImportResponse {
2930
}>;
3031
}
3132

32-
function importRoutes(app: Express, security: SecurityConfig): void {
33-
const passport = security.authentication.passport;
33+
const getMimeType = (filename: string): string => {
34+
const ext = filename.toLowerCase().split('.').pop() || '';
35+
const mimeTypes: { [key: string]: string } = {
36+
'png': 'image/png',
37+
'jpg': 'image/jpeg',
38+
'jpeg': 'image/jpeg',
39+
'gif': 'image/gif',
40+
'bmp': 'image/bmp'
41+
};
42+
return mimeTypes[ext] || 'application/octet-stream';
43+
}
3444

35-
function validate(req: Request, res: Response, next: NextFunction): void | Response {
36-
const layRequest = req as LayerRequest;
37-
if (layRequest.layer.type !== 'Feature') {
38-
return res.status(400).send('Cannot import data, layer type is not "Static".');
39-
}
45+
const validate = async (req: Request, res: Response, next: NextFunction): Promise<void | Response> => {
46+
const layRequest = req as LayerRequest;
47+
if (layRequest.layer.type !== 'Feature') {
48+
return res.status(400).send('Cannot import data, layer type is not "Static".');
49+
}
4050

41-
if (!layRequest.file) {
42-
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
43-
}
51+
if (!layRequest.file) {
52+
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
53+
}
54+
55+
const fileExtension: string = layRequest.file.originalname.toLowerCase().split('.').pop() || '';
4456

45-
const fileExtension: string = layRequest.file.originalname.toLowerCase().split('.').pop() || '';
57+
if (!['kml', 'kmz'].includes(fileExtension)) {
58+
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
59+
}
4660

47-
if (fileExtension === 'kmz') {
48-
try {
49-
const zip = new Zip(layRequest.file.path);
50-
const zipEntries = zip.getEntries();
51-
const kmlEntry = zipEntries.find(entry => entry.entryName.toLowerCase().endsWith('.kml'));
61+
const parser = new DOMParser();
62+
let geoJson: any;
5263

53-
if (!kmlEntry) {
54-
return res.status(400).send('No KML file found inside.');
55-
}
64+
if (fileExtension === 'kmz') {
65+
try {
66+
const zip = new Zip(layRequest.file.path);
67+
const zipEntries = zip.getEntries();
68+
const kmlEntry = zipEntries.find(entry => entry.entryName.toLowerCase().endsWith('.kml'));
69+
// const xslEntry = zipEntries.find(entry => entry.entryName.toLowerCase().endsWith('.xsl') || entry.entryName.toLowerCase().endsWith('.xslt'));
5670

57-
const kmlData: string = kmlEntry.getData().toString('utf8');
58-
processKmlData(kmlData, layRequest, res, next);
59-
} catch (err) {
60-
return res.status(400).send('Unable to extract contents from KMZ file.');
71+
if (!kmlEntry) {
72+
return res.status(400).send('No KML file found inside.');
6173
}
62-
} else if (fileExtension === 'kml') {
63-
fs.readFile(layRequest.file.path, 'utf8', function (err: Error | null, data: string) {
64-
if (err) return next(err);
65-
processKmlData(data, layRequest, res, next);
74+
75+
const images: { [key: string]: string } = {};
76+
zipEntries.forEach(entry => {
77+
const entryName = entry.entryName;
78+
if (!entry.isDirectory && /\.(png|jpg|jpeg|gif|bmp)$/i.test(entryName)) {
79+
const buffer = entry.getData();
80+
const base64 = buffer.toString('base64');
81+
const mimeType = getMimeType(entryName);
82+
images[entryName] = `data:${mimeType};base64,${base64}`;
83+
}
6684
});
67-
} else {
68-
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
69-
}
70-
}
7185

72-
function processKmlData(data: string, req: LayerRequest, res: Response, next: NextFunction): void | Response {
73-
const parser = new DOMParser();
74-
const kml: Document = parser.parseFromString(data, "application/xml");
75-
const parseError = kml.getElementsByTagName("parsererror");
86+
const kmlString = kmlEntry.getData().toString('utf8');
87+
88+
// if (xslEntry) {
89+
// const xslString = xslEntry.getData().toString('utf8');
90+
91+
// const xslt = new Xslt({ cData: true, escape: false });
92+
// const xmlParser = new XmlParser();
93+
94+
// const outXmlString = await xslt.xsltProcess(
95+
// xmlParser.xmlParse(kmlString),
96+
// xmlParser.xmlParse(xslString)
97+
// );
98+
// console.log('outXmlString', outXmlString);
99+
// const transformedDocument = parser.parseFromString(outXmlString, 'text/xml');
100+
// geoJson = toGeoJson.kml(transformedDocument);
101+
// }
102+
103+
const kmlDocument = parser.parseFromString(kmlString, 'text/xml');
104+
geoJson = kml(kmlDocument as any, images);
105+
106+
} catch (err) {
107+
return res.status(400).send('Unable to extract contents from KMZ file.' + err);
108+
}
109+
} else {
110+
const fileData = fs.readFileSync(layRequest.file.path, 'utf8');
111+
const kmlDocument: Document = parser.parseFromString(fileData, 'application/xml');
112+
const parseError = kmlDocument.getElementsByTagName("parsererror");
76113

77114
if (parseError.length > 0) {
78115
console.error("KML Parsing Error:", parseError[0].textContent);
79116
} else {
80117
console.log("Parsed KML successfully");
81118
}
82119

83-
if (!kml || kml.documentElement?.nodeName !== 'kml') {
120+
if (!kmlDocument || kmlDocument.documentElement?.nodeName !== 'kml') {
84121
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
85122
}
86123

87-
req.kml = kml;
88-
return next();
124+
geoJson = kml(kmlDocument as any);
89125
}
90126

127+
layRequest.features = geoJson;
128+
return next();
129+
}
130+
131+
function importRoutes(app: Express, security: SecurityConfig): void {
132+
const passport = security.authentication.passport;
133+
91134
app.post(
92135
'/api/layers/:layerId/kml',
93136
passport.authenticate('bearer'),
94137
access.authorize('CREATE_LAYER' as AnyPermission),
95138
upload.single('file'),
96139
validate,
97-
function (req: Request, res: Response, next: NextFunction): void {
140+
(req: Request, res: Response, next: NextFunction) => {
98141
const layerRequest = req as LayerRequest;
99-
console.log('Importing KML file:', layerRequest.file?.originalname);
100-
const features = toGeoJson.kml(layerRequest.kml!);
101-
new api.Feature(layerRequest.layer).createFeatures(features)
142+
new api.Feature(layerRequest.layer).createFeatures(layerRequest.features)
102143
.then((newFeatures: any[]) => {
103144
const response: ImportResponse = {
104145
files: [{

service/src/utilities/togeojson.js

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ const xpath = require('xpath')
33

44
exports.kml = kml;
55

6-
function kml(document) {
6+
function kml(document, images = {}) {
77
log.info('Generate KML');
88

9-
const styleIndex = getStyles(document);
9+
const styleIndex = getStyles(document, images);
1010

1111
// Pull all placemarks regards of depth level
1212
let placemarks = xpath.select("//*[local-name()='Placemark']", document);
@@ -16,13 +16,13 @@ function kml(document) {
1616

1717
let features = [];
1818
placemarks.forEach(placemark => {
19-
features = features.concat(getPlacemark(placemark, styleIndex));
19+
features = features.concat(getPlacemark(placemark, styleIndex, images));
2020
});
2121

2222
return features;
2323
}
2424

25-
function getProperties(node, styleIndex) {
25+
function getProperties(node, styleIndex, images = {}) {
2626
let properties = {};
2727

2828
let name = nodeVal(get1(node, 'name'));
@@ -53,7 +53,7 @@ function getProperties(node, styleIndex) {
5353
const styleElement = get1(node, 'Style');
5454
if (styleElement) {
5555
properties.style = {
56-
iconStyle: getIconStyle(styleElement),
56+
iconStyle: getIconStyle(styleElement, images),
5757
lineStyle: getLineStyle(styleElement),
5858
labelStyle: getLabelStyle(styleElement),
5959
polyStyle: getPolygonStyle(styleElement)
@@ -64,11 +64,11 @@ function getProperties(node, styleIndex) {
6464
return properties;
6565
}
6666

67-
function getPlacemark(node, styleIndex) {
67+
function getPlacemark(node, styleIndex, images = {}) {
6868
const geometries = getGeometry(node);
6969
if (!geometries.length) return [];
7070

71-
const properties = getProperties(node, styleIndex);
71+
const properties = getProperties(node, styleIndex, images);
7272

7373
return geometries.map(geometry => {
7474
return {
@@ -80,7 +80,7 @@ function getPlacemark(node, styleIndex) {
8080
}
8181

8282
function getGeometry(node) {
83-
if (get1(node, 'MultiGeometry')){
83+
if (get1(node, 'MultiGeometry')) {
8484
return getGeometry(get1(node, 'MultiGeometry'));
8585
}
8686

@@ -95,19 +95,19 @@ function getGeometry(node) {
9595
for (let i = 0; i < geometryNodes.length; i++) {
9696
let geometryNode = geometryNodes[i];
9797

98-
switch(geometryType) {
99-
case 'Point':
100-
geometries.push(getPoint(geometryNode));
101-
break;
102-
case 'LineString':
103-
geometries.push(getLineString(geometryNode));
104-
break;
105-
case 'Track':
106-
geometries.push(getTrack(geometryNode));
107-
break;
108-
case 'Polygon':
109-
geometries.push(getPolygon(geometryNode));
110-
break;
98+
switch (geometryType) {
99+
case 'Point':
100+
geometries.push(getPoint(geometryNode));
101+
break;
102+
case 'LineString':
103+
geometries.push(getLineString(geometryNode));
104+
break;
105+
case 'Track':
106+
geometries.push(getTrack(geometryNode));
107+
break;
108+
case 'Polygon':
109+
geometries.push(getPolygon(geometryNode));
110+
break;
111111
}
112112
}
113113
}
@@ -178,15 +178,15 @@ function gxCoord(v) {
178178
return numarray(v.split(' '));
179179
}
180180

181-
function getStyles(node) {
181+
function getStyles(node, images = {}) {
182182
let styleIndex = {};
183183

184184
const styles = get(node, 'Style');
185185
for (let i = 0; i < styles.length; i++) {
186186
const kmlStyle = styles[i];
187187
const styleId = '#' + attr(kmlStyle, 'id');
188188
styleIndex[styleId] = {
189-
iconStyle: getIconStyle(kmlStyle),
189+
iconStyle: getIconStyle(kmlStyle, images),
190190
lineStyle: getLineStyle(kmlStyle),
191191
labelStyle: getLabelStyle(kmlStyle),
192192
polyStyle: getPolygonStyle(kmlStyle)
@@ -216,7 +216,7 @@ function getStyles(node) {
216216
return styleIndex;
217217
}
218218

219-
function getIconStyle(node) {
219+
function getIconStyle(node, images = {}) {
220220
const iconStyle = get(node, 'IconStyle');
221221
if (iconStyle[0]) {
222222
let style = {};
@@ -228,11 +228,17 @@ function getIconStyle(node) {
228228

229229
const icon = get(iconStyle[0], 'Icon');
230230
if (icon && icon[0]) {
231-
style.icon = {};
232-
233231
const href = get(icon[0], 'href');
234232
if (href[0]) {
235-
style.icon.href = nodeVal(href[0]);
233+
const hrefValue = nodeVal(href[0]);
234+
const isWebUrl = /^(http|https):/.test(hrefValue);
235+
236+
if (isWebUrl || images[hrefValue]) {
237+
style.icon = {
238+
href: images[hrefValue] || hrefValue
239+
};
240+
}
241+
236242
}
237243
}
238244

@@ -332,7 +338,7 @@ function coordinateArray(x) {
332338
o[i] = parseFloat(x[i]);
333339
}
334340

335-
return o.splice(0,2);
341+
return o.splice(0, 2);
336342
}
337343

338344
// get the content of a text node, if any
@@ -366,10 +372,10 @@ function coord(v) {
366372
}
367373

368374
function parseColor(color) {
369-
const r = color.slice(6,8);
370-
const g = color.slice(4,6);
371-
const b = color.slice(2,4);
372-
const a = color.slice(0,2);
375+
const r = color.slice(6, 8);
376+
const g = color.slice(4, 6);
377+
const b = color.slice(2, 4);
378+
const a = color.slice(0, 2);
373379

374380
return {
375381
rgb: '#' + r + g + b,

0 commit comments

Comments
 (0)