Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PB-64, PB-970: Add full support of KMZ file and fixed KML inlined icons #minor #1063

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"geographiclib-geodesic": "^2.1.1",
"hammerjs": "^2.0.8",
"jquery": "^3.7.1",
"jszip": "^3.10.1",
"liang-barsky": "^1.0.5",
"lodash": "^4.17.21",
"maplibre-gl": "^4.6.0",
Expand Down
18 changes: 11 additions & 7 deletions src/api/files.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export function loadKmlMetadata(kmlLayer) {
* Loads the XML data from the file of a given KML layer, using the KML file URL of the layer.
*
* @param {KMLLayer} kmlLayer
* @returns {Promise<String>}
* @returns {Promise<ArrayBuffer>}
*/
export function loadKmlData(kmlLayer) {
return new Promise((resolve, reject) => {
Expand All @@ -313,7 +313,9 @@ export function loadKmlData(kmlLayer) {
new Error(`No file URL defined in this KML layer, cannot load data ${kmlLayer.id}`)
)
}
getFileFromUrl(kmlLayer.kmlFileUrl)
// The file might be a KMZ file, which is a zip archive. Reading zip archive as text
// is asking for trouble therefore we use ArrayBuffer
getFileFromUrl(kmlLayer.kmlFileUrl, { responseType: 'arraybuffer' })
.then((response) => {
if (response.status === 200 && response.data) {
resolve(response.data)
Expand All @@ -339,16 +341,18 @@ export function loadKmlData(kmlLayer) {
*
* @param {string} url URL to fetch
* @param {Number} [options.timeout] How long should the call wait before timing out
* @param {string} [options.responseType] Type of data that the server will respond with. Options
* are 'arraybuffer', 'document', 'json', 'text', 'stream'. Default is `json`
* @returns {Promise<AxiosResponse<any, any>>}
*/
export async function getFileFromUrl(url, options = {}) {
const { timeout = null } = options
const { timeout = null, responseType = null } = options
if (/^https?:\/\/localhost/.test(url) || isInternalUrl(url)) {
// don't go through proxy if it is on localhost or the internal server
return axios.get(url, { timeout })
return axios.get(url, { timeout, responseType })
} else if (url.startsWith('http://')) {
// HTTP request goes through the proxy
return axios.get(proxifyUrl(url), { timeout })
return axios.get(proxifyUrl(url), { timeout, responseType })
}

// For other urls we need to check if they support CORS
Expand All @@ -370,8 +374,8 @@ export async function getFileFromUrl(url, options = {}) {

if (supportCORS) {
// Server support CORS
return axios.get(url, { timeout })
return axios.get(url, { timeout, responseType })
}
// server don't support CORS use proxy
return axios.get(proxifyUrl(url), { timeout })
return axios.get(proxifyUrl(url), { timeout, responseType })
}
5 changes: 5 additions & 0 deletions src/api/layers/KMLLayer.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export default class KMLLayer extends AbstractLayer {
* Default is `null`
* @param {KmlMetadata | null} [kmlLayerData.kmlMetadata=null] Metadata of the KML drawing. This
* object contains all the metadata returned by the backend. Default is `null`
* @param {Map<string, ArrayBuffer>} [kmlLayerData.linkFiles=Map()] Map of KML link files. Those
* files are usually sent with the kml inside a KMZ archive and can be referenced inside the
* KML (e.g. icon, image, ...). Default is `Map()`
* @throws InvalidLayerDataError if no `gpxLayerData` is given or if it is invalid
*/
constructor(kmlLayerData) {
Expand All @@ -42,6 +45,7 @@ export default class KMLLayer extends AbstractLayer {
adminId = null,
kmlData = null,
kmlMetadata = null,
linkFiles = new Map(),
} = kmlLayerData
if (kmlFileUrl === null) {
throw new InvalidLayerDataError('Missing KML file URL', kmlLayerData)
Expand Down Expand Up @@ -79,6 +83,7 @@ export default class KMLLayer extends AbstractLayer {
this.isLoading = true
}
this.kmlData = kmlData
this.linkFiles = linkFiles
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,9 @@ export default function useMapInteractions(map) {
const reader = new FileReader()
reader.onload = (event) => resolve(event.target.result)
reader.onerror = (error) => reject(error)
reader.readAsText(file)
// The file might be a KMZ file, which is a zip archive. Reading zip archive as text
// is asking for trouble therefore we use ArrayBuffer
reader.readAsArrayBuffer(file)
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EmptyGPXError } from '@/utils/gpxUtils'
import { EmptyKMLError } from '@/utils/kmlUtils'
import log from '@/utils/logging'

const acceptedFileTypes = ['.kml', '.KML', '.gpx', '.GPX']
const acceptedFileTypes = ['.kml', '.KML', '.kmz', '.KMZ', '.gpx', '.GPX']

const store = useStore()

Expand Down Expand Up @@ -41,8 +41,10 @@ async function loadFile() {

if (isFormValid.value && selectedFile.value) {
try {
const content = await selectedFile.value.text()
handleFileContent(store, content, selectedFile.value.name)
// The file might be a KMZ which is a zip archive. Handling zip archive as text is
// asking for trouble, therefore we need first to get it as binary
const content = await selectedFile.value.arrayBuffer()
await handleFileContent(store, content, selectedFile.value.name)
importSuccessMessage.value = 'file_imported_success'
} catch (error) {
if (error instanceof OutOfBoundsError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,14 @@ async function loadFile() {
loading.value = true

try {
const response = await getFileFromUrl(fileUrl.value, { timeout: REQUEST_TIMEOUT })
const response = await getFileFromUrl(fileUrl.value, {
timeout: REQUEST_TIMEOUT,
responseType: 'arraybuffer',
})
if (response.status !== 200) {
throw new Error(`Failed to fetch ${fileUrl.value}; status_code=${response.status}`)
}
handleFileContent(store, response.data, fileUrl.value)
await handleFileContent(store, response.data, fileUrl.value)
importSuccessMessage.value = 'file_imported_success'
setTimeout(() => (buttonState.value = 'default'), 3000)
} catch (error) {
Expand Down
37 changes: 26 additions & 11 deletions src/modules/menu/components/advancedTools/ImportFile/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import KMLLayer from '@/api/layers/KMLLayer.class'
import { OutOfBoundsError } from '@/utils/coordinates/coordinateUtils'
import { getExtentForProjection } from '@/utils/extentUtils.js'
import { EmptyGPXError, getGpxExtent } from '@/utils/gpxUtils.js'
import { EmptyKMLError, getKmlExtent } from '@/utils/kmlUtils'
import { EmptyKMLError, getKmlExtent, unzipKmz } from '@/utils/kmlUtils'
import log from '@/utils/logging'
import { isZipContent } from '@/utils/utils'

const dispatcher = { dispatcher: 'ImportFile/utils' }

Expand Down Expand Up @@ -35,21 +37,33 @@ export function isGpx(fileContent) {
* Handle file content
*
* @param {OBject} store Vuex store
* @param {string} content Content of the file
* @param {ArrayBuffer} content Content of the file
* @param {string} source Source of the file (either URL or file path)
* @returns {ExternalLayer} External layer object
*/
export function handleFileContent(store, content, source) {
export async function handleFileContent(store, content, source) {
let layer = null
if (isKml(content)) {
let textContent
let linkFiles
if (isZipContent(content)) {
log.debug(`File content is a zipfile, assume it is a KMZ archive`)
const kmz = await unzipKmz(content, source)
textContent = kmz.kml
linkFiles = kmz.files
} else {
// If it is not a zip file then we assume is a text file and decode it for further handling
textContent = new TextDecoder('utf-8').decode(content)
}
if (isKml(textContent)) {
layer = new KMLLayer({
kmlFileUrl: source,
visible: true,
opacity: 1.0,
adminId: null,
kmlData: content,
kmlData: textContent,
linkFiles,
})
const extent = getKmlExtent(content)
const extent = getKmlExtent(textContent)
if (!extent) {
throw new EmptyKMLError()
}
Expand All @@ -64,17 +78,17 @@ export function handleFileContent(store, content, source) {
} else {
store.dispatch('addLayer', { layer, ...dispatcher })
}
} else if (isGpx(content)) {
} else if (isGpx(textContent)) {
const gpxParser = new GPX()
const metadata = gpxParser.readMetadata(content)
const metadata = gpxParser.readMetadata(textContent)
layer = new GPXLayer({
gpxFileUrl: source,
visible: true,
opacity: 1.0,
gpxData: content,
gpxData: textContent,
gpxMetadata: metadata,
})
const extent = getGpxExtent(content)
const extent = getGpxExtent(textContent)
if (!extent) {
throw new EmptyGPXError()
}
Expand All @@ -85,7 +99,8 @@ export function handleFileContent(store, content, source) {
store.dispatch('zoomToExtent', { extent: projectedExtent, ...dispatcher })
store.dispatch('addLayer', { layer, ...dispatcher })
} else {
throw new Error(`Unsupported file ${source} content`)
throw new Error(`Unsupported file ${source} textContent`)
}

return layer
}
15 changes: 12 additions & 3 deletions src/store/modules/layers.store.js
Original file line number Diff line number Diff line change
Expand Up @@ -652,11 +652,17 @@ const actions = {
* NOTE: all matching layer id will be set.
*
* @param {string} layerId Layer ID of KML to update
* @param {string} data Data KML data to set
* @param {object | null} metadata KML metadata to set (only for geoadmin KMLs)
* @param {string} [data] Data KML data to set
* @param {object} [metadata] KML metadata to set (only for geoadmin KMLs). Default is `null`
* @param {Map<string, ArrayBuffer>} [linkFiles] Map of KML link files. Those files are usually
* sent with the kml inside a KMZ archive and can be referenced inside the KML (e.g. icon,
* image, ...).
* @param {string} dispatcher Action dispatcher name
*/
setKmlGpxLayerData({ commit, getters, rootState }, { layerId, data, metadata, dispatcher }) {
setKmlGpxLayerData(
{ commit, getters, rootState },
{ layerId, data, metadata, linkFiles, dispatcher }
) {
const layers = getters.getActiveLayersById(layerId)
if (!layers) {
throw new Error(
Expand Down Expand Up @@ -693,6 +699,9 @@ const actions = {
clone.name = metadata.name ?? 'GPX'
}
}
if (linkFiles && clone.type === LayerTypes.KML) {
clone.linkFiles = linkFiles
}
return clone
})
commit('updateLayers', { layers: updatedLayers, dispatcher })
Expand Down
Loading
Loading