From 3b00dcc9fc333adbbe8d6520e8d5d1b551942ee7 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Thu, 26 Mar 2026 17:30:00 +0100 Subject: [PATCH 1/8] feat: add commune status layer --- src/modules/api-depot/api-depot.service.ts | 12 ++ .../setting/commune-status-cache.service.ts | 144 ++++++++++++++++++ src/modules/setting/setting.module.ts | 5 +- src/modules/setting/setting.service.ts | 140 ++++++++++++++++- src/modules/tiles/tiles.controller.ts | 63 +++++++- src/modules/tiles/tiles.module.ts | 2 + 6 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 src/modules/setting/commune-status-cache.service.ts diff --git a/src/modules/api-depot/api-depot.service.ts b/src/modules/api-depot/api-depot.service.ts index c09a943..c5ca48f 100644 --- a/src/modules/api-depot/api-depot.service.ts +++ b/src/modules/api-depot/api-depot.service.ts @@ -25,4 +25,16 @@ export class ApiDepotService { return revision; } + + public async getAllCurrentRevisions(): Promise { + const { data: revisions } = await firstValueFrom( + this.httpService.get('/current-revisions').pipe( + catchError((error: AxiosError) => { + return of({ data: [] as Revision[] }); + }), + ), + ); + + return revisions; + } } diff --git a/src/modules/setting/commune-status-cache.service.ts b/src/modules/setting/commune-status-cache.service.ts new file mode 100644 index 0000000..3f569c2 --- /dev/null +++ b/src/modules/setting/commune-status-cache.service.ts @@ -0,0 +1,144 @@ +import { + forwardRef, + Inject, + Injectable, + Logger, + OnModuleInit, +} from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import * as GeoJSONVT from 'geojson-vt'; +import { SettingService } from './setting.service'; +import { getCommune } from '../../utils/cog.utils'; + +interface CommuneFeature { + codeCommune: string; + nomCommune?: string; + mode?: string; + filteredSources?: string[]; +} + +@Injectable() +export class CommuneStatusCacheService implements OnModuleInit { + private cachedFeatures: GeoJSON.Feature[] = []; + private sourceTileIndexes = new Map>(); + private readonly logger = new Logger(CommuneStatusCacheService.name); + + constructor( + @Inject(forwardRef(() => SettingService)) + private readonly settingService: SettingService, + ) {} + + async onModuleInit() { + try { + await this.buildCache(); + } catch (err) { + this.logger.error( + 'Failed to build commune status cache on init', + err.message, + ); + } + } + + @Cron(CronExpression.EVERY_HOUR) + async refreshCache() { + try { + await this.buildCache(); + } catch (err) { + this.logger.error('Failed to refresh commune status cache', err.message); + } + } + + private async buildCache() { + this.logger.log('Building commune status cache...'); + + const [statuses, contours] = await Promise.all([ + this.settingService.computeAllCommuneStatuses(), + this.fetchCommuneContours(), + ]); + + const features: GeoJSON.Feature[] = []; + + for (const [codeCommune, status] of statuses) { + if (status.disabled) continue; + + const contour = contours.get(codeCommune); + if (!contour) continue; + + const commune = getCommune(codeCommune); + + const properties: CommuneFeature = { + codeCommune, + nomCommune: commune?.nom, + mode: status.mode, + }; + + if (status.filteredSources?.length) { + properties.filteredSources = status.filteredSources; + } + + features.push({ + type: 'Feature', + geometry: contour, + properties, + }); + } + + this.cachedFeatures = features; + this.sourceTileIndexes.clear(); + + this.logger.log( + `Commune status cache built: ${features.length} enabled communes`, + ); + } + + private async fetchCommuneContours(): Promise< + Map + > { + const response = await fetch( + 'https://object.data.gouv.fr/contours-administratifs/2025/geojson/communes-100m.geojson', + ); + const communes: GeoJSON.FeatureCollection = await response.json(); + + const map = new Map(); + for (const commune of communes.features) { + if ( + commune.geometry.type === 'Polygon' || + commune.geometry.type === 'MultiPolygon' + ) { + map.set( + commune.properties.code, + commune.geometry as GeoJSON.Polygon | GeoJSON.MultiPolygon, + ); + } + } + + return map; + } + + private getSourceTileIndex(sourceId: string): ReturnType { + let index = this.sourceTileIndexes.get(sourceId); + if (!index) { + const filtered = this.cachedFeatures.filter((f) => { + const props = f.properties as CommuneFeature; + return !props.filteredSources?.includes(sourceId); + }); + index = GeoJSONVT( + { type: 'FeatureCollection', features: filtered }, + { maxZoom: 10, indexMaxZoom: 10, tolerance: 0, buffer: 64 }, + ); + this.sourceTileIndexes.set(sourceId, index); + } + return index; + } + + getTileForSource( + z: number, + x: number, + y: number, + sourceId: string, + ): GeoJSONVT.Tile | null { + if (!this.cachedFeatures.length) return null; + const index = this.getSourceTileIndex(sourceId); + return index.getTile(z, x, y); + } +} diff --git a/src/modules/setting/setting.module.ts b/src/modules/setting/setting.module.ts index 68f85f1..69fbf07 100644 --- a/src/modules/setting/setting.module.ts +++ b/src/modules/setting/setting.module.ts @@ -5,6 +5,7 @@ import { SettingService } from './setting.service'; import { SettingController } from './setting.controller'; import { ApiDepotModule } from '../api-depot/api-depot.module'; import { SourceModule } from '../source/source.module'; +import { CommuneStatusCacheService } from './commune-status-cache.service'; @Module({ imports: [ @@ -13,7 +14,7 @@ import { SourceModule } from '../source/source.module'; forwardRef(() => SourceModule), ], controllers: [SettingController], - providers: [SettingService], - exports: [SettingService], + providers: [SettingService, CommuneStatusCacheService], + exports: [SettingService, CommuneStatusCacheService], }) export class SettingModule {} diff --git a/src/modules/setting/setting.service.ts b/src/modules/setting/setting.service.ts index f4c5a3b..b59432c 100644 --- a/src/modules/setting/setting.service.ts +++ b/src/modules/setting/setting.service.ts @@ -1,5 +1,6 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { Repository } from 'typeorm'; +import { Like, Repository } from 'typeorm'; +import { Revision } from '../api-depot/api-depot.types'; import { InjectRepository } from '@nestjs/typeorm'; import { Setting } from './setting.entity'; import { ApiDepotService } from '../api-depot/api-depot.service'; @@ -8,6 +9,7 @@ import { CommuneSettingsDTO } from './dto/commune-settings.dto'; import { CommuneStatusDTO } from './dto/commune-status.dto'; import { SourceService } from '../source/source.service'; import { EnabledListDTO } from './dto/enabled-list.dto'; +import { CommuneStatusCacheService } from './commune-status-cache.service'; const ObjectIdRE = new RegExp('^[0-9a-fA-F]{24}$'); @@ -18,6 +20,7 @@ export class SettingService { private readonly settingsRepository: Repository, private readonly apiDepotService: ApiDepotService, private readonly sourceService: SourceService, + private readonly communeStatusCacheService: CommuneStatusCacheService, ) {} getCommuneSettingsKey(codeCommune: string): string { @@ -186,6 +189,8 @@ export class SettingService { await this.settingsRepository.save(setting); + this.communeStatusCacheService.refreshCache(); + return setting.content as CommuneSettingsDTO; } @@ -229,6 +234,139 @@ export class SettingService { await this.settingsRepository.save(setting); } + this.communeStatusCacheService.refreshCache(); + return updatedEnabledList; } + + async computeAllCommuneStatuses(): Promise< + Map< + string, + { + disabled: boolean; + mode?: SignalementSubmissionMode; + filteredSources?: string[]; + } + > + > { + const [ + allRevisions, + communeSettingsList, + moissonneurWhitelist, + apiDepotClientWhitelist, + ] = await Promise.all([ + this.apiDepotService.getAllCurrentRevisions(), + this.settingsRepository.find({ + where: { name: Like('%-settings') }, + }), + this.settingsRepository.findOne({ + where: { name: EnabledListKeys.SOURCES_MOISSONNEUR_ENABLED }, + }), + this.settingsRepository.findOne({ + where: { name: EnabledListKeys.API_DEPOT_CLIENTS_ENABLED }, + }), + ]); + + const communeSettingsMap = new Map(); + for (const setting of communeSettingsList) { + const codeCommune = setting.name.replace('-settings', ''); + communeSettingsMap.set( + codeCommune, + setting.content as CommuneSettingsDTO, + ); + } + + const moissonneurWhitelistContent = + (moissonneurWhitelist?.content as string[]) || []; + const apiDepotClientWhitelistContent = + (apiDepotClientWhitelist?.content as string[]) || []; + + const result = new Map< + string, + { + disabled: boolean; + mode?: SignalementSubmissionMode; + filteredSources?: string[]; + } + >(); + + // Index revisions by codeCommune + const revisionsByCommune = new Map(); + for (const revision of allRevisions) { + revisionsByCommune.set(revision.codeCommune, revision); + } + + // Process all communes that have either a revision or custom settings + const allCodeCommunes = new Set([ + ...revisionsByCommune.keys(), + ...communeSettingsMap.keys(), + ]); + + for (const codeCommune of allCodeCommunes) { + const communeSettings = communeSettingsMap.get(codeCommune); + + // Settings override takes priority + if (communeSettings) { + if (communeSettings.disabled) { + result.set(codeCommune, { disabled: true }); + continue; + } + result.set(codeCommune, { + disabled: false, + mode: communeSettings.mode || SignalementSubmissionMode.FULL, + ...(communeSettings.filteredSources?.length && { + filteredSources: communeSettings.filteredSources, + }), + }); + continue; + } + + const revision = revisionsByCommune.get(codeCommune); + + if (!revision) { + result.set(codeCommune, { disabled: true }); + continue; + } + + // Published via mes-adresses + if ( + revision.context?.extras?.balId && + ObjectIdRE.test(revision.context.extras.balId) + ) { + result.set(codeCommune, { + disabled: false, + mode: SignalementSubmissionMode.FULL, + }); + continue; + } + + // Published via moissonneur + if (revision.context?.extras?.sourceId) { + const isEnabled = moissonneurWhitelistContent.includes( + revision.context.extras.sourceId, + ); + result.set(codeCommune, { + disabled: !isEnabled, + ...(isEnabled && { mode: SignalementSubmissionMode.LIGHT }), + }); + continue; + } + + // Published via API depot client + if (revision.client?.id) { + const isEnabled = apiDepotClientWhitelistContent.includes( + revision.client.id, + ); + result.set(codeCommune, { + disabled: !isEnabled, + ...(isEnabled && { mode: SignalementSubmissionMode.LIGHT }), + }); + continue; + } + + result.set(codeCommune, { disabled: true }); + } + + return result; + } } diff --git a/src/modules/tiles/tiles.controller.ts b/src/modules/tiles/tiles.controller.ts index 3e8878d..2088ee3 100644 --- a/src/modules/tiles/tiles.controller.ts +++ b/src/modules/tiles/tiles.controller.ts @@ -19,6 +19,7 @@ import { promisify } from 'util'; import * as zlib from 'zlib'; import * as vtpbf from 'vt-pbf'; import { TilesService, TilesLayerEnum } from './tiles.service'; +import { CommuneStatusCacheService } from '../setting/commune-status-cache.service'; import { ReportStatusEnum } from '../../common/report-status.enum'; const gzip = promisify(zlib.gzip); @@ -26,7 +27,10 @@ const gzip = promisify(zlib.gzip); @Controller('tiles') @ApiTags('tiles') export class TilesController { - constructor(private tilesService: TilesService) {} + constructor( + private tilesService: TilesService, + private communeStatusCacheService: CommuneStatusCacheService, + ) {} @Get('/:z/:x/:y.pbf') @ApiOperation({ @@ -48,7 +52,7 @@ export class TilesController { enum: TilesLayerEnum, isArray: true, description: - 'Layers to include in the tiles (defaults to both alerts and signalements)', + 'Layers to include in the tiles (defaults to alerts and signalements)', example: [TilesLayerEnum.ALERTS, TilesLayerEnum.SIGNALEMENTS], }) @ApiResponse({ @@ -99,4 +103,59 @@ export class TilesController { }) .send(compressedPbf); } + + @Get('/commune-status/:z/:x/:y.pbf') + @ApiOperation({ + summary: + 'Get vector tiles with commune status (enabled communes for a given source)', + operationId: 'getCommuneStatusTiles', + }) + @ApiParam({ name: 'z', required: true, type: String }) + @ApiParam({ name: 'x', required: true, type: String }) + @ApiParam({ name: 'y', required: true, type: String }) + @ApiQuery({ + name: 'sourceId', + required: true, + type: String, + description: 'Source ID to filter commune status', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'PBF vector tile with commune-status layer', + }) + @ApiResponse({ + status: HttpStatus.NO_CONTENT, + description: 'No data for this tile', + }) + async getCommuneStatusTiles( + @Query('sourceId') sourceId: string, + @Param('z') z: string, + @Param('x') x: string, + @Param('y') y: string, + @Res() res: Response, + ) { + const tile = this.communeStatusCacheService.getTileForSource( + parseInt(z), + parseInt(x), + parseInt(y), + sourceId, + ); + + // console.log('Tile requested for sourceId:', sourceId, 'Tile found:', tile); + + if (!tile) { + return res.status(HttpStatus.NO_CONTENT).send(); + } + + const pbf = vtpbf.fromGeojsonVt({ 'commune-status': tile }); + + const compressedPbf = await gzip(Buffer.from(pbf)); + + return res + .set({ + 'Content-Type': 'application/x-protobuf', + 'Content-Encoding': 'gzip', + }) + .send(compressedPbf); + } } diff --git a/src/modules/tiles/tiles.module.ts b/src/modules/tiles/tiles.module.ts index 98b370a..35de481 100644 --- a/src/modules/tiles/tiles.module.ts +++ b/src/modules/tiles/tiles.module.ts @@ -4,12 +4,14 @@ import { TilesService } from './tiles.service'; import { AlertModule } from '../alert/alert.module'; import { SignalementModule } from '../signalement/signalement.module'; import { ReportModule } from '../report/report.module'; +import { SettingModule } from '../setting/setting.module'; @Module({ imports: [ forwardRef(() => AlertModule), forwardRef(() => SignalementModule), forwardRef(() => ReportModule), + forwardRef(() => SettingModule), ], controllers: [TilesController], providers: [TilesService], From c76a32bdcd201cf667be5cca275805d090d851e3 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 27 Mar 2026 13:24:40 +0100 Subject: [PATCH 2/8] fix: optimize commune status layer --- .../setting/commune-status-cache.service.ts | 122 +++----- src/modules/setting/setting.service.ts | 268 ++++++++---------- src/modules/tiles/tiles.controller.ts | 11 +- src/tests/alert.spec.ts | 1 + src/tests/report.spec.ts | 1 + src/tests/signalement.spec.ts | 1 + 6 files changed, 163 insertions(+), 241 deletions(-) diff --git a/src/modules/setting/commune-status-cache.service.ts b/src/modules/setting/commune-status-cache.service.ts index 3f569c2..5ace31b 100644 --- a/src/modules/setting/commune-status-cache.service.ts +++ b/src/modules/setting/commune-status-cache.service.ts @@ -8,19 +8,11 @@ import { import { Cron, CronExpression } from '@nestjs/schedule'; import * as GeoJSONVT from 'geojson-vt'; import { SettingService } from './setting.service'; -import { getCommune } from '../../utils/cog.utils'; - -interface CommuneFeature { - codeCommune: string; - nomCommune?: string; - mode?: string; - filteredSources?: string[]; -} @Injectable() export class CommuneStatusCacheService implements OnModuleInit { - private cachedFeatures: GeoJSON.Feature[] = []; - private sourceTileIndexes = new Map>(); + private cachedIndex: ReturnType = null; + private communesWithContours: GeoJSON.FeatureCollection = null; private readonly logger = new Logger(CommuneStatusCacheService.name); constructor( @@ -30,7 +22,8 @@ export class CommuneStatusCacheService implements OnModuleInit { async onModuleInit() { try { - await this.buildCache(); + this.communesWithContours = await this.fetchCommuneContours(); + await this.buildCache(this.communesWithContours); } catch (err) { this.logger.error( 'Failed to build commune status cache on init', @@ -42,103 +35,74 @@ export class CommuneStatusCacheService implements OnModuleInit { @Cron(CronExpression.EVERY_HOUR) async refreshCache() { try { - await this.buildCache(); + await this.buildCache(this.communesWithContours); } catch (err) { this.logger.error('Failed to refresh commune status cache', err.message); } } - private async buildCache() { + private async buildCache(communes?: GeoJSON.FeatureCollection) { this.logger.log('Building commune status cache...'); - const [statuses, contours] = await Promise.all([ - this.settingService.computeAllCommuneStatuses(), - this.fetchCommuneContours(), - ]); - - const features: GeoJSON.Feature[] = []; - - for (const [codeCommune, status] of statuses) { - if (status.disabled) continue; - - const contour = contours.get(codeCommune); - if (!contour) continue; + if (!communes) { + communes = await this.fetchCommuneContours(); + } - const commune = getCommune(codeCommune); + const statuses = await this.settingService.computeAllCommuneStatuses(); - const properties: CommuneFeature = { - codeCommune, - nomCommune: commune?.nom, - mode: status.mode, - }; + const features: GeoJSON.Feature[] = []; - if (status.filteredSources?.length) { - properties.filteredSources = status.filteredSources; + for (const feature of communes.features) { + const { geometry, properties } = feature; + if (geometry.type !== 'Polygon' && geometry.type !== 'MultiPolygon') { + continue; // Skip non-polygon geometries + } + const codeCommune = properties.code; + const status = statuses.get(codeCommune); + + if (status) { + const { disabled, mode, filteredSources } = status; + properties.disabled = disabled; + if (mode) { + properties.mode = mode; + } + if (filteredSources) { + properties.filteredSources = filteredSources; + } + } else { + properties.disabled = true; // Default to disabled if no specific status } features.push({ type: 'Feature', - geometry: contour, + geometry, properties, }); } - this.cachedFeatures = features; - this.sourceTileIndexes.clear(); + this.cachedIndex = GeoJSONVT( + { type: 'FeatureCollection', features }, + { maxZoom: 11, indexMaxZoom: 11 }, + ); this.logger.log( - `Commune status cache built: ${features.length} enabled communes`, + `Commune status index cache ready with ${features.length} features`, ); } - private async fetchCommuneContours(): Promise< - Map - > { + private async fetchCommuneContours(): Promise { + this.logger.log('Fetching commune contours...'); + const response = await fetch( 'https://object.data.gouv.fr/contours-administratifs/2025/geojson/communes-100m.geojson', ); const communes: GeoJSON.FeatureCollection = await response.json(); - const map = new Map(); - for (const commune of communes.features) { - if ( - commune.geometry.type === 'Polygon' || - commune.geometry.type === 'MultiPolygon' - ) { - map.set( - commune.properties.code, - commune.geometry as GeoJSON.Polygon | GeoJSON.MultiPolygon, - ); - } - } - - return map; - } - - private getSourceTileIndex(sourceId: string): ReturnType { - let index = this.sourceTileIndexes.get(sourceId); - if (!index) { - const filtered = this.cachedFeatures.filter((f) => { - const props = f.properties as CommuneFeature; - return !props.filteredSources?.includes(sourceId); - }); - index = GeoJSONVT( - { type: 'FeatureCollection', features: filtered }, - { maxZoom: 10, indexMaxZoom: 10, tolerance: 0, buffer: 64 }, - ); - this.sourceTileIndexes.set(sourceId, index); - } - return index; + return communes; } - getTileForSource( - z: number, - x: number, - y: number, - sourceId: string, - ): GeoJSONVT.Tile | null { - if (!this.cachedFeatures.length) return null; - const index = this.getSourceTileIndex(sourceId); - return index.getTile(z, x, y); + getTileForSource(z: number, x: number, y: number): GeoJSONVT.Tile | null { + if (!this.cachedIndex) return null; + return this.cachedIndex.getTile(z, x, y); } } diff --git a/src/modules/setting/setting.service.ts b/src/modules/setting/setting.service.ts index b59432c..933ca14 100644 --- a/src/modules/setting/setting.service.ts +++ b/src/modules/setting/setting.service.ts @@ -27,60 +27,38 @@ export class SettingService { return `${codeCommune}-settings`; } - async getCommuneStatus( - codeCommune: string, - sourceId: string, - ): Promise { - // Check source id - await this.sourceService.findOneOrFail(sourceId); - - // First check if the commune is in the disabled list - const setting = await this.settingsRepository.findOne({ - where: { name: this.getCommuneSettingsKey(codeCommune) }, - }); - - const communesSettings = setting?.content as CommuneSettingsDTO; - - if (communesSettings) { - if (communesSettings.disabled) { - return { - disabled: true, - message: - communesSettings.message || - 'La commune a demandé la désactivation du dépôt de signalements. Nous vous recommandons de contacter directement la mairie.', - }; - } else if (communesSettings.filteredSources?.includes(sourceId)) { - return { - disabled: true, - message: - communesSettings.message || - 'La commune a demandé la désactivation de cette source de signalements. Nous vous recommandons de contacter directement la mairie.', - }; - } else { - return { - disabled: false, - mode: communesSettings.mode || SignalementSubmissionMode.FULL, - }; + private determineCommuneStatus( + communeSettings: CommuneSettingsDTO | null, + revision: Revision | null, + moissonneurWhitelistContent: string[], + apiDepotClientWhitelistContent: string[], + ): { + disabled: boolean; + mode?: SignalementSubmissionMode; + filteredSources?: string[]; + } { + // Settings override takes priority + if (communeSettings) { + if (communeSettings.disabled) { + return { disabled: true }; } - } - - // Then get current revision to know how commune is published - const currentRevision = - await this.apiDepotService.getCurrentRevision(codeCommune); - - // If the commune is not published, signalement is disabled - if (!currentRevision) { return { - disabled: true, - message: - "Les signalements ne peuvent pas être proposés sur cette commune car elle n'a pas publié sa Base Adresse Locale. Nous vous recommandons de contacter directement la mairie.", + disabled: false, + mode: communeSettings.mode || SignalementSubmissionMode.FULL, + ...(communeSettings.filteredSources?.length && { + filteredSources: communeSettings.filteredSources, + }), }; } - // If commune is published via mes-adresses, signalement is enabled + if (!revision) { + return { disabled: true }; + } + + // Published via mes-adresses if ( - currentRevision?.context?.extras?.balId && - ObjectIdRE.test(currentRevision.context.extras.balId) + revision.context?.extras?.balId && + ObjectIdRE.test(revision.context.extras.balId) ) { return { disabled: false, @@ -88,62 +66,98 @@ export class SettingService { }; } - // If the commune is published via moissonneur, check if the source is in the white list - if (currentRevision?.context?.extras?.sourceId) { - const moissonneurSourceWhiteList = await this.settingsRepository.findOne({ - where: { name: EnabledListKeys.SOURCES_MOISSONNEUR_ENABLED }, - }); - - const moissonneurSourceWhiteListContent = - moissonneurSourceWhiteList.content as string[]; - - const isSourceEnabled = moissonneurSourceWhiteListContent.includes( - currentRevision.context.extras.sourceId, + // Published via moissonneur + if (revision.context?.extras?.sourceId) { + const isEnabled = moissonneurWhitelistContent.includes( + revision.context.extras.sourceId, ); + return { + disabled: !isEnabled, + ...(isEnabled && { mode: SignalementSubmissionMode.LIGHT }), + }; + } - if (isSourceEnabled) { - return { - disabled: false, - mode: SignalementSubmissionMode.LIGHT, - }; - } else { - return { - disabled: true, - message: - 'Cette commune ne gère pas encore la prise en compte des signalements depuis notre site. Nous vous recommandons de contacter directement la mairie.', - }; - } + // Published via API depot client + if (revision.client?.id) { + const isEnabled = apiDepotClientWhitelistContent.includes( + revision.client.id, + ); + return { + disabled: !isEnabled, + ...(isEnabled && { mode: SignalementSubmissionMode.LIGHT }), + }; } - // If the commune is published via API depot, check if the client is in the white list - if (currentRevision?.client?.id) { - const apiDepotClientEnabled = await this.settingsRepository.findOne({ + return { disabled: true }; + } + + async getCommuneStatus( + codeCommune: string, + sourceId: string, + ): Promise { + // Check source id + await this.sourceService.findOneOrFail(sourceId); + + const [ + setting, + currentRevision, + moissonneurWhitelist, + apiDepotClientWhitelist, + ] = await Promise.all([ + this.settingsRepository.findOne({ + where: { name: this.getCommuneSettingsKey(codeCommune) }, + }), + this.apiDepotService.getCurrentRevision(codeCommune), + this.settingsRepository.findOne({ + where: { name: EnabledListKeys.SOURCES_MOISSONNEUR_ENABLED }, + }), + this.settingsRepository.findOne({ where: { name: EnabledListKeys.API_DEPOT_CLIENTS_ENABLED }, - }); + }), + ]); - const apiDepotClientEnabledContent = - apiDepotClientEnabled.content as string[]; + const communeSettings = (setting?.content as CommuneSettingsDTO) || null; + const moissonneurWhitelistContent = + (moissonneurWhitelist?.content as string[]) || []; + const apiDepotClientWhitelistContent = + (apiDepotClientWhitelist?.content as string[]) || []; - const isClientEnabled = apiDepotClientEnabledContent.includes( - currentRevision.client.id, - ); + const status = this.determineCommuneStatus( + communeSettings, + currentRevision, + moissonneurWhitelistContent, + apiDepotClientWhitelistContent, + ); - if (isClientEnabled) { - return { - disabled: false, - mode: SignalementSubmissionMode.LIGHT, - }; + // Handle sourceId-specific filtering + if (!status.disabled && status.filteredSources?.includes(sourceId)) { + return { + disabled: true, + message: + communeSettings?.message || + 'La commune a demandé la désactivation de cette source de signalements. Nous vous recommandons de contacter directement la mairie.', + }; + } + + if (status.disabled) { + let message: string | undefined; + if (communeSettings?.disabled) { + message = + communeSettings.message || + 'La commune a demandé la désactivation du dépôt de signalements. Nous vous recommandons de contacter directement la mairie.'; + } else if (!currentRevision) { + message = + "Les signalements ne peuvent pas être proposés sur cette commune car elle n'a pas publié sa Base Adresse Locale. Nous vous recommandons de contacter directement la mairie."; } else { - return { - disabled: true, - message: - 'Cette commune ne gère pas encore la prise en compte des signalements depuis notre site. Nous vous recommandons de contacter directement la mairie.', - }; + message = + 'Cette commune ne gère pas encore la prise en compte des signalements depuis notre site. Nous vous recommandons de contacter directement la mairie.'; } + return { disabled: true, message }; } return { - disabled: true, + disabled: false, + mode: status.mode, }; } @@ -303,68 +317,18 @@ export class SettingService { ]); for (const codeCommune of allCodeCommunes) { - const communeSettings = communeSettingsMap.get(codeCommune); - - // Settings override takes priority - if (communeSettings) { - if (communeSettings.disabled) { - result.set(codeCommune, { disabled: true }); - continue; - } - result.set(codeCommune, { - disabled: false, - mode: communeSettings.mode || SignalementSubmissionMode.FULL, - ...(communeSettings.filteredSources?.length && { - filteredSources: communeSettings.filteredSources, - }), - }); - continue; - } - - const revision = revisionsByCommune.get(codeCommune); - - if (!revision) { - result.set(codeCommune, { disabled: true }); - continue; - } - - // Published via mes-adresses - if ( - revision.context?.extras?.balId && - ObjectIdRE.test(revision.context.extras.balId) - ) { - result.set(codeCommune, { - disabled: false, - mode: SignalementSubmissionMode.FULL, - }); - continue; - } - - // Published via moissonneur - if (revision.context?.extras?.sourceId) { - const isEnabled = moissonneurWhitelistContent.includes( - revision.context.extras.sourceId, - ); - result.set(codeCommune, { - disabled: !isEnabled, - ...(isEnabled && { mode: SignalementSubmissionMode.LIGHT }), - }); - continue; - } + const communeSettings = communeSettingsMap.get(codeCommune) || null; + const revision = revisionsByCommune.get(codeCommune) || null; - // Published via API depot client - if (revision.client?.id) { - const isEnabled = apiDepotClientWhitelistContent.includes( - revision.client.id, - ); - result.set(codeCommune, { - disabled: !isEnabled, - ...(isEnabled && { mode: SignalementSubmissionMode.LIGHT }), - }); - continue; - } - - result.set(codeCommune, { disabled: true }); + result.set( + codeCommune, + this.determineCommuneStatus( + communeSettings, + revision, + moissonneurWhitelistContent, + apiDepotClientWhitelistContent, + ), + ); } return result; diff --git a/src/modules/tiles/tiles.controller.ts b/src/modules/tiles/tiles.controller.ts index 2088ee3..9ce51e5 100644 --- a/src/modules/tiles/tiles.controller.ts +++ b/src/modules/tiles/tiles.controller.ts @@ -106,19 +106,12 @@ export class TilesController { @Get('/commune-status/:z/:x/:y.pbf') @ApiOperation({ - summary: - 'Get vector tiles with commune status (enabled communes for a given source)', + summary: 'Get vector tiles with commune status (enabled communes)', operationId: 'getCommuneStatusTiles', }) @ApiParam({ name: 'z', required: true, type: String }) @ApiParam({ name: 'x', required: true, type: String }) @ApiParam({ name: 'y', required: true, type: String }) - @ApiQuery({ - name: 'sourceId', - required: true, - type: String, - description: 'Source ID to filter commune status', - }) @ApiResponse({ status: HttpStatus.OK, description: 'PBF vector tile with commune-status layer', @@ -128,7 +121,6 @@ export class TilesController { description: 'No data for this tile', }) async getCommuneStatusTiles( - @Query('sourceId') sourceId: string, @Param('z') z: string, @Param('x') x: string, @Param('y') y: string, @@ -138,7 +130,6 @@ export class TilesController { parseInt(z), parseInt(x), parseInt(y), - sourceId, ); // console.log('Tile requested for sourceId:', sourceId, 'Tile found:', tile); diff --git a/src/tests/alert.spec.ts b/src/tests/alert.spec.ts index 4ef6745..d7de73a 100644 --- a/src/tests/alert.spec.ts +++ b/src/tests/alert.spec.ts @@ -72,6 +72,7 @@ const mockAPIDepotService = { extras: { balId: '614b3385e1d1f2602d7ad284' }, }, }), + getAllCurrentRevisions: jest.fn().mockResolvedValue([]), }; @Module({ providers: [ diff --git a/src/tests/report.spec.ts b/src/tests/report.spec.ts index aa2b691..3a42618 100644 --- a/src/tests/report.spec.ts +++ b/src/tests/report.spec.ts @@ -38,6 +38,7 @@ const mockAPIDepotService = { extras: { balId: '614b3385e1d1f2602d7ad284' }, }, }), + getAllCurrentRevisions: jest.fn().mockResolvedValue([]), }; @Module({ providers: [ diff --git a/src/tests/signalement.spec.ts b/src/tests/signalement.spec.ts index 8c5ed80..706cc3c 100644 --- a/src/tests/signalement.spec.ts +++ b/src/tests/signalement.spec.ts @@ -87,6 +87,7 @@ const mockAPIDepotService = { extras: { balId: '614b3385e1d1f2602d7ad284' }, }, }), + getAllCurrentRevisions: jest.fn().mockResolvedValue([]), }; @Module({ providers: [ From 59fd32f4073be71bd4c6b81e5f9b80950651a6ab Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 27 Mar 2026 13:47:47 +0100 Subject: [PATCH 3/8] fix: use contour 1000m --- src/modules/setting/commune-status-cache.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/setting/commune-status-cache.service.ts b/src/modules/setting/commune-status-cache.service.ts index 5ace31b..7372953 100644 --- a/src/modules/setting/commune-status-cache.service.ts +++ b/src/modules/setting/commune-status-cache.service.ts @@ -94,7 +94,7 @@ export class CommuneStatusCacheService implements OnModuleInit { this.logger.log('Fetching commune contours...'); const response = await fetch( - 'https://object.data.gouv.fr/contours-administratifs/2025/geojson/communes-100m.geojson', + 'https://object.data.gouv.fr/contours-administratifs/2025/geojson/communes-1000m.geojson', ); const communes: GeoJSON.FeatureCollection = await response.json(); From d4077a4e0bc67e93e64fbfb6f72066f31ba6dd55 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 27 Mar 2026 14:58:20 +0100 Subject: [PATCH 4/8] fix: throw if error on get all current revisions --- src/modules/api-depot/api-depot.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/modules/api-depot/api-depot.service.ts b/src/modules/api-depot/api-depot.service.ts index c5ca48f..6e4f5ac 100644 --- a/src/modules/api-depot/api-depot.service.ts +++ b/src/modules/api-depot/api-depot.service.ts @@ -28,11 +28,7 @@ export class ApiDepotService { public async getAllCurrentRevisions(): Promise { const { data: revisions } = await firstValueFrom( - this.httpService.get('/current-revisions').pipe( - catchError((error: AxiosError) => { - return of({ data: [] as Revision[] }); - }), - ), + this.httpService.get('/current-revisions'), ); return revisions; From 22788f561364d0a33d92ff0cb6aa1caa9f8def7d Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 27 Mar 2026 14:58:33 +0100 Subject: [PATCH 5/8] dirty hack to test on staging --- src/modules/setting/setting.service.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/modules/setting/setting.service.ts b/src/modules/setting/setting.service.ts index 933ca14..f431ae1 100644 --- a/src/modules/setting/setting.service.ts +++ b/src/modules/setting/setting.service.ts @@ -320,6 +320,15 @@ export class SettingService { const communeSettings = communeSettingsMap.get(codeCommune) || null; const revision = revisionsByCommune.get(codeCommune) || null; + // Dirty hack to test on staging while waiting for the balId to be added in the revision context on production. Should be removed once the balId is available in production revisions. + if (revision.clientId === '651c1496142003dd4ba592ff') { + revision.context = { + extras: { + balId: '614b3385e1d1f2602d7ad284', + }, + }; + } + result.set( codeCommune, this.determineCommuneStatus( From 7e5960de9be9022ead7c4b961a920bec0fe35084 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 27 Mar 2026 15:06:46 +0100 Subject: [PATCH 6/8] fix: optimize RAM --- .../setting/commune-status-cache.service.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/modules/setting/commune-status-cache.service.ts b/src/modules/setting/commune-status-cache.service.ts index 7372953..2fe4fcb 100644 --- a/src/modules/setting/commune-status-cache.service.ts +++ b/src/modules/setting/commune-status-cache.service.ts @@ -12,7 +12,6 @@ import { SettingService } from './setting.service'; @Injectable() export class CommuneStatusCacheService implements OnModuleInit { private cachedIndex: ReturnType = null; - private communesWithContours: GeoJSON.FeatureCollection = null; private readonly logger = new Logger(CommuneStatusCacheService.name); constructor( @@ -22,8 +21,7 @@ export class CommuneStatusCacheService implements OnModuleInit { async onModuleInit() { try { - this.communesWithContours = await this.fetchCommuneContours(); - await this.buildCache(this.communesWithContours); + await this.buildCache(); } catch (err) { this.logger.error( 'Failed to build commune status cache on init', @@ -35,18 +33,16 @@ export class CommuneStatusCacheService implements OnModuleInit { @Cron(CronExpression.EVERY_HOUR) async refreshCache() { try { - await this.buildCache(this.communesWithContours); + await this.buildCache(); } catch (err) { this.logger.error('Failed to refresh commune status cache', err.message); } } - private async buildCache(communes?: GeoJSON.FeatureCollection) { - this.logger.log('Building commune status cache...'); + private async buildCache() { + this.logger.log('Building commune status index cache...'); - if (!communes) { - communes = await this.fetchCommuneContours(); - } + const communes = await this.fetchCommuneContours(); const statuses = await this.settingService.computeAllCommuneStatuses(); @@ -82,7 +78,7 @@ export class CommuneStatusCacheService implements OnModuleInit { this.cachedIndex = GeoJSONVT( { type: 'FeatureCollection', features }, - { maxZoom: 11, indexMaxZoom: 11 }, + { maxZoom: 11, indexMaxZoom: 9 }, ); this.logger.log( From b7af74ad3885b4649c3ec6ac5d56254f7a4f0b03 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 27 Mar 2026 16:53:49 +0100 Subject: [PATCH 7/8] fix: optimize feature --- .../setting/commune-settings-cache.service.ts | 71 ++++++++++++ .../setting/commune-status-cache.service.ts | 104 ------------------ .../setting/dto/commune-settings.dto.ts | 2 + src/modules/setting/dto/commune-status.dto.ts | 5 +- src/modules/setting/setting.controller.ts | 31 +++++- src/modules/setting/setting.module.ts | 6 +- src/modules/setting/setting.service.ts | 10 +- src/modules/tiles/tiles.controller.ts | 52 +-------- src/modules/tiles/tiles.module.ts | 2 - 9 files changed, 116 insertions(+), 167 deletions(-) create mode 100644 src/modules/setting/commune-settings-cache.service.ts delete mode 100644 src/modules/setting/commune-status-cache.service.ts diff --git a/src/modules/setting/commune-settings-cache.service.ts b/src/modules/setting/commune-settings-cache.service.ts new file mode 100644 index 0000000..c4e504d --- /dev/null +++ b/src/modules/setting/commune-settings-cache.service.ts @@ -0,0 +1,71 @@ +import { + forwardRef, + Inject, + Injectable, + Logger, + OnModuleInit, +} from '@nestjs/common'; +import { Cron, CronExpression } from '@nestjs/schedule'; +import { SettingService } from './setting.service'; +import { SignalementSubmissionMode } from './setting.type'; + +export type CommuneSettingsMap = Record< + string, + { + disabled: boolean; + mode?: SignalementSubmissionMode; + filteredSources?: string[]; + } +>; + +@Injectable() +export class CommuneSettingsCacheService implements OnModuleInit { + private cachedSettings: CommuneSettingsMap | null = null; + private readonly logger = new Logger(CommuneSettingsCacheService.name); + + constructor( + @Inject(forwardRef(() => SettingService)) + private readonly settingService: SettingService, + ) {} + + async onModuleInit() { + try { + await this.buildCache(); + } catch (err) { + this.logger.error( + 'Failed to build commune status cache on init', + err.message, + ); + } + } + + @Cron(CronExpression.EVERY_HOUR) + async refreshCache() { + try { + await this.buildCache(); + } catch (err) { + this.logger.error('Failed to refresh commune status cache', err.message); + } + } + + private async buildCache() { + this.logger.log('Building commune status cache...'); + + const statuses = await this.settingService.computeAllCommuneStatuses(); + + const result: CommuneSettingsMap = {}; + for (const [codeCommune, status] of statuses) { + result[codeCommune] = status; + } + + this.cachedSettings = result; + + this.logger.log( + `Commune settings cache ready with ${Object.keys(result).length} communes`, + ); + } + + getCachedSettings(): CommuneSettingsMap | null { + return this.cachedSettings; + } +} diff --git a/src/modules/setting/commune-status-cache.service.ts b/src/modules/setting/commune-status-cache.service.ts deleted file mode 100644 index 2fe4fcb..0000000 --- a/src/modules/setting/commune-status-cache.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - forwardRef, - Inject, - Injectable, - Logger, - OnModuleInit, -} from '@nestjs/common'; -import { Cron, CronExpression } from '@nestjs/schedule'; -import * as GeoJSONVT from 'geojson-vt'; -import { SettingService } from './setting.service'; - -@Injectable() -export class CommuneStatusCacheService implements OnModuleInit { - private cachedIndex: ReturnType = null; - private readonly logger = new Logger(CommuneStatusCacheService.name); - - constructor( - @Inject(forwardRef(() => SettingService)) - private readonly settingService: SettingService, - ) {} - - async onModuleInit() { - try { - await this.buildCache(); - } catch (err) { - this.logger.error( - 'Failed to build commune status cache on init', - err.message, - ); - } - } - - @Cron(CronExpression.EVERY_HOUR) - async refreshCache() { - try { - await this.buildCache(); - } catch (err) { - this.logger.error('Failed to refresh commune status cache', err.message); - } - } - - private async buildCache() { - this.logger.log('Building commune status index cache...'); - - const communes = await this.fetchCommuneContours(); - - const statuses = await this.settingService.computeAllCommuneStatuses(); - - const features: GeoJSON.Feature[] = []; - - for (const feature of communes.features) { - const { geometry, properties } = feature; - if (geometry.type !== 'Polygon' && geometry.type !== 'MultiPolygon') { - continue; // Skip non-polygon geometries - } - const codeCommune = properties.code; - const status = statuses.get(codeCommune); - - if (status) { - const { disabled, mode, filteredSources } = status; - properties.disabled = disabled; - if (mode) { - properties.mode = mode; - } - if (filteredSources) { - properties.filteredSources = filteredSources; - } - } else { - properties.disabled = true; // Default to disabled if no specific status - } - - features.push({ - type: 'Feature', - geometry, - properties, - }); - } - - this.cachedIndex = GeoJSONVT( - { type: 'FeatureCollection', features }, - { maxZoom: 11, indexMaxZoom: 9 }, - ); - - this.logger.log( - `Commune status index cache ready with ${features.length} features`, - ); - } - - private async fetchCommuneContours(): Promise { - this.logger.log('Fetching commune contours...'); - - const response = await fetch( - 'https://object.data.gouv.fr/contours-administratifs/2025/geojson/communes-1000m.geojson', - ); - const communes: GeoJSON.FeatureCollection = await response.json(); - - return communes; - } - - getTileForSource(z: number, x: number, y: number): GeoJSONVT.Tile | null { - if (!this.cachedIndex) return null; - return this.cachedIndex.getTile(z, x, y); - } -} diff --git a/src/modules/setting/dto/commune-settings.dto.ts b/src/modules/setting/dto/commune-settings.dto.ts index 9a478de..dc1e69f 100644 --- a/src/modules/setting/dto/commune-settings.dto.ts +++ b/src/modules/setting/dto/commune-settings.dto.ts @@ -1,7 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { CommuneStatusDTO } from './commune-status.dto'; +import { IsOptional } from 'class-validator'; export class CommuneSettingsDTO extends CommuneStatusDTO { @ApiProperty({ required: false, type: [String] }) + @IsOptional() filteredSources?: string[]; } diff --git a/src/modules/setting/dto/commune-status.dto.ts b/src/modules/setting/dto/commune-status.dto.ts index b80080b..6abf931 100644 --- a/src/modules/setting/dto/commune-status.dto.ts +++ b/src/modules/setting/dto/commune-status.dto.ts @@ -1,12 +1,14 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum } from 'class-validator'; +import { IsBoolean, IsEnum, IsOptional } from 'class-validator'; import { SignalementSubmissionMode } from '../setting.type'; export class CommuneStatusDTO { @ApiProperty({ required: true, nullable: false }) + @IsBoolean() disabled: boolean; @ApiProperty({ required: false }) + @IsOptional() message?: string; @ApiProperty({ @@ -15,5 +17,6 @@ export class CommuneStatusDTO { enum: SignalementSubmissionMode, }) @IsEnum(SignalementSubmissionMode) + @IsOptional() mode?: SignalementSubmissionMode; } diff --git a/src/modules/setting/setting.controller.ts b/src/modules/setting/setting.controller.ts index 192443b..0607059 100644 --- a/src/modules/setting/setting.controller.ts +++ b/src/modules/setting/setting.controller.ts @@ -26,11 +26,18 @@ import { CommuneStatusDTO } from './dto/commune-status.dto'; import { CommuneSettingsDTO } from './dto/commune-settings.dto'; import { EnabledListKeys } from './setting.type'; import { EnabledListDTO } from './dto/enabled-list.dto'; +import { + CommuneSettingsCacheService, + CommuneSettingsMap, +} from './commune-settings-cache.service'; @ApiTags('settings') @Controller('settings') export class SettingController { - constructor(private settingService: SettingService) {} + constructor( + private settingService: SettingService, + private communeSettingsCacheService: CommuneSettingsCacheService, + ) {} @Get('commune-status/:codeCommune') @ApiOperation({ @@ -116,6 +123,28 @@ export class SettingController { res.status(HttpStatus.OK).json(settings); } + @Get('commune-settings') + @ApiOperation({ + summary: 'Get the settings of all communes', + operationId: 'getAllCommuneSettings', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Map of commune codes to their settings', + schema: { + type: 'object', + additionalProperties: { + $ref: '#/components/schemas/CommuneSettingsDTO', + }, + }, + }) + async getAllCommuneSettings(@Res() res: Response) { + const settings: CommuneSettingsMap = + this.communeSettingsCacheService.getCachedSettings() || {}; + + res.status(HttpStatus.OK).json(settings); + } + @Get('enabled-list/:listKey/:id') @ApiOperation({ summary: diff --git a/src/modules/setting/setting.module.ts b/src/modules/setting/setting.module.ts index 69fbf07..385c6a9 100644 --- a/src/modules/setting/setting.module.ts +++ b/src/modules/setting/setting.module.ts @@ -5,7 +5,7 @@ import { SettingService } from './setting.service'; import { SettingController } from './setting.controller'; import { ApiDepotModule } from '../api-depot/api-depot.module'; import { SourceModule } from '../source/source.module'; -import { CommuneStatusCacheService } from './commune-status-cache.service'; +import { CommuneSettingsCacheService } from './commune-settings-cache.service'; @Module({ imports: [ @@ -14,7 +14,7 @@ import { CommuneStatusCacheService } from './commune-status-cache.service'; forwardRef(() => SourceModule), ], controllers: [SettingController], - providers: [SettingService, CommuneStatusCacheService], - exports: [SettingService, CommuneStatusCacheService], + providers: [SettingService, CommuneSettingsCacheService], + exports: [SettingService, CommuneSettingsCacheService], }) export class SettingModule {} diff --git a/src/modules/setting/setting.service.ts b/src/modules/setting/setting.service.ts index f431ae1..17ab0a7 100644 --- a/src/modules/setting/setting.service.ts +++ b/src/modules/setting/setting.service.ts @@ -9,7 +9,7 @@ import { CommuneSettingsDTO } from './dto/commune-settings.dto'; import { CommuneStatusDTO } from './dto/commune-status.dto'; import { SourceService } from '../source/source.service'; import { EnabledListDTO } from './dto/enabled-list.dto'; -import { CommuneStatusCacheService } from './commune-status-cache.service'; +import { CommuneSettingsCacheService } from './commune-settings-cache.service'; const ObjectIdRE = new RegExp('^[0-9a-fA-F]{24}$'); @@ -20,7 +20,7 @@ export class SettingService { private readonly settingsRepository: Repository, private readonly apiDepotService: ApiDepotService, private readonly sourceService: SourceService, - private readonly communeStatusCacheService: CommuneStatusCacheService, + private readonly communeSettingsCacheService: CommuneSettingsCacheService, ) {} getCommuneSettingsKey(codeCommune: string): string { @@ -203,7 +203,7 @@ export class SettingService { await this.settingsRepository.save(setting); - this.communeStatusCacheService.refreshCache(); + this.communeSettingsCacheService.refreshCache(); return setting.content as CommuneSettingsDTO; } @@ -248,7 +248,7 @@ export class SettingService { await this.settingsRepository.save(setting); } - this.communeStatusCacheService.refreshCache(); + this.communeSettingsCacheService.refreshCache(); return updatedEnabledList; } @@ -321,7 +321,7 @@ export class SettingService { const revision = revisionsByCommune.get(codeCommune) || null; // Dirty hack to test on staging while waiting for the balId to be added in the revision context on production. Should be removed once the balId is available in production revisions. - if (revision.clientId === '651c1496142003dd4ba592ff') { + if (revision?.clientId === '651c1496142003dd4ba592ff') { revision.context = { extras: { balId: '614b3385e1d1f2602d7ad284', diff --git a/src/modules/tiles/tiles.controller.ts b/src/modules/tiles/tiles.controller.ts index 9ce51e5..26caeca 100644 --- a/src/modules/tiles/tiles.controller.ts +++ b/src/modules/tiles/tiles.controller.ts @@ -19,7 +19,6 @@ import { promisify } from 'util'; import * as zlib from 'zlib'; import * as vtpbf from 'vt-pbf'; import { TilesService, TilesLayerEnum } from './tiles.service'; -import { CommuneStatusCacheService } from '../setting/commune-status-cache.service'; import { ReportStatusEnum } from '../../common/report-status.enum'; const gzip = promisify(zlib.gzip); @@ -27,10 +26,7 @@ const gzip = promisify(zlib.gzip); @Controller('tiles') @ApiTags('tiles') export class TilesController { - constructor( - private tilesService: TilesService, - private communeStatusCacheService: CommuneStatusCacheService, - ) {} + constructor(private tilesService: TilesService) {} @Get('/:z/:x/:y.pbf') @ApiOperation({ @@ -103,50 +99,4 @@ export class TilesController { }) .send(compressedPbf); } - - @Get('/commune-status/:z/:x/:y.pbf') - @ApiOperation({ - summary: 'Get vector tiles with commune status (enabled communes)', - operationId: 'getCommuneStatusTiles', - }) - @ApiParam({ name: 'z', required: true, type: String }) - @ApiParam({ name: 'x', required: true, type: String }) - @ApiParam({ name: 'y', required: true, type: String }) - @ApiResponse({ - status: HttpStatus.OK, - description: 'PBF vector tile with commune-status layer', - }) - @ApiResponse({ - status: HttpStatus.NO_CONTENT, - description: 'No data for this tile', - }) - async getCommuneStatusTiles( - @Param('z') z: string, - @Param('x') x: string, - @Param('y') y: string, - @Res() res: Response, - ) { - const tile = this.communeStatusCacheService.getTileForSource( - parseInt(z), - parseInt(x), - parseInt(y), - ); - - // console.log('Tile requested for sourceId:', sourceId, 'Tile found:', tile); - - if (!tile) { - return res.status(HttpStatus.NO_CONTENT).send(); - } - - const pbf = vtpbf.fromGeojsonVt({ 'commune-status': tile }); - - const compressedPbf = await gzip(Buffer.from(pbf)); - - return res - .set({ - 'Content-Type': 'application/x-protobuf', - 'Content-Encoding': 'gzip', - }) - .send(compressedPbf); - } } diff --git a/src/modules/tiles/tiles.module.ts b/src/modules/tiles/tiles.module.ts index 35de481..98b370a 100644 --- a/src/modules/tiles/tiles.module.ts +++ b/src/modules/tiles/tiles.module.ts @@ -4,14 +4,12 @@ import { TilesService } from './tiles.service'; import { AlertModule } from '../alert/alert.module'; import { SignalementModule } from '../signalement/signalement.module'; import { ReportModule } from '../report/report.module'; -import { SettingModule } from '../setting/setting.module'; @Module({ imports: [ forwardRef(() => AlertModule), forwardRef(() => SignalementModule), forwardRef(() => ReportModule), - forwardRef(() => SettingModule), ], controllers: [TilesController], providers: [TilesService], From 96f79b1118643ae6a3ba344962a1336e8e92fe57 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Tue, 7 Apr 2026 15:36:16 +0200 Subject: [PATCH 8/8] fix: remove hack --- src/modules/setting/setting.service.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/modules/setting/setting.service.ts b/src/modules/setting/setting.service.ts index 17ab0a7..85b175f 100644 --- a/src/modules/setting/setting.service.ts +++ b/src/modules/setting/setting.service.ts @@ -320,15 +320,6 @@ export class SettingService { const communeSettings = communeSettingsMap.get(codeCommune) || null; const revision = revisionsByCommune.get(codeCommune) || null; - // Dirty hack to test on staging while waiting for the balId to be added in the revision context on production. Should be removed once the balId is available in production revisions. - if (revision?.clientId === '651c1496142003dd4ba592ff') { - revision.context = { - extras: { - balId: '614b3385e1d1f2602d7ad284', - }, - }; - } - result.set( codeCommune, this.determineCommuneStatus(