diff --git a/src/app/modules/map-config/services/map-import/map-import.service.ts b/src/app/modules/map-config/services/map-import/map-import.service.ts index a995edf6..3d4fe2ea 100644 --- a/src/app/modules/map-config/services/map-import/map-import.service.ts +++ b/src/app/modules/map-config/services/map-import/map-import.service.ts @@ -242,7 +242,9 @@ export class MapImportService { } else { propertySelectorValues.propertySource = PROPERTY_SELECTOR_SOURCE.interpolated; - const getValue = (inputValues[2] as Array)[1]; + const interpolatedColumn = (inputValues[2] as Array); + // cause interpolated column could contain only one value, ex: [zoom] + const getValue = (interpolatedColumn.length > 1) ? interpolatedColumn[1] : interpolatedColumn[0]; if (getValue.startsWith('count')) { propertySelectorValues.propertyInterpolatedFg = { propertyInterpolatedCountOrMetricCtrl: COUNT_OR_METRIC.COUNT, @@ -305,7 +307,7 @@ export class MapImportService { } } const min = inputValues[4]; - const max = inputValues.pop(); + const max = inputValues[inputValues.length - 1]; propertySelectorValues.propertyInterpolatedFg.propertyInterpolatedMinValueCtrl = min === 0 ? '0' : min; propertySelectorValues.propertyInterpolatedFg.propertyInterpolatedMaxValueCtrl = max === 0 ? '0' : max; } @@ -536,6 +538,7 @@ export class MapImportService { (manualValues || []).forEach(kc => typeFg.colorFg.addToColorManualValuesCtrl(kc)); + return layerFg; } @@ -550,7 +553,6 @@ export class MapImportService { values.visibilityStep.featuresMax = layerSource.maxfeatures; values.styleStep.geometryType = layer.type === 'symbol' ? 'label' : layer.type; values.styleStep.filter = layer.filter; - } public static importLayerFeaturesMetric( @@ -597,9 +599,13 @@ export class MapImportService { values.geometryStep.rawGeometry = isGeometryTypeRaw ? layerSource.raw_geometry.geometry : null; values.geometryStep.clusterSort = isGeometryTypeRaw ? layerSource.raw_geometry.sort : null; values.visibilityStep.featuresMin = layerSource.minfeatures; - values.styleStep.geometryType = layer.type === 'symbol' ? 'label' : layer.type; + // to display the correct geom type when editing a circle-heat layer + if(layer.metadata.hiddenProps && layer.metadata.hiddenProps.geomType === GEOMETRY_TYPE.circleHeat) { + values.styleStep.geometryType = GEOMETRY_TYPE.circleHeat; + } else { + values.styleStep.geometryType = layer.type === 'symbol' ? 'label' : layer.type; + } values.styleStep.filter = layer.filter; - } public doImport(config: Config, mapConfig: MapConfig) { diff --git a/src/app/modules/map-config/services/map-layer-form-builder/map-layer-form-builder.service.ts b/src/app/modules/map-config/services/map-layer-form-builder/map-layer-form-builder.service.ts index 1bdf3d27..71f969d6 100644 --- a/src/app/modules/map-config/services/map-layer-form-builder/map-layer-form-builder.service.ts +++ b/src/app/modules/map-config/services/map-layer-form-builder/map-layer-form-builder.service.ts @@ -696,8 +696,15 @@ export class MapLayerAllTypesFormGroup extends ConfigFormGroup { ) { super({ geometryStep: new ConfigFormGroup({ - ...geometryFormControls - }).withStepName(marker('Geometry')), + ...geometryFormControls, + }).withStepName(marker('Geometry')) + .withDependsOn(() => [this.geometryType]) + .withOnDependencyChange( + configForm => { + if(this.geometryType.value === GEOMETRY_TYPE.circleHeat) { + configForm.get('aggregatedGeometry').setValue(AGGREGATE_GEOMETRY_TYPE.cell_center); + } + }), styleStep: new ConfigFormGroup({ ...styleFormControls, geometryType: new SelectFormControl( @@ -1438,6 +1445,7 @@ export class MapLayerTypeClusterFormGroup extends MapLayerAllTypesFormGroup { GEOMETRY_TYPE.fill, GEOMETRY_TYPE.circle, GEOMETRY_TYPE.heatmap, + GEOMETRY_TYPE.circleHeat, GEOMETRY_TYPE.label ], propertySelectorFormBuilder, diff --git a/src/app/modules/map-config/services/map-layer-form-builder/models.ts b/src/app/modules/map-config/services/map-layer-form-builder/models.ts index 4f58db89..000ef7b4 100644 --- a/src/app/modules/map-config/services/map-layer-form-builder/models.ts +++ b/src/app/modules/map-config/services/map-layer-form-builder/models.ts @@ -20,6 +20,7 @@ export enum GEOMETRY_TYPE { fill = 'fill', line = 'line', circle = 'circle', + circleHeat = 'circle-heatmap', heatmap = 'heatmap', label = 'label' } diff --git a/src/app/services/main-form-manager/config-map-export-helper.ts b/src/app/services/main-form-manager/config-map-export-helper.ts index 3dd220d8..f2f5bf09 100644 --- a/src/app/services/main-form-manager/config-map-export-helper.ts +++ b/src/app/services/main-form-manager/config-map-export-helper.ts @@ -35,13 +35,16 @@ import { Layer, Layout, MapConfig, - Paint, + Paint, PaintValue, SELECT_LAYER_PREFIX } from './models-map-config'; +import {InterpolatedProperty, ModesValues} from '@shared/interfaces/config-map.interfaces'; +import {CIRCLE_HEATMAP_RADIUS_GRANULARITY} from '@shared-models/circle-heat-map-radius-granularity'; export enum VISIBILITY { visible = 'visible', none = 'none' } + export const NORMALIZED = 'normalized'; export class ConfigMapExportHelper { @@ -166,6 +169,17 @@ export class ConfigMapExportHelper { collection, collectionDisplayName }; + + // with this prop we ll be able to restore the good geomType when we ll reload the layer; + if (metadata.hasOwnProperty('hiddenProps')) { + delete metadata['hiddenProps']; + } + + if(modeValues.styleStep.geometryType === GEOMETRY_TYPE.circleHeat) { + metadata.hiddenProps = {geomType: GEOMETRY_TYPE.circleHeat}; + } + + if (modeValues.styleStep.geometryType === GEOMETRY_TYPE.fill.toString()) { const fillStroke: FillStroke = { color: this.getMapProperty(modeValues.styleStep.strokeColorFg, mode, colorService, taggableFields), @@ -189,7 +203,7 @@ export class ConfigMapExportHelper { const layerSource: LayerSourceConfig = ConfigExportHelper.getLayerSourceConfig(layerFg); const layer: Layer = { id: layerFg.value.arlasId, - type: modeValues.styleStep.geometryType === 'label' ? 'symbol' : modeValues.styleStep.geometryType, + type: this.getLayerType(modeValues.styleStep.geometryType), source: layerSource.source, minzoom: modeValues.visibilityStep.zoomMin, maxzoom: modeValues.visibilityStep.zoomMax, @@ -231,6 +245,19 @@ export class ConfigMapExportHelper { return layer; } + /** + * set the correct layer type before we save it. + * @param geometryType + */ + public static getLayerType(geometryType: GEOMETRY_TYPE): GEOMETRY_TYPE | string { + /** we change the type of circle heat map to keep the compatibility with mapbox **/ + if(geometryType === GEOMETRY_TYPE.circleHeat){ + return GEOMETRY_TYPE.circle; + } + + return geometryType === 'label' ? 'symbol' : geometryType; + } + public static getLayerPaint(modeValues, mode, colorService: ArlasColorService, taggableFields?: Set) { const paint: Paint = {}; const color = this.getMapProperty(modeValues.styleStep.colorFg, mode, colorService, taggableFields); @@ -281,6 +308,15 @@ export class ConfigMapExportHelper { break; } + case GEOMETRY_TYPE.circleHeat: { + paint['circle-stroke-opacity'] = 0; + paint['circle-stroke-color'] = color; + paint['circle-opacity'] = opacity; + paint['circle-color'] = color; + paint['circle-radius'] = this.getRadPropFromGranularity(modeValues as ModesValues); + paint['circle-blur'] = 1; + break; + } } return paint; } @@ -304,6 +340,10 @@ export class ConfigMapExportHelper { layout['symbol-placement'] = modeValues.styleStep.labelPlacementCtrl; break; } + case GEOMETRY_TYPE.circleHeat: { + layout['circle-sort-key'] = this.getCircleHeatMapSortKey(modeValues.styleStep.colorFg, mode); + break; + } } return layout; } @@ -429,41 +469,9 @@ export class ConfigMapExportHelper { } case PROPERTY_SELECTOR_SOURCE.interpolated: { const interpolatedValues = fgValues.propertyInterpolatedFg; - let interpolatedColor: Array>; - const getField = () => - (interpolatedValues.propertyInterpolatedCountOrMetricCtrl === 'metric') - ? interpolatedValues.propertyInterpolatedFieldCtrl + '_' + - (interpolatedValues.propertyInterpolatedMetricCtrl as string).toLowerCase() + '_' : - interpolatedValues.propertyInterpolatedFieldCtrl; - - if (mode !== LAYER_MODE.features && interpolatedValues.propertyInterpolatedCountOrMetricCtrl === 'count') { - // for types FEATURE-METRIC and CLUSTER, if we interpolate by count - interpolatedColor = [ - 'interpolate', - ['linear'], - ['get', 'count' + (!!interpolatedValues.propertyInterpolatedCountNormalizeCtrl ? `_:${NORMALIZED}` : '')] - ]; - } else if (interpolatedValues.propertyInterpolatedNormalizeCtrl) { - // otherwise if we normalize - interpolatedColor = [ - 'interpolate', - ['linear'], - this.getArray( - getField() - .concat(':' + NORMALIZED) - .concat(interpolatedValues.propertyInterpolatedNormalizeByKeyCtrl ? - ':' + interpolatedValues.propertyInterpolatedNormalizeLocalFieldCtrl : '')) - ]; - } else { - // if we don't normalize - interpolatedColor = [ - 'interpolate', - ['linear'], - this.getArray(getField()) - ]; - } - return interpolatedColor.concat((interpolatedValues.propertyInterpolatedValuesCtrl as Array) - .flatMap(pc => [pc.proportion, pc.value])); + const values = (interpolatedValues.propertyInterpolatedValuesCtrl as Array) + .flatMap(pc => [pc.proportion, pc.value]); + return this.buildPropsValuesFromInterpolatedValues(interpolatedValues, mode, values); } case PROPERTY_SELECTOR_SOURCE.heatmap_density: { const interpolatedValues = fgValues.propertyInterpolatedFg; @@ -484,6 +492,95 @@ export class ConfigMapExportHelper { } } + /** + * Build the correct array from interpolated values to obtain an array that respects the MapBox expression format + * https://docs.mapbox.com/style-spec/reference/expressions/ + * @param interpolatedValues interpolated properties that determine the type of array we build ( count, normalize ) + * @param mode + * @param valuesToInsert the value to insert at the end of the array + * @private + */ + private static buildPropsValuesFromInterpolatedValues(interpolatedValues: InterpolatedProperty, + mode: LAYER_MODE, + valuesToInsert?: (string | number)[]){ + let interpolatedColor: Array>; + const getField = () => + (interpolatedValues.propertyInterpolatedCountOrMetricCtrl === 'metric') + ? interpolatedValues.propertyInterpolatedFieldCtrl + '_' + + (interpolatedValues.propertyInterpolatedMetricCtrl as string).toLowerCase() + '_' : + interpolatedValues.propertyInterpolatedFieldCtrl; + + if (mode !== LAYER_MODE.features && interpolatedValues.propertyInterpolatedCountOrMetricCtrl === 'count') { + // for types FEATURE-METRIC and CLUSTER, if we interpolate by count + interpolatedColor = [ + 'interpolate', + ['linear'], + ['get', 'count' + (!!interpolatedValues.propertyInterpolatedCountNormalizeCtrl ? `_:${NORMALIZED}` : '')] + ]; + } else if (interpolatedValues.propertyInterpolatedNormalizeCtrl) { + // otherwise if we normalize + interpolatedColor = [ + 'interpolate', + ['linear'], + this.getArray( + getField() + .concat(':' + NORMALIZED) + .concat(interpolatedValues.propertyInterpolatedNormalizeByKeyCtrl ? + ':' + interpolatedValues.propertyInterpolatedNormalizeLocalFieldCtrl : '')) + ]; + } else { + // if we don't normalize + interpolatedColor = [ + 'interpolate', + ['linear'], + this.getArray(getField()) + ]; + } + + if (valuesToInsert) { + return interpolatedColor.concat(valuesToInsert); + } + + return interpolatedColor; + } + + /** + * Method used to construct the key props. + * based on color props. + */ + public static getCircleHeatMapSortKey(fgValues: any, mode: LAYER_MODE): PaintValue { + switch (fgValues.propertySource) { + case PROPERTY_SELECTOR_SOURCE.fix_color: + return 1; + case PROPERTY_SELECTOR_SOURCE.interpolated: + const interpolatedValues = fgValues.propertyInterpolatedFg as InterpolatedProperty; + const minValue = interpolatedValues.propertyInterpolatedValuesCtrl[0].proportion; + const maxValue = interpolatedValues + .propertyInterpolatedValuesCtrl[interpolatedValues.propertyInterpolatedValuesCtrl.length - 1] + .proportion; + // those values ([minValue, 0, maxValue, 8]) don't have a special meaning and made to guarantee the interpolation of the circle sort key + return this.buildPropsValuesFromInterpolatedValues(interpolatedValues, mode, [minValue, 0, maxValue, 8]); + } + } + + /** + * set default Radius according to granularity. WARNING : only for circle-heatMap + */ + public static getRadPropFromGranularity(modeValues: ModesValues): PaintValue { + const granularity = modeValues.geometryStep.granularity?.toLowerCase(); + const aggType = modeValues.geometryStep.aggType?.toLowerCase(); + const radiusSteps = CIRCLE_HEATMAP_RADIUS_GRANULARITY[aggType][granularity] || []; + return [ + 'interpolate', + [ + 'linear' + ], + ['zoom'], + ...radiusSteps + ]; + } + + public static getFieldPath(field: string, taggableFields: Set): string { return (taggableFields && taggableFields.has(field)) ? field + '.0' : field; } @@ -557,4 +654,5 @@ export class ConfigMapExportHelper { return value as number + nbPixel; } } + } diff --git a/src/app/services/main-form-manager/models-map-config.ts b/src/app/services/main-form-manager/models-map-config.ts index 17c1a715..5eb16aa4 100644 --- a/src/app/services/main-form-manager/models-map-config.ts +++ b/src/app/services/main-form-manager/models-map-config.ts @@ -56,9 +56,10 @@ export interface Layout { 'text-allow-overlap'?: boolean; 'text-anchor'?: string; 'symbol-placement'?: string; + 'circle-sort-key'?: PaintValue; } -type PaintValue = Array | number> | PaintColor | string | number; +export type PaintValue = Array | number> | PaintColor | string | number; export interface Paint { 'fill-color'?: PaintValue; 'fill-opacity'?: number; @@ -69,6 +70,7 @@ export interface Paint { 'line-width'?: PaintValue; 'line-dasharray'?: PaintValue; 'circle-radius'?: PaintValue; + 'circle-blur'?: number; 'circle-stroke-width'?: PaintValue; 'heatmap-color'?: PaintValue; 'heatmap-radius'?: PaintValue; diff --git a/src/app/shared/components/config-form-control/config-form-control.component.html b/src/app/shared/components/config-form-control/config-form-control.component.html index c47438e1..308ebb62 100644 --- a/src/app/shared/components/config-form-control/config-form-control.component.html +++ b/src/app/shared/components/config-form-control/config-form-control.component.html @@ -63,7 +63,7 @@
{{opt.value | translate}} {{opt.detail | translate }} diff --git a/src/app/shared/interfaces/config-map.interfaces.ts b/src/app/shared/interfaces/config-map.interfaces.ts new file mode 100644 index 00000000..50df0529 --- /dev/null +++ b/src/app/shared/interfaces/config-map.interfaces.ts @@ -0,0 +1,93 @@ +/* +Licensed to Gisaïa under one or more contributor +license agreements. See the NOTICE.txt file distributed with +this work for additional information regarding copyright +ownership. Gisaïa licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +interface InterpolatedValue { + proportion: number; +} + +export interface InterpolatedValueNumber extends InterpolatedValue { + value: number; +} + +export interface InterpolatedValueString extends InterpolatedValue { + value: string; +} + +export interface Propriety { + propertySource: string; + propertyFixColor?: string; + propertyFixSlider?: string; +} + +export interface InterpolatedProperty { + propertyInterpolatedCountOrMetricCtrl?: string; + propertyInterpolatedCountNormalizeCtrl?: boolean; + propertyInterpolatedFieldCtrl?: string; + propertyInterpolatedNormalizeCtrl?: boolean; + propertyInterpolatedNormalizeByKeyCtrl?: string; + propertyInterpolatedNormalizeLocalFieldCtrl?: string; + propertyInterpolatedMinFieldValueCtrl?: number; + propertyInterpolatedMaxFieldValueCtrl?: number; + propertyInterpolatedMetricCtrl?: string; + propertyInterpolatedValuesCtrl?: interpolatedValues[]; + propertyInterpolatedMinValueCtrl?: number; + propertyInterpolatedMaxValueCtrl?: number; + propertyInterpolatedValuesButton?: string; + propertyInterpolatedValuesPreview?: interpolatedValues[]; +} + +export interface VisibilityStep { + visible: boolean; + zoomMin: number; + zoomMax: number; + featuresMin: number; +} + +export interface GeometryStep { + aggGeometry: string; + aggType: string; + granularity: string; + clusterGeometryType: string; + aggregatedGeometry: string; +} + +export interface StyleStep { + geometryType: string; + filter?: []; + opacity?: { + propertySource: string; + propertyInterpolatedFg?: InterpolatedProperty | any; + }; + colorFg?: { + propertySource: string; + propertyInterpolatedFg?: InterpolatedProperty | any; + }; + radiusFg?: Propriety; + strokeColorFg?: Propriety; + strokeWidthFg?: Propriety; + strokeOpacityFg?: Propriety; +} + +type interpolatedValues = InterpolatedValueString | InterpolatedValueNumber; + +// TODO: when we ll be sure of the object structure remove the any. +export interface ModesValues { + geometryStep: GeometryStep | any; + styleStep: StyleStep | any; + visibilityStep: VisibilityStep; +} diff --git a/src/app/shared/models/circle-heat-map-radius-granularity.ts b/src/app/shared/models/circle-heat-map-radius-granularity.ts new file mode 100644 index 00000000..5af8112f --- /dev/null +++ b/src/app/shared/models/circle-heat-map-radius-granularity.ts @@ -0,0 +1,86 @@ +/* +Licensed to Gisaïa under one or more contributor +license agreements. See the NOTICE.txt file distributed with +this work for additional information regarding copyright +ownership. Gisaïa licenses this file to you under +the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, +software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, either express or implied. See the License for the +specific language governing permissions and limitations +under the License. +*/ +export const CIRCLE_HEATMAP_RADIUS_GRANULARITY = { + geohash: { + finest: [ + 0, 9, + 2.9999999999999, 25, + 3, 9, + 4.9999999999999, 12, + 5, 4, + 7.9999999999999, 20, + 8, 10, + 10.9999999999999, 25, + 11, 60, + 13.9999999999999, 200, + 14, 200, + 16.9999999999999, 1000, + 17, 1000, + 19.9999999999999, 1500, + 20, 2000 + ], + fine: [ + 0, 10, + 1.999, 20, + 4.999999, 60, + 5, 30, + 7.999999, 120, + 8, 40, + 10.499999, 180, + 10.5, 30, + 13.999999, 210, + 14, 300, + 16.999999, 1000, + 17, 2000 + ], + medium: [ + 0, 10, + 2.999999, 90, + 3, 30, + 5.999999, 130, + 6, 40, + 8.999999, 200, + 9, 50, + 11.999999, 360, + 12, 70, + 14.999999, 460, + 15, 600, + 17, 2000 + ], + coarse: [ + 0, 30, + 3.999999, 250, + 4, 50, + 6.999999, 300, + 7, 120, + 9.999999, 480, + 10, 120, + 12.999999, 580, + 13, 160, + 14, 600, + 17, 1000 + ], + }, + tile: { + finest: [0,9, 23,15], + fine: [0, 35, 23,45], + medium: [0, 50, 23, 100], + coarse: [0, 80, 23,150] + } +};