Skip to content

Commit

Permalink
Feat: query postgres extended voie (#479)
Browse files Browse the repository at this point in the history
* fix: query postgres extended bal

* fix: query postgres extended voie

* bbox in voie entitie

* correct test

* delete console.log

* correct migration

* delete postdeploy from ProcFile

* correct agg comments voies
  • Loading branch information
fufeck authored Oct 16, 2024
1 parent 17816d8 commit 863c0d3
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 81 deletions.
1 change: 0 additions & 1 deletion Procfile
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
postdeploy: yarn typeorm:migration:run
clock: yarn start:cron
6 changes: 4 additions & 2 deletions apps/api/src/modules/base_locale/base_locale.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,10 @@ export class BaseLocaleController {
const voies: Voie[] = await this.voieService.findMany({
balId: req.baseLocale.id,
});
const extendedVoie: ExtendedVoieDTO[] =
await this.voieService.extendVoies(voies);
const extendedVoie: ExtendedVoieDTO[] = await this.voieService.extendVoies(
req.baseLocale.id,
voies,
);
res.status(HttpStatus.OK).json(extendedVoie);
}

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/modules/base_locale/base_locale.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ export class BaseLocaleService {
balId: baseLocale.id,
});
await Promise.all(
voiesCreated.map(({ id }) => this.voieService.calcCentroid(id)),
voiesCreated.map(({ id }) => this.voieService.calcCentroidAndBbox(id)),
);
// On retourne la Bal
return baseLocale;
Expand Down
74 changes: 58 additions & 16 deletions apps/api/src/modules/numeros/numero.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Point,
Repository,
UpdateResult,
Polygon,
} from 'typeorm';
import { v4 as uuid } from 'uuid';
import { pick, chunk } from 'lodash';
Expand Down Expand Up @@ -87,6 +88,31 @@ export class NumeroService {
});
}

async countVoiesNumeroAndCertifie(balId: string): Promise<
{
voieId: string;
nbNumeros: string;
nbNumerosCertifies: string;
comments: string[];
}[]
> {
const query = this.numerosRepository
.createQueryBuilder('numeros')
.select('numeros.voie_id', 'voieId')
.addSelect('count(numeros.id)', 'nbNumeros')
.addSelect(
'count(CASE WHEN numeros.certifie THEN true END)',
'nbNumerosCertifies',
)
.addSelect(
`array_remove(array_agg(CASE WHEN numeros.comment IS NOT NULL THEN concat(numeros.numero, numeros.suffixe, ' - ', numeros.comment) END), NULL)`,
'comments',
)
.where('numeros.bal_id = :balId', { balId })
.groupBy('numeros.voie_id');
return query.getRawMany();
}

async countBalNumeroAndCertifie(balId: string): Promise<{
nbNumeros: string;
nbNumerosCertifies: string;
Expand Down Expand Up @@ -270,7 +296,7 @@ export class NumeroService {
const numeroCreated: Numero =
await this.numerosRepository.save(entityToSave);
// On calcule le centroid de la voie
await this.voieService.calcCentroid(voie.id);
await this.voieService.calcCentroidAndBbox(voie.id);
// On met a jour le updatedAt de la Bal
await this.baseLocaleService.touch(numero.balId);
return numeroCreated;
Expand Down Expand Up @@ -312,11 +338,11 @@ export class NumeroService {
// Si le numero a été modifié
if (updateNumeroDto.voieId) {
// On recalcule le centroid de l'ancienne et la nouvelle voie si le numero a changé de voie
await this.voieService.calcCentroid(numero.voieId);
await this.voieService.calcCentroid(numeroUpdated.voieId);
await this.voieService.calcCentroidAndBbox(numero.voieId);
await this.voieService.calcCentroidAndBbox(numeroUpdated.voieId);
} else if (updateNumeroDto.positions) {
// On recalcule le centroid de la voie si les positions du numeros on changé
await this.voieService.calcCentroid(numero.voieId);
await this.voieService.calcCentroidAndBbox(numero.voieId);
}
// On met a jour le updatedAt de la voie
await this.voieService.touch(numero.voieId);
Expand Down Expand Up @@ -353,7 +379,7 @@ export class NumeroService {
// Si le numero a été suprimé
if (affected > 0) {
// On recalcule le centroid de la voie du numéro
await this.voieService.calcCentroid(numero.voieId);
await this.voieService.calcCentroidAndBbox(numero.voieId);
// On met a jour le updatedAt de la bal, la voie et le toponyme
await this.touch(numero);
}
Expand Down Expand Up @@ -446,9 +472,9 @@ export class NumeroService {
await this.voieService.touch(changes.voieId);
// On recalcule tous les centroid des voies
await Promise.all(
voieIds.map((voieId) => this.voieService.calcCentroid(voieId)),
voieIds.map((voieId) => this.voieService.calcCentroidAndBbox(voieId)),
);
await this.voieService.calcCentroid(changes.voieId);
await this.voieService.calcCentroidAndBbox(changes.voieId);
} else {
// Sinon on met a jour les updatedAt des voies des numeros
await Promise.all(
Expand Down Expand Up @@ -494,7 +520,7 @@ export class NumeroService {
await this.baseLocaleService.touch(baseLocale.id);
// On met a jour les centroid des voies des numeros archivé
await Promise.all(
voieIds.map((voidId) => this.voieService.calcCentroid(voidId)),
voieIds.map((voidId) => this.voieService.calcCentroidAndBbox(voidId)),
);
// Si les numeros avaient des toponyme, on met a jour leurs updatedAt
if (toponymeIds.length > 0) {
Expand Down Expand Up @@ -544,14 +570,30 @@ export class NumeroService {
}
}

public async findCentroid(voieId: string): Promise<Point> {
const res: { st_asgeojson: string }[] = await this.numerosRepository
.createQueryBuilder('numeros')
.select('ST_AsGeoJSON(st_centroid(st_union(positions.point)))')
.leftJoin('numeros.positions', 'positions')
.where('numeros.voie_id = :voieId', { voieId })
.execute();
return JSON.parse(res[0].st_asgeojson);
public async findCentroidAndBboxVoie(
voieId: string,
): Promise<{ centroid: Point; polygon: Polygon } | undefined> {
const res: { centroid: string; polygon: string } =
await this.numerosRepository
.createQueryBuilder('numeros')
.select(
'ST_AsGeoJSON(st_centroid(st_union(positions.point)))',
'centroid',
)
.addSelect(
'ST_AsGeoJSON(ST_Extent(positions.point::geometry))',
'polygon',
)
.leftJoin('numeros.positions', 'positions')
.where('numeros.voie_id = :voieId', { voieId })
.groupBy('numeros.voie_id')
.getRawOne();
return (
res && {
centroid: JSON.parse(res.centroid),
polygon: JSON.parse(res.polygon),
}
);
}

async touch(numero: Numero, updatedAt: Date = new Date()) {
Expand Down
7 changes: 1 addition & 6 deletions apps/api/src/modules/voie/dto/extended_voie.dto.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { BBox as BboxTurf } from '@turf/helpers';

import { Numero } from '@/shared/entities/numero.entity';
import { Voie } from '@/shared/entities/voie.entity';

export class ExtendedVoieDTO extends Voie {
Expand All @@ -15,8 +13,5 @@ export class ExtendedVoieDTO extends Voie {
isAllCertified?: boolean;

@ApiProperty()
commentedNumeros?: Numero[];

@ApiProperty()
bbox?: BboxTurf;
comments?: string[];
}
124 changes: 69 additions & 55 deletions apps/api/src/modules/voie/voie.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,17 @@ import {
FindOptionsSelect,
FindOptionsWhere,
In,
Point,
Repository,
UpdateResult,
} from 'typeorm';
import { groupBy } from 'lodash';
import { keyBy } from 'lodash';
import * as turf from '@turf/turf';
import bbox from '@turf/bbox';
import { Feature as FeatureTurf, BBox as BboxTurf } from '@turf/helpers';
import { v4 as uuid } from 'uuid';

import {
BaseLocale,
StatusBaseLocalEnum,
} from '@/shared/entities/base_locale.entity';
import { extendWithNumeros } from '@/shared/utils/numero.utils';
import { Position } from '@/shared/entities/position.entity';
import { Numero } from '@/shared/entities/numero.entity';
import { Voie, TypeNumerotationEnum } from '@/shared/entities/voie.entity';
import { Toponyme } from '@/shared/entities/toponyme.entity';

Expand Down Expand Up @@ -147,6 +141,7 @@ export class VoieService {
trace: createVoieDto.trace || null,
nomAlt: createVoieDto.nomAlt ? cleanNomAlt(createVoieDto.nomAlt) : null,
centroid: null,
bbox: null,
};
// Calculer le centroid si la trace et le type de numerotation est metrique
if (voie.trace && voie.typeNumerotation === TypeNumerotationEnum.METRIQUE) {
Expand Down Expand Up @@ -217,7 +212,7 @@ export class VoieService {
updateVoieDto.trace &&
voieUpdated.typeNumerotation === TypeNumerotationEnum.METRIQUE
) {
await this.calcCentroidWithTrace(voieUpdated);
await this.calcCentroidAndBboxWithTrace(voieUpdated);
}
// On met a jour le updatedAt de la BAL
await this.baseLocaleService.touch(
Expand Down Expand Up @@ -275,7 +270,7 @@ export class VoieService {
id: In(numerosIds),
});
// On met a jour le centroid de la voie
this.calcCentroid(voie.id);
this.calcCentroidAndBbox(voie.id);
}
// On met a jour le updatedAt de la BAL
await this.baseLocaleService.touch(voie.balId);
Expand Down Expand Up @@ -328,84 +323,103 @@ export class VoieService {
return toponyme;
}

public async extendVoies(voies: Voie[]): Promise<ExtendedVoieDTO[]> {
const numeros = await this.numeroService.findMany(
{
voieId: In(voies.map(({ id }) => id)),
},
{ certifie: true, comment: true, voieId: true },
public async extendVoies(
balId: string,
voies: Voie[],
): Promise<ExtendedVoieDTO[]> {
const voiesMetas =
await this.numeroService.countVoiesNumeroAndCertifie(balId);
const voiesMetasIndex = keyBy(voiesMetas, 'voieId');

return voies.map((voie) =>
this.extendVoieWithMeta(voie, voiesMetasIndex[voie.id]),
);
const numerosByVoies = groupBy(numeros, 'voieId');
}

return voies.map((voie) => ({
...extendWithNumeros(voie, numerosByVoies[voie.id] || []),
bbox: this.getBBOX(voie, numerosByVoies[voie.id] || []),
}));
private extendVoieWithMeta(
voie: Voie,
voieMeta?: {
voieId: string;
nbNumeros: string;
nbNumerosCertifies: string;
comments: string[];
},
): ExtendedVoieDTO {
const nbNumeros: number = Number(voieMeta?.nbNumeros) || 0;
const nbNumerosCertifies: number =
Number(voieMeta?.nbNumerosCertifies) || 0;
return {
...voie,
nbNumeros,
nbNumerosCertifies,
isAllCertified: nbNumeros > 0 ? nbNumeros === nbNumerosCertifies : false,
comments: voieMeta?.comments || [],
};
}

public async extendVoie(voie: Voie): Promise<ExtendedVoieDTO> {
const numeros = await this.numeroService.findMany({
voieId: voie.id,
});

const nbNumerosCertifies = numeros.filter(
(n) => n.certifie === true,
).length;

return {
...extendWithNumeros(voie, numeros),
bbox: this.getBBOX(voie, numeros),
...voie,
nbNumeros: numeros.length,
nbNumerosCertifies: nbNumerosCertifies,
isAllCertified:
numeros.length > 0 && numeros.length === nbNumerosCertifies,
comments: numeros
.filter(
(n) =>
n.comment !== undefined && n.comment !== null && n.comment !== '',
)
.map(
({ numero, suffixe, comment }) => `${numero}${suffixe} - ${comment}`,
),
};
}

public async touch(voieId: string, updatedAt: Date = new Date()) {
return this.voiesRepository.update({ id: voieId }, { updatedAt });
}

public async calcCentroid(voieId: string): Promise<void> {
public async calcCentroidAndBbox(voieId: string): Promise<void> {
// On récupère la voie
const voie: Voie = await this.findOneOrFail(voieId);
if (voie.typeNumerotation === TypeNumerotationEnum.NUMERIQUE) {
// On calcule la voie avec les numero si la voie est numerique
await this.calcCentroidWithNumeros(voieId);
await this.calcCentroidAndBboxWithNumeros(voieId);
} else if (
voie.trace &&
voie.typeNumerotation === TypeNumerotationEnum.METRIQUE
) {
// On calcul la voie avec la trace si la voie est metrique
await this.calcCentroidWithTrace(voie);
await this.calcCentroidAndBboxWithTrace(voie);
}
}

private async calcCentroidWithNumeros(voieId: string): Promise<void> {
const centroid: Point = await this.numeroService.findCentroid(voieId);
await this.voiesRepository.update({ id: voieId }, { centroid });
private async calcCentroidAndBboxWithNumeros(voieId: string): Promise<void> {
const res = await this.numeroService.findCentroidAndBboxVoie(voieId);
if (res) {
const { centroid, polygon } = res;
const bbox: number[] = turf.bbox(polygon);
await this.voiesRepository.update({ id: voieId }, { centroid, bbox });
} else {
await this.voiesRepository.update(
{ id: voieId },
{ centroid: null, bbox: null },
);
}
}

private async calcCentroidWithTrace(voie: Voie): Promise<void> {
private async calcCentroidAndBboxWithTrace(voie: Voie): Promise<void> {
const centroid = turf.centroid(voie.trace)?.geometry;
await this.voiesRepository.update({ id: voie.id }, { centroid });
}

private getBBOX(voie: Voie, numeros: Numero[]): BboxTurf {
// On récupère toutes les positions des numeros de la voie
const allPositions: Position[] = numeros
.filter((n) => n.positions && n.positions.length > 0)
.reduce((acc, n) => [...acc, ...n.positions], []);

if (allPositions.length > 0) {
// Si il y a des positions de numeros
// On créer un feature collection avec turf
const features: FeatureTurf[] = allPositions.map(({ point }) =>
turf.feature(point),
);
const featuresCollection = turf.featureCollection(features);
// On créer un bbox a partir de la feature collection
return bbox(featuresCollection);
} else if (
voie.trace &&
voie.typeNumerotation === TypeNumerotationEnum.METRIQUE
) {
// Si la voie a une trace et est de type metrique
// On créer un bbox a partir de la trace
return bbox(voie.trace);
}
const bbox = turf.bbox(voie.trace);
await this.voiesRepository.update({ id: voie.id }, { centroid, bbox });
}

async getFilairesVoies(): Promise<FilaireVoieDTO[]> {
Expand Down
4 changes: 4 additions & 0 deletions libs/shared/src/entities/voie.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export class Voie extends GlobalEntity {
})
trace: LineString | null;

@ApiProperty()
@Column('float', { nullable: true, array: true })
bbox: number[] | null;

@ApiProperty({ type: () => BaseLocale })
@ManyToOne(() => BaseLocale, (baseLocale) => baseLocale.voies, {
onDelete: 'CASCADE',
Expand Down
Loading

0 comments on commit 863c0d3

Please sign in to comment.