Browse Source

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.
pull/15400/head
Vladyslav_Prykhodko 4 months ago
committed by Vladyslav Prykhodko
parent
commit
8f9077a4cd
  1. 46
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-layer.ts
  2. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html
  3. 12
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.ts
  4. 31
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html
  5. 17
      ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts
  6. 44
      ui-ngx/src/app/shared/models/widget/maps/map.models.ts
  7. 13
      ui-ngx/src/assets/locale/locale.constant-en_US.json

46
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<S extends MapLayerSettings> {
inputSettings: DeepPartial<MapLayerSettings>) {
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<OpenStreetMapLayerSettings> {
}
const openFreeMapStyleUrl = (style: OpenFreeMapStyleType): string =>
`https://tiles.openfreemap.org/styles/${style}`;
class TbOpenFreeMapLayer extends TbMapLayer<OpenFreeMapLayerSettings> {
constructor(protected ctx: WidgetContext,
protected inputSettings: DeepPartial<MapLayerSettings>) {
super(ctx, inputSettings);
}
protected defaultSettings(): OpenFreeMapLayerSettings {
return defaultOpenFreeMapLayerSettings;
}
protected createLayer(): Observable<L.Layer> {
const styleUrl = openFreeMapStyleUrl(this.settings.layerType);
const layer = L.TB.MapLibreGL.mapLibreGLLayer({
style: styleUrl,
attributionControl: {
customAttribution: 'OpenFreeMap, &copy; OpenMapTiles, Data from OpenStreetMap'
}
});
return of(layer);
}
}
class TbGoogleMapLayer extends TbMapLayer<GoogleMapLayerSettings> {
static loadedApiKeysGlobal: {[key: string]: boolean} = {};
@ -318,7 +349,20 @@ class TbCustomMapLayer extends TbMapLayer<CustomMapLayerSettings> {
}
protected createLayer(): Observable<L.Layer> {
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);
}

12
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-row.component.html

@ -28,6 +28,18 @@
}
</mat-select>
</mat-form-field>
@if (MapProvider.openfreemap === layerFormGroup.get('provider').value) {
<mat-form-field
class="tb-inline-field tb-layer-field" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="layerType" required>
@for (styleType of openFreeMapStyleTypes; track styleType) {
<mat-option [value]="styleType">
{{ openFreeMapStyleTranslationMap.get(styleType) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (MapProvider.openstreet === layerFormGroup.get('provider').value) {
<mat-form-field
class="tb-inline-field tb-layer-field" appearance="outline" subscriptSizing="dynamic">

12
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)) {

31
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.html

@ -36,8 +36,29 @@
</mat-select>
</mat-form-field>
</div>
@if (MapProvider.custom === layerFormGroup.get('provider').value) {
<div class="tb-form-row space-between column-xs">
<mat-slide-toggle class="mat-slide" formControlName="vectorTiles">
<div tb-hint-tooltip-icon="{{ 'widgets.maps.layer.provider.custom.vector-tiles-hint' | translate }}">
{{ 'widgets.maps.layer.provider.custom.vector-tiles' | translate }}
</div>
</mat-slide-toggle>
</div>
}
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width"> {{ (MapProvider.custom === layerFormGroup.get('provider').value ? 'widgets.maps.layer.provider.custom.tile-url' : 'widgets.maps.layer.layer') | translate }}</div>
<div class="fixed-title-width"> {{ (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 }}</div>
@if (MapProvider.openfreemap === layerFormGroup.get('provider').value) {
<mat-form-field
class="flex" appearance="outline" subscriptSizing="dynamic">
<mat-select formControlName="layerType" required>
@for (styleType of openFreeMapStyleTypes; track styleType) {
<mat-option [value]="styleType">
{{ openFreeMapStyleTranslationMap.get(styleType) | translate }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
@if (MapProvider.openstreet === layerFormGroup.get('provider').value) {
<mat-form-field
class="flex" appearance="outline" subscriptSizing="dynamic">
@ -117,6 +138,14 @@
</mat-select>
</mat-form-field>
</div>
@if (MapProvider.custom === layerFormGroup.get('provider').value) {
<div class="tb-form-row space-between column-xs">
<div class="fixed-title-width" tb-hint-tooltip-icon="{{ 'widgets.maps.layer.provider.custom.attribution-hint' | translate }}" translate>widgets.maps.layer.provider.custom.attribution</div>
<mat-form-field class="flex" appearance="outline" subscriptSizing="dynamic">
<input matInput formControlName="customAttribution" placeholder="{{ 'widget-config.set' | translate }}">
</mat-form-field>
</div>
}
</div>
<div class="tb-map-layer-settings-panel-buttons">
<button mat-button

17
ui-ngx/src/app/modules/home/components/widget/lib/settings/common/map/map-layer-settings-panel.component.ts

@ -29,6 +29,8 @@ import {
MapProvider,
mapProviders,
mapProviderTranslationMap,
openFreeMapStyleTranslationMap,
openFreeMapStyleTypes,
openStreetLayerTypes,
openStreetMapLayerTranslationMap, referenceLayerTypes, referenceLayerTypeTranslationMap,
tencentLayerTranslationMap,
@ -52,6 +54,10 @@ export class MapLayerSettingsPanelComponent implements OnInit {
mapProviderTranslationMap = mapProviderTranslationMap;
openFreeMapStyleTypes = openFreeMapStyleTypes;
openFreeMapStyleTranslationMap = openFreeMapStyleTranslationMap;
openStreetLayerTypes = openStreetLayerTypes;
openStreetMapLayerTranslationMap = openStreetMapLayerTranslationMap;
@ -95,6 +101,8 @@ export class MapLayerSettingsPanelComponent implements OnInit {
provider: [null, [Validators.required]],
layerType: [null, [Validators.required]],
tileUrl: [null, [Validators.required]],
vectorTiles: [false, []],
customAttribution: [null, []],
apiKey: [null, [Validators.required]],
referenceLayer: [null, []]
}
@ -107,6 +115,11 @@ export class MapLayerSettingsPanelComponent implements OnInit {
).subscribe((newProvider: MapProvider) => {
this.onProviderChanged(newProvider);
});
this.layerFormGroup.get('vectorTiles').valueChanges.pipe(
takeUntilDestroyed(this.destroyRef)
).subscribe(() => {
this.updateValidators();
});
this.updateValidators();
}
@ -140,9 +153,13 @@ export class MapLayerSettingsPanelComponent implements 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)) {

44
ui-ngx/src/app/shared/models/widget/maps/map.models.ts

@ -929,6 +929,7 @@ export const defaultMapActionButtonSettings: MapActionButtonSettings = {
}
export enum MapProvider {
openfreemap = 'openfreemap',
openstreet = 'openstreet',
google = 'google',
here = 'here',
@ -940,6 +941,7 @@ export const mapProviders = Object.keys(MapProvider) as MapProvider[];
export const mapProviderTranslationMap = new Map<MapProvider, string>(
[
[MapProvider.openfreemap, 'widgets.maps.layer.provider.openfreemap.title'],
[MapProvider.openstreet, 'widgets.maps.layer.provider.openstreet.title'],
[MapProvider.google, 'widgets.maps.layer.provider.google.title'],
[MapProvider.here, 'widgets.maps.layer.provider.here.title'],
@ -975,6 +977,9 @@ export const mapLayerValid = (layer: MapLayerSettings): boolean => {
return false;
}
switch (layer.provider) {
case MapProvider.openfreemap:
const openFreeMapLayer = layer as OpenFreeMapLayerSettings;
return !!openFreeMapLayer.layerType;
case MapProvider.openstreet:
const openStreetLayer = layer as OpenStreetMapLayerSettings;
return !!openStreetLayer.layerType;
@ -1008,6 +1013,9 @@ export const defaultLayerTitle = (layer: MapLayerSettings): string => {
return null;
}
switch (layer.provider) {
case MapProvider.openfreemap:
const ofmLayer = layer as OpenFreeMapLayerSettings;
return openFreeMapStyleTranslationMap.get(ofmLayer.layerType);
case MapProvider.openstreet:
const openStreetLayer = layer as OpenStreetMapLayerSettings;
return openStreetMapLayerTranslationMap.get(openStreetLayer.layerType);
@ -1059,6 +1067,32 @@ export const defaultOpenStreetMapLayerSettings: OpenStreetMapLayerSettings = {
layerType: OpenStreetLayerType.openStreetMapnik
}
export enum OpenFreeMapStyleType {
bright = 'bright',
positron = 'positron',
liberty = 'liberty'
}
export const openFreeMapStyleTypes = Object.values(OpenFreeMapStyleType) as OpenFreeMapStyleType[];
export const openFreeMapStyleTranslationMap = new Map<OpenFreeMapStyleType, string>(
[
[OpenFreeMapStyleType.liberty, 'widgets.maps.layer.provider.openfreemap.liberty'],
[OpenFreeMapStyleType.bright, 'widgets.maps.layer.provider.openfreemap.bright'],
[OpenFreeMapStyleType.positron, 'widgets.maps.layer.provider.openfreemap.positron']
]
);
export interface OpenFreeMapLayerSettings extends MapLayerSettings {
provider: MapProvider.openfreemap;
layerType: OpenFreeMapStyleType;
}
export const defaultOpenFreeMapLayerSettings: OpenFreeMapLayerSettings = {
provider: MapProvider.openfreemap,
layerType: OpenFreeMapStyleType.bright
}
export enum GoogleLayerType {
roadmap = 'roadmap',
satellite = 'satellite',
@ -1148,6 +1182,8 @@ export const defaultTencentMapLayerSettings: TencentMapLayerSettings = {
export interface CustomMapLayerSettings extends MapLayerSettings {
provider: MapProvider.custom;
tileUrl: string;
vectorTiles?: boolean;
customAttribution?: string;
}
export const defaultCustomMapLayerSettings: CustomMapLayerSettings = {
@ -1157,6 +1193,8 @@ export const defaultCustomMapLayerSettings: CustomMapLayerSettings = {
export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings => {
switch (provider) {
case MapProvider.openfreemap:
return defaultOpenFreeMapLayerSettings;
case MapProvider.openstreet:
return defaultOpenStreetMapLayerSettings;
case MapProvider.google:
@ -1173,9 +1211,9 @@ export const defaultMapLayerSettings = (provider: MapProvider): MapLayerSettings
export const defaultMapLayers: MapLayerSettings[] = [
{
label: '{i18n:widgets.maps.layer.roadmap}',
provider: MapProvider.openstreet,
layerType: OpenStreetLayerType.openStreetMapnik,
} as OpenStreetMapLayerSettings,
provider: MapProvider.openfreemap,
layerType: OpenFreeMapStyleType.bright,
} as OpenFreeMapLayerSettings,
{
label: '{i18n:widgets.maps.layer.satellite}',
provider: MapProvider.openstreet,

13
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -8929,7 +8929,18 @@
},
"custom": {
"title": "Custom",
"tile-url": "Tile URL"
"tile-url": "Tile URL",
"style-url": "Style URL",
"vector-tiles": "Vector tiles",
"vector-tiles-hint": "Enable when using a style URL from providers like OpenFreeMap or MapTiler.",
"attribution": "Copyright text",
"attribution-hint": "Displayed at the bottom of the map. Required by most tile providers."
},
"openfreemap": {
"title": "OpenFreeMap",
"liberty": "Liberty",
"bright": "Bright",
"positron": "Positron"
}
},
"credentials": {

Loading…
Cancel
Save