Skip to content

Commit

Permalink
Groupes espèces (#19)
Browse files Browse the repository at this point in the history
* Ajout du fichier des groupes d'espèces

* Premier jet lecture du fichier .ods groupes d'espèces

* Création du fichier de groupes d'espèces

* Début UI groupes d'espèces

* Nouvelle création de la liste des espèces protégées. Avec des streams + basée sur le CD_REF

* Utilisation de la nouvelle liste d'espèce et du type EspèceProtégées dans la saisie d'espèces

* Suppression du type Espèce qui a été remplacé par EspèceProtégées

* Correction nom type

* Pré-remplissage par groupe

* Amélioration des perfs de rendu quand il y a plusieurs composants AutocompleteEspèces
  • Loading branch information
DavidBruant authored Jun 21, 2024
1 parent 07ba590 commit 20fbe14
Show file tree
Hide file tree
Showing 17 changed files with 11,346 additions and 17,476 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,7 @@ Puis lancer `node outils/liste-espèces.js` pour régénérer une liste d'espèc
### Synchroniser dossiers récemment modifiés de Démarches Simplifiées

`node --env-file=.env outils/sync-démarches-simplifiées.js`

### Fabriquer le JSON de la liste des groupes d'espèces

`node outils/groupes-espèces.js`
Binary file added data/ListeGroupesEspeces.ods
Binary file not shown.
1 change: 1 addition & 0 deletions data/groupes_especes.json

Large diffs are not rendered by default.

10,544 changes: 10,544 additions & 0 deletions data/liste-espèces-protégées.csv

Large diffs are not rendered by default.

17,232 changes: 0 additions & 17,232 deletions data/liste_especes.csv

This file was deleted.

9 changes: 5 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
<meta name="description" content=" ">
<meta name="viewport" content="width=device-width, initial-scale=1">

<link id="especes-data" rel="preload" as="fetch" href="/data/liste_especes.csv" crossorigin="anonymous">
<link id="activites-data" rel="preload" as="fetch" href="/data/activités.csv" crossorigin="anonymous">
<link id="methodes-data" rel="preload" as="fetch" href="/data/méthodes.csv" crossorigin="anonymous">
<link id="transports-data" rel="preload" as="fetch" href="/data/transports.csv" crossorigin="anonymous">
<link id="especes-data" href="/data/liste-espèces-protégées.csv" crossorigin="anonymous">
<link id="activites-data" href="/data/activités.csv" crossorigin="anonymous">
<link id="methodes-data" href="/data/méthodes.csv" crossorigin="anonymous">
<link id="transports-data" href="/data/transports.csv" crossorigin="anonymous">
<link id="groupes-especes-data" href="data/groupes_especes.json" crossorigin="anonymous">

<link crossorigin="anonymous" rel="stylesheet" href="/style/dsfr/dsfr.min.css">

Expand Down
93 changes: 93 additions & 0 deletions outils/groupes-espèces.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
//@ts-check

import {readFile, writeFile} from 'node:fs/promises'
import {sheetRawContentToObjects, getODSTableRawContent} from 'ods-xlsx'
import { dsvFormat } from 'd3-dsv';

import {espèceProtégéeStringToEspèceProtégée} from '../scripts/commun/outils-espèces.js'

import '../scripts/types.js'

/**
* Cet outil prend le fichier data/ListeGroupesEspeces.ods et en fait un fichier json plus léger
* qui contient les groupes d'espèces pour usage dans
*/

const lignesGroupeEspècesP = readFile('data/ListeGroupesEspeces.ods')
.then(({buffer: groupeEspècesODSArrayBuffer}) => getODSTableRawContent(groupeEspècesODSArrayBuffer))
.then(tableRaw => {
const sheetRaw = tableRaw.get('ListeGroupesEspeces')

// Trouver la première ligne avec plus de 5 éléments (qui est la "vraie" première ligne, avec les noms de colonne)
const firstRowIndex = sheetRaw.findIndex(row => row.length >= 5)
const actualSheetRaw = sheetRaw.slice(firstRowIndex)

return sheetRawContentToObjects(actualSheetRaw)
})

/** @type {Promise<Map<string, EspèceProtégée>>} */
const espèceParNomScientifiqueP = readFile('data/liste-espèces-protégées.csv', 'utf8')
.then(str => {
/** @type {EspèceProtégéeStrings[]} */
const espèceStrs = dsvFormat(';').parse(str)

const espèces = espèceStrs.map(espèceProtégéeStringToEspèceProtégée)

/** @type {Map<string, EspèceProtégée>} */
const ret = new Map()

for(const espèce of espèces){
for(const nom of espèce.nomsScientifiques){
ret.set(nom, espèce)
}
}

return ret
})

const outputPath = 'data/groupes_especes.json'

Promise.all([lignesGroupeEspècesP, espèceParNomScientifiqueP])
.then(([lignesGroupeEspèces, espèceParNomScientifique]) => {
/** @type {GroupesEspèces} */
const groupesEspèces = Object.create(null)

const espècesNonReconnues = []

for(const ligne of lignesGroupeEspèces){
const {'Nom scientifique': nomScientifique, 'Nom du groupe': nomGroupe} = ligne

let espèceDuGroupe = espèceParNomScientifique.get(nomScientifique)

/** @type {EspèceSimplifiée | string} */
let jsonableEspèce;

if(espèceDuGroupe){
const {nomsScientifiques, CD_REF} = espèceDuGroupe
jsonableEspèce = {nom: [...nomsScientifiques][0], CD_REF}
}
else{
espècesNonReconnues.push(ligne)
jsonableEspèce = nomScientifique
}

const groupe = groupesEspèces[nomGroupe] || []
groupe.push(jsonableEspèce)
groupesEspèces[nomGroupe] = groupe
}

if(espècesNonReconnues.length >= 1){
const set = new Set(espècesNonReconnues.map(e => e['Nom scientifique']))
console.log([...set].join('\n'))
console.log(
set.size,
'espèces non reconnues sur',
(new Set(lignesGroupeEspèces.map(e => e['Nom scientifique'])).size)
)
}

const jsonOutput = JSON.stringify(groupesEspèces);

return writeFile(outputPath, jsonOutput, 'utf8');
})
.then(() => console.log(`Fichier ${outputPath} créé avec succès`))
224 changes: 196 additions & 28 deletions outils/liste-espèces.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,215 @@
//@ts-check

import {readFileSync, writeFileSync} from 'node:fs'
import {csvParse, tsvParse, dsvFormat} from 'd3-dsv'
import {readFile} from 'node:fs/promises'
import {createReadStream, createWriteStream} from 'node:fs'
import { parse } from 'csv-parse';
import { stringify } from 'csv-stringify';
import {dsvFormat} from 'd3-dsv'

import {TAXREF_ROWClassification, nomsVernaculaires} from '../scripts/commun/outils-espèces.js'

// BDC_STATUTS
import '../scripts/types.js'

let bdc_statuts_raw = csvParse(readFileSync('data/sources_especes/BDC_STATUTS_17.csv', 'utf-8'))

console.log('bdc_statuts_raw.length', bdc_statuts_raw.length)
process.title = `Génération liste espèces`

/**
*
* BDC_STATUTS
*
*/
const keptCdTypeStatus = new Set(['POM', 'PD', 'PN', 'PR'])

const bdc_statuts = bdc_statuts_raw
.filter(({CD_TYPE_STATUT}) => keptCdTypeStatus.has(CD_TYPE_STATUT))
.map(
({CD_NOM, CD_TYPE_STATUT, LABEL_STATUT}) => ({CD_NOM, CD_TYPE_STATUT, LABEL_STATUT})
)
const bdcParser = parse({
delimiter: ',',
columns: true,
trim: true
});

/** @type {Promise<BDC_STATUT_ROW[]>} */
const bdc_statutsP = new Promise((resolve, reject) => {

/** @type {BDC_STATUT_ROW[]} */
const espècesProtégéesBDC_STATUTS = []
// Use the readable stream api to consume records
bdcParser.on('readable', function(){
/** @type {BDC_STATUT_ROW} */
let record;
while ((record = bdcParser.read()) !== null) {
// > Le CD_NOM du taxon retenu dans la base de connaissance est celui du nom cité dans
// le document source faisant référence au statut. Le CD_REF est l’identifiant attaché
// au nom valide correspondant àce CD_NOM dans la dernière version diffusée de TAXREF.
// https://inpn.mnhn.fr/docs-web/docs/download/232196 (page 6)
const {CD_NOM, CD_REF, CD_TYPE_STATUT, LABEL_STATUT} = record
if(keptCdTypeStatus.has(CD_TYPE_STATUT)){
espècesProtégéesBDC_STATUTS.push({CD_NOM, CD_REF, CD_TYPE_STATUT, LABEL_STATUT});
}
}
});

bdcParser.on('error', reject);
bdcParser.on('end', () => resolve(espècesProtégéesBDC_STATUTS));
})

// @ts-ignore
bdc_statuts_raw = undefined // for GC

console.log('bdc_statuts.length', bdc_statuts.length)
createReadStream('data/sources_especes/BDC_STATUTS_17.csv').pipe(bdcParser)

bdc_statutsP.then(bdc_statuts => {
console.log('bdc_statuts.length', bdc_statuts.length)
console.log('bdc_statuts unique CD_NOM', new Set(bdc_statuts.map(({CD_NOM}) => CD_NOM)).size)
})

console.log('bdc_statuts unique CD_NOM', new Set(bdc_statuts.map(({CD_NOM}) => CD_NOM)).size)

// Espèces Manquantes
let espèce_manquantes_raw = dsvFormat(';').parse(readFileSync('data/sources_especes/espèces_manquantes.csv', 'utf-8'))
/** @type {Promise<BDC_STATUT_ROW[]>} */
const espèces_manquantesP = readFile('data/sources_especes/espèces_manquantes.csv', 'utf-8')
.then(espèces_manquantes_rawStr => {
/** @type {BDC_STATUT_ROW[]} */
// @ts-ignore
const espèces_manquantes_raw = dsvFormat(';').parse(espèces_manquantes_rawStr)
console.log('espèces_manquantes_raw.length', espèces_manquantes_raw.length)
return espèces_manquantes_raw
.map(({ CD_NOM, LABEL_STATUT }) => ({ CD_NOM, CD_REF: CD_NOM, CD_TYPE_STATUT: "Protection Pitchou", LABEL_STATUT }))
})

/** @type {Promise<BDC_STATUT_ROW[]>} */
const protectionsEspècesP = Promise.all([bdc_statutsP, espèces_manquantesP])
// @ts-ignore
.then(([bdc_statuts, espèce_manquantes]) => [...espèce_manquantes, ...bdc_statuts])


/**
*
* TAXREF
*
*/
const taxrefParser = parse({
delimiter: '\t',
columns: true,
trim: true
});


/** @type {Promise<TAXREF_ROW[]>} */
const taxrefP = new Promise((resolve, reject) => {

/** @type {TAXREF_ROW[]} */
const taxref = []
// Use the readable stream api to consume records
taxrefParser.on('readable', function(){
/** @type {TAXREF_ROW} */
let record;
while ((record = taxrefParser.read()) !== null) {
const {LB_NOM, CD_NOM, NOM_VERN, CD_REF, REGNE, CLASSE} = record
taxref.push({LB_NOM, CD_NOM, CD_REF, NOM_VERN, REGNE, CLASSE});
}
});

taxrefParser.on('error', reject);
taxrefParser.on('end', () => resolve(taxref));
})

console.log('espèce_manquantes_raw.length', espèce_manquantes_raw.length)
createReadStream('data/sources_especes/TAXREFv17.txt').pipe(taxrefParser)

taxrefP.then(taxref => {
console.log('taxref.length', taxref.length)
})

const espèces_manquantes = espèce_manquantes_raw.map(({ CD_NOM, LABEL_STATUT }) => ({ CD_NOM, CD_TYPE_STATUT: "Protection Pitchou", LABEL_STATUT }))
// @ts-ignore
const espèces_protégées = [].concat(bdc_statuts, espèces_manquantes)

// TAXREF
/**
*
* Génération du fichier liste espèces
*
*/

Promise.all([taxrefP, protectionsEspècesP])
.then(([taxref, protectionsEspèces]) => {
/** @type {Map<EspèceProtégées['CD_REF'], Partial<EspèceProtégées>>} */
const espècesProtégées = new Map()

for(const {CD_REF, CD_TYPE_STATUT} of protectionsEspèces){
let espèceProtégée = espècesProtégées.get(CD_REF)

if(!espèceProtégée){
espèceProtégée = {
CD_REF,
CD_TYPE_STATUTS: new Set(),
nomsScientifiques: new Set(),
nomsVernaculaires: new Set(),
}
espècesProtégées.set(CD_REF, espèceProtégée)
}

// @ts-ignore
espèceProtégée.CD_TYPE_STATUTS.add(CD_TYPE_STATUT)
}

// Rajouter les noms de Taxref
for(const row of taxref){
const {CD_NOM, CD_REF, NOM_VERN, LB_NOM} = row
// uniquement pour les espèces protégées
const espèceProtégée = espècesProtégées.get(CD_REF)

if(espèceProtégée){
if(!espèceProtégée.classification){
espèceProtégée.classification = TAXREF_ROWClassification(row)
}

if(CD_REF === CD_NOM){
// mettre le nom de l'espèce de référence en premier
// @ts-ignore
espèceProtégée.nomsScientifiques = new Set([LB_NOM, ...espèceProtégée.nomsScientifiques])
espèceProtégée.nomsVernaculaires = new Set([...nomsVernaculaires(NOM_VERN), ...espèceProtégée.nomsVernaculaires])
}
else{
// @ts-ignore
espèceProtégée.nomsScientifiques.add(LB_NOM)
espèceProtégée.nomsVernaculaires = new Set([...espèceProtégée.nomsVernaculaires, ...nomsVernaculaires(NOM_VERN)])
}
}
}

// nettoyage, car parfois certaines espèces protégées n'ont pas été trouvées dans TAXREF
for(const [CD_REF, espèce] of espècesProtégées){
if(!espèce.classification){
console.warn(`Espèce sans classification CD_REF ${CD_REF}`)
espècesProtégées.delete(CD_REF)
}
else{
//@ts-ignore
if(espèce.nomsScientifiques.size === 0 && espèce.nomsVernaculaires.size === 0){
console.warn(`Espèce sans noms CD_REF ${CD_REF}`)
espècesProtégées.delete(CD_REF)
}
}
}



const stringifier = stringify({
delimiter: ';',
header: true
});

stringifier.pipe(createWriteStream('data/liste-espèces-protégées.csv'))

for(const {CD_REF, classification, nomsScientifiques, nomsVernaculaires, CD_TYPE_STATUTS} of [...espècesProtégées.values()]){
stringifier.write({
CD_REF,
classification,
nomsScientifiques: [...nomsScientifiques].join(','),
nomsVernaculaires: [...nomsVernaculaires].join(','),
CD_TYPE_STATUTS: [...CD_TYPE_STATUTS].join(',')
})
}
stringifier.end()

})

let taxref_raw = tsvParse(readFileSync('data/sources_especes/TAXREFv17.txt', 'utf-8'))

console.log('taxref_raw.length', taxref_raw.length)

const taxref = taxref_raw
.filter(({CD_NOM, CD_REF}) => CD_NOM === CD_REF)
.map(({LB_NOM, NOM_COMPLET_HTML, CD_NOM, NOM_VERN, REGNE, CLASSE}) =>
({LB_NOM, NOM_COMPLET_HTML, CD_NOM, NOM_VERN, REGNE, CLASSE}))


/*
// @ts-ignore
taxref_raw = undefined // for GC
Expand Down Expand Up @@ -74,4 +240,6 @@ const classes = new Set(output.map(({CLASSE}) => CLASSE))
console.log('Liste des classes (tout règne confondu) parmi les espèces protégées :', [...classes].join(', '))
writeFileSync('data/liste_especes.csv', dsvFormat(';').format(output))
writeFileSync('data/liste_especes.csv', dsvFormat(';').format(output))
*/
Loading

0 comments on commit 20fbe14

Please sign in to comment.