Skip to content

Commit a04001d

Browse files
authored
KMZ importing updated to support image uploads (#298)
Improve KMZ file importing where observations with included images are not displayed properly
1 parent 33064d2 commit a04001d

4 files changed

Lines changed: 453 additions & 432 deletions

File tree

service/src/routes/imports.ts

Lines changed: 65 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { AnyPermission } from '../entities/authorization/entities.permissions'
55
import fs from 'fs-extra';
66
import Zip from 'adm-zip';
77
import { defaultHandler as upload } from '../upload';
8-
import { DOMParser, Document } from '@xmldom/xmldom';
9-
import toGeoJson from '../utilities/togeojson';
8+
import { KmlFeature, kml } from '../utilities/transformKML';
109

1110
interface SecurityConfig {
1211
authentication: {
@@ -17,7 +16,7 @@ interface LayerRequest extends Request {
1716
layer: {
1817
type: string;
1918
};
20-
kml?: Document;
19+
features?: any[];
2120
file?: Express.Multer.File;
2221
}
2322

@@ -29,76 +28,90 @@ interface ImportResponse {
2928
}>;
3029
}
3130

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

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-
}
43+
const kmlToGeoJSON = (kmlPathname: string, isKMZ: boolean): KmlFeature[] => {
44+
let kmlString: string;
45+
let images: Record<string, string> = {};
4046

41-
if (!layRequest.file) {
42-
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
43-
}
47+
if (isKMZ) {
48+
// TODO: Update how images are handled in KMZ files to prevent duplication. Move images to a separate directory and store their paths in the KML.
49+
const zip = new Zip(kmlPathname);
50+
const zipEntries = zip.getEntries();
51+
const kmlEntry = zipEntries.find(entry => entry.entryName.toLowerCase().endsWith('.kml'));
4452

45-
const fileExtension: string = layRequest.file.originalname.toLowerCase().split('.').pop() || '';
53+
if (!kmlEntry) {
54+
throw new Error('No KML file found inside KMZ.');
55+
}
4656

47-
if (fileExtension === 'kmz') {
57+
zipEntries.forEach(entry => {
58+
const entryName = entry.entryName;
4859
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'));
52-
53-
if (!kmlEntry) {
54-
return res.status(400).send('No KML file found inside.');
60+
if (!entry.isDirectory && /\.(png|jpg|jpeg|gif|bmp)$/i.test(entryName)) {
61+
const buffer = entry.getData();
62+
const base64 = buffer.toString('base64');
63+
const mimeType = getMimeType(entryName);
64+
images[entryName] = `data:${mimeType};base64,${base64}`;
5565
}
56-
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.');
66+
} catch (error) {
67+
console.error(`Error processing entry ${entryName}:`, error);
6168
}
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);
66-
});
67-
} else {
68-
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
69-
}
69+
});
70+
71+
kmlString = kmlEntry.getData().toString('utf8');
72+
} else {
73+
kmlString = fs.readFileSync(kmlPathname, 'utf8');
7074
}
7175

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");
76+
try {
77+
return kml(kmlString, images);
78+
} catch (error) {
79+
throw new Error('Failed to transform KML: ' + error);
80+
}
81+
}
7682

77-
if (parseError.length > 0) {
78-
console.error("KML Parsing Error:", parseError[0].textContent);
79-
} else {
80-
console.log("Parsed KML successfully");
81-
}
83+
const validate = async (req: Request, res: Response, next: NextFunction): Promise<void | Response> => {
84+
const layRequest = req as LayerRequest;
85+
if (layRequest.layer.type !== 'Feature') {
86+
return res.status(400).send('Cannot import data, layer type is not "Static".');
87+
}
8288

83-
if (!kml || kml.documentElement?.nodeName !== 'kml') {
84-
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
85-
}
89+
const fileExtension: string = layRequest.file?.originalname?.toLowerCase().split('.').pop() || '';
8690

87-
req.kml = kml;
88-
return next();
91+
if (!['kml', 'kmz'].includes(fileExtension)) {
92+
return res.status(400).send('Invalid file, please upload a KML or KMZ file.');
8993
}
9094

95+
try {
96+
layRequest.features = kmlToGeoJSON(layRequest.file!.path, fileExtension === 'kmz');
97+
} catch (err) {
98+
return res.status(400).send('Unable to extract contents from KMZ file.' + err);
99+
}
100+
return next();
101+
}
102+
103+
function importRoutes(app: Express, security: SecurityConfig): void {
104+
const passport = security.authentication.passport;
105+
91106
app.post(
92107
'/api/layers/:layerId/kml',
93108
passport.authenticate('bearer'),
94109
access.authorize('CREATE_LAYER' as AnyPermission),
95110
upload.single('file'),
96111
validate,
97-
function (req: Request, res: Response, next: NextFunction): void {
112+
(req: Request, res: Response, next: NextFunction) => {
98113
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)
114+
new api.Feature(layerRequest.layer).createFeatures(layerRequest.features)
102115
.then((newFeatures: any[]) => {
103116
const response: ImportResponse = {
104117
files: [{

0 commit comments

Comments
 (0)