Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,52 @@
/>
</dd>
</template>
<template v-if="wfsFormats.length">
<dt class="font-bold fr-text--sm fr-mb-0">
<div class="flex gap-1 items-center">
{{ t('Formats exportés depuis le service WFS') }}
<span v-if="defaultWfsProjection"> ({{ t('projection {crs}', { crs: defaultWfsProjection }) }})</span>
<Tooltip>
<RiInformationLine
class="flex-none size-4"
:aria-label="t(`Le lien de téléchargement interroge directement le flux WFS distant. Le nombre de features téléchargées peut être limité.`)"
aria-hidden="true"
/>
<template #tooltip>
<p class="text-sm font-normal mb-0">
{{ t(`Le lien de téléchargement interroge directement le flux WFS distant.`) }}
</p>
<p class="text-sm font-normal mb-0">
{{ t(`Le nombre de features téléchargées peut être limité.`) }}
</p>
</template>
</Tooltip>
</div>
</dt>
<dd
v-for="wfsFormat in wfsFormats"
:key="wfsFormat.format"
class="text-sm pl-0 mb-4 text-gray-medium h-8 flex flex-wrap items-center"
>
<span>
<span class="text-datagouv fr-icon-download-line fr-icon--sm fr-mr-1v fr-mt-1v" />
<a
:href="wfsFormat.url"
class="fr-link"
rel="ugc nofollow noopener"
@click="trackEvent('Jeux de données', 'Télécharger un fichier', `Bouton : format ${wfsFormat.format}`)"
>
<span>{{ t('Format {format}', { format: wfsFormat.format }) }}</span>
</a>
</span>
<CopyButton
:label="t('Copier le lien')"
:copied-label="t('Lien copié !')"
:text="wfsFormat.url"
class="relative"
/>
</dd>
</template>
</dl>
</div>
<div v-if="tab.key === 'swagger'">
Expand All @@ -251,7 +297,7 @@

<script setup lang="ts">
import { computed, defineAsyncComponent } from 'vue'
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiSubtractLine } from '@remixicon/vue'
import { RiDownloadLine, RiFileCopyLine, RiFileWarningLine, RiInformationLine, RiSubtractLine } from '@remixicon/vue'
import { toast } from 'vue-sonner'
import BrandedButton from '../BrandedButton.vue'
import CopyButton from '../CopyButton.vue'
Expand All @@ -263,6 +309,7 @@ import TabList from '../Tabs/TabList.vue'
import Tab from '../Tabs/Tab.vue'
import TabPanels from '../Tabs/TabPanels.vue'
import TabPanel from '../Tabs/TabPanel.vue'
import Tooltip from '../Tooltip.vue'
import Preview from '../ResourceAccordion/Preview.vue'
import DataStructure from '../ResourceAccordion/DataStructure.vue'
import Metadata from '../ResourceAccordion/Metadata.vue'
Expand Down Expand Up @@ -316,6 +363,8 @@ const {
ogcService,
ogcWms,
generatedFormats,
wfsFormats,
defaultWfsProjection,
isResourceUrl,
tabsOptions,
} = useResourceCapabilities(() => props.resource, () => props.dataset)
Expand Down
16 changes: 15 additions & 1 deletion datagouv-components/src/composables/useResourceCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { useTranslation } from './useTranslation'
import { useHasTabularData } from './useHasTabularData'
import { detectOgcService } from '../functions/resources'
import { isOrganizationCertified } from '../functions/organizations'
import type { Resource } from '../types/resources'
import type { Resource, WfsMetadata } from '../types/resources'
import type { Dataset, DatasetV2 } from '../types/datasets'
import { getWfsExportFormats } from '../functions/resourceCapabilities'

const GENERATED_FORMATS = ['parquet', 'pmtiles', 'geojson']
const URL_FORMATS = ['url', 'doi', 'www:link', 'www:link-1.0-http--link', 'www:link-1.0-http--partners', 'www:link-1.0-http--related', 'www:link-1.0-http--samples']
Expand Down Expand Up @@ -67,6 +68,17 @@ export function useResourceCapabilities(
return formats
})

const wfsFormats = computed(() => {
return getWfsExportFormats(toValue(resource))
})

const defaultWfsProjection = computed<string | null>(() => {
const r = toValue(resource)
const wfsMetadata = r.extras['analysis:parsing:ogc_metadata'] as WfsMetadata | null
if (!wfsMetadata || wfsMetadata.format !== `wfs`) return null
return wfsMetadata?.detected_layer?.default_crs ?? null
})

const isResourceUrl = computed(() => URL_FORMATS.includes(toValue(resource).format))

const tabsOptions = computed(() => {
Expand Down Expand Up @@ -111,6 +123,8 @@ export function useResourceCapabilities(
ogcService,
ogcWms,
generatedFormats,
wfsFormats,
defaultWfsProjection,
isResourceUrl,
tabsOptions,
}
Expand Down
55 changes: 55 additions & 0 deletions datagouv-components/src/functions/resourceCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Resource, WfsMetadata, OgcLayerInfo } from '../types/resources'

const WFS_EXPORT_FORMATS = [
{
name: 'csv',
mimetype: 'csv',
},
{
name: 'json',
mimetype: 'application/json',
},
{
name: 'shp',
mimetype: 'SHAPE-ZIP',
},
{
name: 'gml',
mimetype: 'application/gml+xml',
},
{
name: 'kml',
mimetype: 'KML',
},
{
name: 'gpkg',
mimetype: 'application/geopackage+sqlite3',
},
]

function buildWfsDownloadUrl(baseUrl: string, wfsMetadata: WfsMetadata, format: { name: string, mimetype: string }, layer: OgcLayerInfo) {
const version = wfsMetadata.version
const query = new URLSearchParams({
SERVICE: 'WFS',
REQUEST: 'GetFeature',
VERSION: version,
...(Number(version.split('.')[0]) >= 2 ? { TYPENAMES: layer.name } : { TYPENAME: layer.name }),
OUTPUTFORMAT: format.mimetype,
...(layer.default_crs ? { SRSNAME: layer.default_crs } : {}),
})
return `${baseUrl.split('?')[0]}?${query.toString()}`
}

export function getWfsExportFormats(resource: Pick<Resource, 'extras' | 'url'>) {
const wfsMetadata = resource.extras['analysis:parsing:ogc_metadata'] as WfsMetadata | null
if (!wfsMetadata || wfsMetadata.format !== `wfs`) return []
const outputFormats = wfsMetadata.output_formats.map((format: string) => format.toLowerCase())
const layer = wfsMetadata.detected_layer
if (!layer) return []
const formats = WFS_EXPORT_FORMATS.filter(format => outputFormats.includes(format.mimetype.toLowerCase()))
.map(format => ({
url: buildWfsDownloadUrl(resource.url, wfsMetadata, format, layer),
format: format.name,
}))
return formats
}
10 changes: 10 additions & 0 deletions datagouv-components/src/types/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,13 @@ export interface ResourceGroup {
total: number
items: Resource[]
}

export type OgcLayerInfo = { name: string, default_crs: string | null }

export type WfsMetadata = {
format: string
layers: Array<OgcLayerInfo>
version: string
detected_layer: OgcLayerInfo | null
output_formats: Array<string>
}
114 changes: 114 additions & 0 deletions tests/resources/use-resource-capabilities.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { getWfsExportFormats } from '~/datagouv-components/src/functions/resourceCapabilities'
import { test, expect } from '../base'

test('WFS format download URLs generation', async () => {
const resource = {
url: 'https://example.com/wfs?service=WFS',
extras: {
'analysis:parsing:ogc_metadata': {
format: 'wfs',
version: '2.0.0',
output_formats: ['application/json', 'SHAPE-ZIP'],
detected_layer: { name: 'my_layer', default_crs: 'EPSG:4326' },
},
},
}

const wfsFormats = getWfsExportFormats(resource)

expect(wfsFormats).toEqual([
{
url: `https://example.com/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my_layer&OUTPUTFORMAT=${encodeURIComponent('application/json')}&SRSNAME=${encodeURIComponent('EPSG:4326')}`,
format: 'json',
},
{
url: `https://example.com/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=2.0.0&TYPENAMES=my_layer&OUTPUTFORMAT=SHAPE-ZIP&SRSNAME=${encodeURIComponent('EPSG:4326')}`,
format: 'shp',
},
])
})

test('WFS format download URLs generation with version 1.0.0', async () => {
const resource = {
url: 'https://example.com/wfs?service=WFS',
extras: {
'analysis:parsing:ogc_metadata': {
format: 'wfs',
version: '1.1.0',
output_formats: ['application/json', 'SHAPE-ZIP'],
detected_layer: { name: 'my_layer', default_crs: 'EPSG:4326' },
},
},
}

const wfsFormats = getWfsExportFormats(resource)

expect(wfsFormats).toEqual([
{
url: `https://example.com/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my_layer&OUTPUTFORMAT=${encodeURIComponent('application/json')}&SRSNAME=${encodeURIComponent('EPSG:4326')}`,
format: 'json',
},
{
url: `https://example.com/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my_layer&OUTPUTFORMAT=SHAPE-ZIP&SRSNAME=${encodeURIComponent('EPSG:4326')}`,
format: 'shp',
},
])
})

test('WFS format download URLs generation with no detected layer', async () => {
const resource = {
url: 'https://example.com/wfs?service=WFS',
extras: {
'analysis:parsing:ogc_metadata': {
format: 'wfs',
version: '1.1.0',
output_formats: ['application/json', 'SHAPE-ZIP'],
},
},
}

const wfsFormats = getWfsExportFormats(resource)

expect(wfsFormats).toEqual([])
})

test('WMS service don\'t expose WFS export formats', async () => {
const resource = {
url: 'https://example.com/wfs?service=WFS',
extras: {
'analysis:parsing:ogc_metadata': {
format: 'wms',
version: '1.1.0',
output_formats: ['application/json', 'SHAPE-ZIP'],
detected_layer: null,
},
},
}

const wfsFormats = getWfsExportFormats(resource)

expect(wfsFormats).toEqual([])
})

test('WFS format download URLs generation with null default_crs', async () => {
const resource = {
url: 'https://example.com/wfs?service=WFS',
extras: {
'analysis:parsing:ogc_metadata': {
format: 'wfs',
version: '1.1.0',
output_formats: ['application/json'],
detected_layer: { name: 'my_layer', default_crs: null },
},
},
}

const wfsFormats = getWfsExportFormats(resource)

expect(wfsFormats).toEqual([
{
url: `https://example.com/wfs?SERVICE=WFS&REQUEST=GetFeature&VERSION=1.1.0&TYPENAME=my_layer&OUTPUTFORMAT=${encodeURIComponent('application/json')}`,
format: 'json',
},
])
})
Loading