From 8f9077a4cdd0a50b231f40792653bf0f046f5fab Mon Sep 17 00:00:00 2001 From: Vladyslav_Prykhodko Date: Wed, 1 Apr 2026 12:53:37 +0300 Subject: [PATCH] feat: add OpenFreeMap provider and vector tiles support for custom provider Add OpenFreeMap as a new built-in map provider with Bright, Positron, and Liberty styles. It becomes the default roadmap layer for new geo map widgets. No API key required. Extend the Custom provider with a "Vector tiles" toggle that allows using MapLibre GL style URLs from providers like OpenFreeMap or MapTiler, alongside the existing raster tile URL support. Add optional attribution field for custom providers. Fully backward compatible with existing widget configurations. --- .../components/widget/lib/maps/map-layer.ts | 46 ++++++++++++++++++- .../common/map/map-layer-row.component.html | 12 +++++ .../common/map/map-layer-row.component.ts | 12 +++++ .../map-layer-settings-panel.component.html | 31 ++++++++++++- .../map/map-layer-settings-panel.component.ts | 17 +++++++ .../shared/models/widget/maps/map.models.ts | 44 ++++++++++++++++-- .../assets/locale/locale.constant-en_US.json | 13 +++++- 7 files changed, 169 insertions(+), 6 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts index 2953c609ce..c45a3c0ca1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts @@ -20,12 +20,15 @@ import { defaultGoogleMapLayerSettings, defaultHereMapLayerSettings, defaultLayerTitle, + defaultOpenFreeMapLayerSettings, defaultOpenStreetMapLayerSettings, defaultTencentMapLayerSettings, GoogleMapLayerSettings, HereMapLayerSettings, MapLayerSettings, MapProvider, + OpenFreeMapLayerSettings, + OpenFreeMapStyleType, OpenStreetMapLayerSettings, ReferenceLayerType, TencentMapLayerSettings @@ -61,6 +64,8 @@ export abstract class TbMapLayer { inputSettings: DeepPartial) { switch (inputSettings.provider) { + case MapProvider.openfreemap: + return new TbOpenFreeMapLayer(ctx, inputSettings); case MapProvider.openstreet: return new TbOpenStreetMapLayer(ctx, inputSettings); case MapProvider.google: @@ -219,6 +224,32 @@ class TbOpenStreetMapLayer extends TbMapLayer { } +const openFreeMapStyleUrl = (style: OpenFreeMapStyleType): string => + `https://tiles.openfreemap.org/styles/${style}`; + +class TbOpenFreeMapLayer extends TbMapLayer { + + constructor(protected ctx: WidgetContext, + protected inputSettings: DeepPartial) { + super(ctx, inputSettings); + } + + protected defaultSettings(): OpenFreeMapLayerSettings { + return defaultOpenFreeMapLayerSettings; + } + + protected createLayer(): Observable { + const styleUrl = openFreeMapStyleUrl(this.settings.layerType); + const layer = L.TB.MapLibreGL.mapLibreGLLayer({ + style: styleUrl, + attributionControl: { + customAttribution: 'OpenFreeMap, © OpenMapTiles, Data from OpenStreetMap' + } + }); + return of(layer); + } +} + class TbGoogleMapLayer extends TbMapLayer { static loadedApiKeysGlobal: {[key: string]: boolean} = {}; @@ -318,7 +349,20 @@ class TbCustomMapLayer extends TbMapLayer { } protected createLayer(): Observable { - const layer = L.tileLayer(this.settings.tileUrl); + if (this.settings.vectorTiles) { + const options: any = { + style: this.settings.tileUrl + }; + if (this.settings.customAttribution) { + options.attributionControl = { + customAttribution: this.settings.customAttribution + }; + } + return of(L.TB.MapLibreGL.mapLibreGLLayer(options)); + } + const layer = L.tileLayer(this.settings.tileUrl, this.settings.customAttribution ? { + attribution: this.settings.customAttribution + } : undefined); return of(layer); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html index 4416ab2d6c..69d1abeab2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html @@ -28,6 +28,18 @@ } + @if (MapProvider.openfreemap === layerFormGroup.get('provider').value) { + + + @for (styleType of openFreeMapStyleTypes; track styleType) { + + {{ openFreeMapStyleTranslationMap.get(styleType) | translate }} + + } + + + } @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts index 2fdd892fa9..4c128d63f8 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts @@ -49,6 +49,8 @@ import { MapProvider, mapProviders, mapProviderTranslationMap, + openFreeMapStyleTranslationMap, + openFreeMapStyleTypes, openStreetLayerTypes, openStreetMapLayerTranslationMap, tencentLayerTranslationMap, @@ -81,6 +83,10 @@ export class MapLayerRowComponent implements ControlValueAccessor, OnInit { mapProviderTranslationMap = mapProviderTranslationMap; + openFreeMapStyleTypes = openFreeMapStyleTypes; + + openFreeMapStyleTranslationMap = openFreeMapStyleTranslationMap; + openStreetLayerTypes = openStreetLayerTypes; openStreetMapLayerTranslationMap = openStreetMapLayerTranslationMap; @@ -124,6 +130,8 @@ export class MapLayerRowComponent implements ControlValueAccessor, OnInit { provider: [null, [Validators.required]], layerType: [null, [Validators.required]], tileUrl: [null, [Validators.required]], + vectorTiles: [false, []], + customAttribution: [null, []], apiKey: [null, [Validators.required]], referenceLayer: [null, []] }); @@ -216,9 +224,13 @@ export class MapLayerRowComponent implements ControlValueAccessor, OnInit { const provider: MapProvider = this.layerFormGroup.get('provider').value; if (provider === MapProvider.custom) { this.layerFormGroup.get('tileUrl').enable({emitEvent: false}); + this.layerFormGroup.get('vectorTiles').enable({emitEvent: false}); + this.layerFormGroup.get('customAttribution').enable({emitEvent: false}); this.layerFormGroup.get('layerType').disable({emitEvent: false}); } else { this.layerFormGroup.get('tileUrl').disable({emitEvent: false}); + this.layerFormGroup.get('vectorTiles').disable({emitEvent: false}); + this.layerFormGroup.get('customAttribution').disable({emitEvent: false}); this.layerFormGroup.get('layerType').enable({emitEvent: false}); } if ([MapProvider.google, MapProvider.here].includes(provider)) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html index 28957d4b6a..6974e08150 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html @@ -36,8 +36,29 @@ + @if (MapProvider.custom === layerFormGroup.get('provider').value) { +
+ +
+ {{ 'widgets.maps.layer.provider.custom.vector-tiles' | translate }} +
+
+
+ }
-
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? 'widgets.maps.layer.provider.custom.tile-url' : 'widgets.maps.layer.layer') | translate }}
+
{{ (MapProvider.custom === layerFormGroup.get('provider').value ? (layerFormGroup.get('vectorTiles').value ? 'widgets.maps.layer.provider.custom.style-url' : 'widgets.maps.layer.provider.custom.tile-url') : 'widgets.maps.layer.layer') | translate }}
+ @if (MapProvider.openfreemap === layerFormGroup.get('provider').value) { + + + @for (styleType of openFreeMapStyleTypes; track styleType) { + + {{ openFreeMapStyleTranslationMap.get(styleType) | translate }} + + } + + + } @if (MapProvider.openstreet === layerFormGroup.get('provider').value) { @@ -117,6 +138,14 @@
+ @if (MapProvider.custom === layerFormGroup.get('provider').value) { +
+
widgets.maps.layer.provider.custom.attribution
+ + + +
+ }