diff --git a/.idea/jsLinters/eslint.xml b/.idea/jsLinters/eslint.xml new file mode 100644 index 0000000..541945b --- /dev/null +++ b/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/src/badTypes.ts b/src/badTypes.ts deleted file mode 100644 index 01124b1..0000000 --- a/src/badTypes.ts +++ /dev/null @@ -1,43 +0,0 @@ -import {WellKnownName} from 'geostyler-style'; - -export type MarkerPlacement = Record; -export type Stroke = Record; -export type Fill = Record; - -export interface Options { - [key: string]: any; - toLowerCase?: boolean; -} - -export interface Rule { - name: string; - symbolizers?: Symbolizer[]; - scaleDenominator?: any; - filter?: any[] | any; -} - -export interface Symbolizer { - kind?: string; - anchor?: string; - rotate?: any; - color?: any; - font?: string; - label?: string | string[]; - size?: number; - weight?: any; - perpendicularOffset?: number; - offset?: number[]; - anchorPointX?: number; - anchorPointY?: number; - haloColor?: any; - haloSize?: number; - haloOpacity?: number; - group?: boolean; - wellKnownName?: WellKnownName; - opacity?: number; - graphicStroke?: any; - graphicStrokeInterval?: any; - graphicStrokeOffset?: any; - graphicFill?: any; - graphicFillMargin?: any; -} diff --git a/src/esri/types/labeling/CIMSymbolReference.ts b/src/esri/types/labeling/CIMSymbolReference.ts index 6858644..298e47d 100644 --- a/src/esri/types/labeling/CIMSymbolReference.ts +++ b/src/esri/types/labeling/CIMSymbolReference.ts @@ -3,11 +3,19 @@ import { CIMSymbol } from '../symbols/index.ts'; type CIMPrimitiveOverride = {}; type CIMScaleDependentSizeVariation = {}; + +export type Geometry = { + rings?: number[][][]; + paths?: number[][][]; + curveRings?: { a?: number[][]; c?: number[][] }[][]; +}; + /** * Represents a symbol reference. * */ export type CIMSymbolReference = { + geometry: Geometry; /** * Gets or sets the primitive overrides. Typically set by renderers at draw time. */ diff --git a/src/esri/types/layers/CIMFeatureLayer.ts b/src/esri/types/layers/CIMFeatureLayer.ts index 4d89967..f38e86c 100644 --- a/src/esri/types/layers/CIMFeatureLayer.ts +++ b/src/esri/types/layers/CIMFeatureLayer.ts @@ -1,8 +1,8 @@ import { CIMLayerAction } from '../CIMLayerAction.ts'; import { CIMLayerDefinition } from './CIMLayerDefinition.ts'; -import { CIMRenderer } from '../renderers/CIMRenderer.ts'; import { CIMLabelClass } from '../labeling/CIMLabelClass.ts'; import { CIMSymbolReference } from '../labeling/CIMSymbolReference.ts'; +import {CIMRenderer} from '../renderers'; type CIMDataConnection = {}; type CIMSymbolLayerMasking = {}; diff --git a/src/esri/types/renderers/CIMBreaksRenderer.ts b/src/esri/types/renderers/CIMBreaksRenderer.ts new file mode 100644 index 0000000..3dfa458 --- /dev/null +++ b/src/esri/types/renderers/CIMBreaksRenderer.ts @@ -0,0 +1,17 @@ +import {CIMRenderer, Group } from './CIMRenderer.ts'; +import {CIMSymbolReference} from '../labeling'; + +export type CIMBreaksRenderer = CIMRenderer & { + classBreakType: string; + defaultSymbol?: CIMSymbolReference; + field: string; + groups?: Group[]; + showInAscendingOrder: boolean; + breaks: { + type: string; + fieldValues: string[]; + label: string; + symbol: CIMSymbolReference; + upperBound: number; + }[]; +}; diff --git a/src/esri/types/renderers/CIMRenderer.ts b/src/esri/types/renderers/CIMRenderer.ts index 924782b..0d1709c 100644 --- a/src/esri/types/renderers/CIMRenderer.ts +++ b/src/esri/types/renderers/CIMRenderer.ts @@ -1,12 +1,38 @@ -import { CIMObject } from '../CIMObject.ts'; +import {CIMObject} from '../CIMObject.ts'; +import {CIMSymbolReference} from '../labeling'; -type Group = {}; -type SymbolReference = {}; + +export type Class = { + alternateSymbols: CIMSymbolReference[]; + label: string; + filter: string; + symbol: CIMSymbolReference; + minValue?: number; + maxValue?: number; + breakCount?: number; + breakValues?: number[]; + breakLabels?: string[]; + breakSymbols?: CIMSymbolReference[]; + values: { + type: string; + fieldValues: string[]; + }[]; +}; + +export type Group = { + classes: Class[]; +}; + +export type VisualVariable = CIMObject & { + rotationTypeZ: string; + visualVariableInfoZ: { + expression: string; + valueExpressionInfo: { + expression: string; + }; + }; +}; export type CIMRenderer = CIMObject & { - type: string; - fields?: string[]; - groups?: Group[]; - defaultSymbol?: SymbolReference; - classBreakType?: string; + visualVariables?: VisualVariable[]; }; diff --git a/src/esri/types/renderers/CIMSimpleRenderer.ts b/src/esri/types/renderers/CIMSimpleRenderer.ts new file mode 100644 index 0000000..608c135 --- /dev/null +++ b/src/esri/types/renderers/CIMSimpleRenderer.ts @@ -0,0 +1,7 @@ +import {CIMRenderer} from './CIMRenderer.ts'; +import {CIMSymbolReference} from '../labeling'; + +export type CIMSimpleRenderer = CIMRenderer & { + label: string; + symbol: CIMSymbolReference; +}; diff --git a/src/esri/types/renderers/CIMUniqueValueRenderer.ts b/src/esri/types/renderers/CIMUniqueValueRenderer.ts index b0c9eb4..b6d35be 100644 --- a/src/esri/types/renderers/CIMUniqueValueRenderer.ts +++ b/src/esri/types/renderers/CIMUniqueValueRenderer.ts @@ -1,4 +1,8 @@ +import {CIMRenderer, Group} from './CIMRenderer.ts'; +import {CIMSymbolReference} from '../labeling'; -export type CIMUniqueValueRenderer = { - +export type CIMUniqueValueRenderer = CIMRenderer & { + defaultSymbol?: CIMSymbolReference; + fields?: string[]; + groups?: Group[]; }; diff --git a/src/esri/types/symbols/CIMSymbol.ts b/src/esri/types/symbols/CIMSymbol.ts index 4053842..65c02a6 100644 --- a/src/esri/types/symbols/CIMSymbol.ts +++ b/src/esri/types/symbols/CIMSymbol.ts @@ -1,3 +1,41 @@ import { CIMObject } from '../CIMObject.ts'; +import {CIMColor} from './CIMTextSymbol.ts'; +import {CIMSymbolReference} from '../labeling'; -export type CIMSymbol = CIMObject & {}; +export type CIMColorType = CIMColor & CIMObject; + +export type CIMMarkerPlacement = CIMObject & { + angleToLine: boolean; + extremityPlacement: string; + flipFirst: boolean; + placementTemplate: number[]; + positionArray: number[]; +}; + +export type CIMEffect = CIMObject & { + dashTemplate: number[]; + offset: number; +}; + +export type SymbolLayer = CIMObject & { + capStyle: string; + characterIndex: number; + color: CIMColorType; + effects: CIMEffect[]; + enable: boolean; + fontFamilyName: string; + joinStyle: string; + lineSymbol: CIMSymbol; + markerPlacement: CIMMarkerPlacement; + markerGraphics: CIMSymbolReference[]; + rotateClockwise: boolean; + rotation: number; + separation: number; + size: number; + symbol: CIMSymbol; +}; + +export type CIMSymbol = CIMObject & { + enabled: boolean; + symbolLayers?: SymbolLayer[]; +}; diff --git a/src/expressions.ts b/src/expressions.ts index 80ff022..85fa38d 100644 --- a/src/expressions.ts +++ b/src/expressions.ts @@ -1,10 +1,38 @@ import { LabelExpressionEngine } from './esri/types/index.ts'; -import {ComparisonOperator, Filter} from 'geostyler-style'; +import { + CombinationFilter, + ComparisonOperator, + Filter, + Fproperty, + GeoStylerNumberFunction +} from 'geostyler-style'; +import {WARNINGS} from './toGeostylerUtils.ts'; + +export const fieldToFProperty = (field: string, toLowerCase: boolean): Fproperty => { + return { + args: [toLowerCase ? field.toLowerCase() : field], + name: 'property', + }; +}; + +export const andFilter = (filters: Filter[]): CombinationFilter => { + return ['&&', ...filters]; +}; + +export const orFilter = (conditions: Filter[]): CombinationFilter => { + return ['||', ...conditions]; +}; + +export const equalFilter = (name: string, val: string, toLowerCase: boolean): Filter => { + return getSimpleFilter('==', name, val, toLowerCase); +}; export const getSimpleFilter = ( operator: ComparisonOperator, - value1: string, value2: string, - toLowerCase=true): Filter => { + value1: string, + value2: string, + toLowerCase=true +): Filter => { return [operator, stringToParameter(value1, toLowerCase), stringToParameter(value2, toLowerCase)]; }; @@ -35,66 +63,65 @@ export const convertExpression = ( return processPropertyName(expression); }; - -export const convertWhereClause = (clause: string, toLowerCase: boolean): any => { +export const convertWhereClause = (clause: string, toLowerCase: boolean): Filter => { clause = clause.replace('(', '').replace(')', ''); - const expression = []; if (clause.includes(' AND ')) { - expression.push('And'); - let subexpressions = clause.split(' AND ').map(s => s.trim()); - expression.push(...subexpressions.map(s => convertWhereClause(s, toLowerCase))); - return expression; + const subexpressions = clause.split(' AND ').map(s => s.trim()); + return andFilter(subexpressions.map(s => convertWhereClause(s, toLowerCase))); } if (clause.includes('=')) { - let tokens = clause.split('=').map(t => t.trim()); + const tokens = clause.split('=').map(t => t.trim()); return getSimpleFilter('==', tokens[0], tokens[1], toLowerCase); } if (clause.includes('<>')) { - let tokens = clause.split('<>').map(t => t.trim()); + const tokens = clause.split('<>').map(t => t.trim()); return getSimpleFilter('!=', tokens[0], tokens[1], toLowerCase); } if (clause.includes('>')) { - let tokens = clause.split('>').map(t => t.trim()); + const tokens = clause.split('>').map(t => t.trim()); return getSimpleFilter('>', tokens[0], tokens[1], toLowerCase); } if (clause.toLowerCase().includes(' in ')) { clause = clause.replace(' IN ', ' in '); - let tokens = clause.split(' in '); - let attribute = tokens[0]; + const tokens = clause.split(' in '); + const attribute = tokens[0]; let values: string[] = []; if (tokens[1].startsWith('() ')) { values = tokens[1].substring(3).split(','); } - let subexpressions = []; - for (let v of values) { - subexpressions.push([ - 'PropertyIsEqualTo', - stringToParameter(attribute, toLowerCase), stringToParameter(v, toLowerCase) - ]); - } + const subexpressions: Filter[] = []; + values.forEach(value => { + subexpressions.push( + getSimpleFilter( + '==', + `${stringToParameter(attribute, toLowerCase)}`, + `${stringToParameter(value, toLowerCase)}` + ) + ); + }); if (values.length === 1) { return subexpressions[0]; } - - let accum: any = ['Or', subexpressions[0], subexpressions[1]]; + let accum: Filter = orFilter([subexpressions[0], subexpressions[1]]); for (let subexpression of subexpressions.slice(2)) { - accum = ['Or', accum, subexpression]; + accum = orFilter([accum, subexpression]); } return accum; } - return clause; + WARNINGS.push(`Clause skipped because it is not supported as filter: ${clause}}`); + return ['==', 0, 0]; }; export const processRotationExpression = ( expression: string, rotationType: string, - toLowerCase: boolean): [string, string[], number] | null => { - let field = expression.includes('$feature') ? convertArcadeExpression(expression) : processPropertyName(expression); - let propertyNameExpression = ['PropertyName', toLowerCase ? field.toLowerCase() : field]; + toLowerCase: boolean): GeoStylerNumberFunction | null => { + const field = expression.includes('$feature') ? convertArcadeExpression(expression) : processPropertyName(expression); + const fProperty: Fproperty = fieldToFProperty(field, toLowerCase); if (rotationType === 'Arithmetic') { - return ['Mul', propertyNameExpression, -1]; + return { args: [fProperty, -1], name: 'mul' }; } else if (rotationType === 'Geographic') { - return ['Sub', propertyNameExpression, 90]; + return { args: [fProperty, 90], name: 'sub' }; } return null; }; diff --git a/src/index.spec.ts b/src/index.spec.ts index 6504eb5..b149545 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,10 +1,11 @@ import { expect, it, describe, beforeAll } from 'vitest'; import fs from 'fs'; import { LyrxParser } from './index.ts'; -import {ReadStyleResult, Rule, TextSymbolizer} from 'geostyler-style'; +import {FillSymbolizer, MarkSymbolizer, ReadStyleResult, Rule, TextSymbolizer} from 'geostyler-style'; +import {CIMLayerDocument} from './esri/types'; describe('LyrxParser should parse ae_netzbetreiber.lyrx', () => { - let lyrx: any; + let lyrx: CIMLayerDocument; let lyrxParser: LyrxParser; let geostylerStyle: ReadStyleResult; @@ -41,14 +42,15 @@ describe('LyrxParser should parse ae_netzbetreiber.lyrx', () => { if (!rule) {return;} expect(rule.symbolizers).toBeDefined(); expect(rule.symbolizers.length).toEqual(1); - expect(rule.symbolizers[0].kind).toEqual('Fill'); - expect((rule.symbolizers as any)[0].color).toEqual('#ffffbe'); - expect((rule.symbolizers as any)[0].fillOpacity).toEqual(1); + const symbolizer = rule.symbolizers[0] as FillSymbolizer; + expect(symbolizer.kind).toEqual('Fill'); + expect(symbolizer.color).toEqual('#ffffbe'); + expect(symbolizer.fillOpacity).toEqual(1); }); }); describe('LyrxParser should parse feature-layer-polygon-simple-renderer.lyrx', () => { - let lyrx: any; + let lyrx: CIMLayerDocument; let lyrxParser: LyrxParser; let geostylerStyle: ReadStyleResult; @@ -71,11 +73,11 @@ describe('LyrxParser should parse feature-layer-polygon-simple-renderer.lyrx', ( expect(rule).toBeDefined(); if (!rule) {return;} expect(rule.symbolizers).toHaveLength(2); - const symbolizer1 = rule.symbolizers[0] as any; + const symbolizer1 = rule.symbolizers[0] as FillSymbolizer; expect(symbolizer1.kind).toEqual('Fill'); expect(symbolizer1.color).toEqual('#d1cffc'); expect(symbolizer1.fillOpacity).toEqual(1); - const symbolizer2 = rule.symbolizers[1] as any; + const symbolizer2 = rule.symbolizers[1] as FillSymbolizer; expect(symbolizer2.kind).toEqual('Fill'); expect(symbolizer2.outlineWidth).toEqual(0.9333333333333332); expect(symbolizer2.outlineOpacity).toEqual(1); @@ -83,7 +85,7 @@ describe('LyrxParser should parse feature-layer-polygon-simple-renderer.lyrx', ( }); describe('LyrxParser should parse feature-layer-point-graduated-colors-renderer.lyrx', () => { - let lyrx: any; + let lyrx: CIMLayerDocument; let lyrxParser: LyrxParser; let geostylerStyle: ReadStyleResult; @@ -120,14 +122,14 @@ describe('LyrxParser should parse feature-layer-point-graduated-colors-renderer. expect(rule).toBeDefined(); if (!rule) {return;} expect(rule.symbolizers).toHaveLength(1); - const symbolizer = rule.symbolizers[0] as any; + const symbolizer = rule.symbolizers[0] as MarkSymbolizer; expect(symbolizer.kind).toEqual('Mark'); expect(symbolizer.wellKnownName).toEqual('circle'); expect(symbolizer.opacity).toEqual(1); expect(symbolizer.fillOpacity).toEqual(1); expect(symbolizer.color).toEqual('#f4f400'); expect(symbolizer.rotate).toEqual(0); - expect(symbolizer.radius).toEqual(2.6666666666666665); // FIXME + expect(symbolizer.radius).toEqual(2.6666666666666665); expect(symbolizer.strokeColor).toEqual('#000000'); expect(symbolizer.strokeWidth).toEqual(0.9333333333333332); expect(symbolizer.strokeOpacity).toEqual(1); @@ -135,7 +137,7 @@ describe('LyrxParser should parse feature-layer-point-graduated-colors-renderer. }); describe('LyrxParser should parse afu_gwn_02.lyrx', () => { - let lyrx: any; + let lyrx: CIMLayerDocument; let lyrxParser: LyrxParser; let geostylerStyle: ReadStyleResult; @@ -157,7 +159,7 @@ describe('LyrxParser should parse afu_gwn_02.lyrx', () => { }); describe('LyrxParser should parse kai_blattpk100_01.lyrx', () => { - let lyrx: any; + let lyrx: CIMLayerDocument; let lyrxParser: LyrxParser; let geostylerStyle: ReadStyleResult; diff --git a/src/index.ts b/src/index.ts index 8dd697f..bd3f4c7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,11 +23,11 @@ export class LyrxParser implements StyleParser { unsupportedProperties: UnsupportedProperties = {}; readStyle(inputStyle: CIMLayerDocument): Promise { - const geostyleStyle = convert(inputStyle); + const geostylerStyle = convert(inputStyle); return Promise.resolve({ output: { - name: geostyleStyle[0].name, - rules: geostyleStyle[0].rules, + name: geostylerStyle[0].name, + rules: geostylerStyle[0].rules, }, warnings: [], errors: [], diff --git a/src/processSymbolLayer.ts b/src/processSymbolLayer.ts index 778447c..b538443 100644 --- a/src/processSymbolLayer.ts +++ b/src/processSymbolLayer.ts @@ -1,4 +1,4 @@ -import { Fill, Stroke, Symbolizer } from './badTypes.ts'; +import {Effect, Options} from './types.ts'; import { toWKT } from './wktGeometries.ts'; import { ESRI_SYMBOLS_FONT, OFFSET_FACTOR, ptToPx } from './constants.ts'; import { processColor, processOpacity } from './processUtils.ts'; @@ -11,17 +11,24 @@ import { WARNINGS, } from './toGeostylerUtils.ts'; import { processSymbolReference } from './processSymbolReference.ts'; -import { MarkSymbolizer, WellKnownName } from 'geostyler-style'; +import { + FillSymbolizer, + LineSymbolizer, + MarkSymbolizer, + Symbolizer, + WellKnownName +} from 'geostyler-style'; +import {CIMEffect, SymbolLayer} from './esri/types/symbols'; // import { writeFileSync, existsSync, mkdirSync } from 'fs'; // import uuid from 'uuid'; // import { tmpdir } from 'os'; // import path from 'path'; export const processSymbolLayer = ( - layer: any, - symboltype: any, - options?: any -) => { + layer: SymbolLayer, + symboltype: string, + options: Options +): Symbolizer | undefined => { let layerType: string = layer.type; switch (layerType) { case 'CIMSolidStroke': @@ -42,54 +49,56 @@ export const processSymbolLayer = ( } }; -const processSymbolSolidStroke = (layer: any, symboltype: any) => { - let effects = extractEffect(layer); +const processSymbolSolidStroke = (layer: SymbolLayer, symboltype: string): Symbolizer => { + const effects = extractEffect(layer); if (symboltype === 'CIMPolygonSymbol') { - let stroke: Stroke = { + const fillSymbolizer: FillSymbolizer = { kind: 'Fill', outlineColor: processColor(layer.color), outlineOpacity: processOpacity(layer.color), outlineWidth: ptToPxProp(layer, 'width', 0), }; if ('dasharray' in effects) { - stroke.outlineDasharray = effects.dasharray; - } - return stroke; - } else { - let stroke: Stroke = { - kind: 'Line', - color: processColor(layer.color), - opacity: processOpacity(layer.color), - width: ptToPxProp(layer, 'width', 0), - perpendicularOffset: 0, - cap: layer.capStyle.toLowerCase(), - join: layer.joinStyle.toLowerCase(), - }; - if ('dasharray' in effects) { - stroke.dasharray = effects.dasharray; - } - if ('offset' in effects) { - stroke.perpendicularOffset = effects.offset; + fillSymbolizer.outlineDasharray = effects.dasharray; } - return stroke; + return fillSymbolizer; } + const cap = layer.capStyle.toLowerCase(); + const join = layer.joinStyle.toLowerCase(); + const stroke: LineSymbolizer = { + kind: 'Line', + color: processColor(layer.color), + opacity: processOpacity(layer.color), + width: ptToPxProp(layer, 'width', 0), + perpendicularOffset: 0, + cap: ['butt', 'round', 'square'].includes(cap) ? cap as ('butt' | 'round' | 'square'): undefined, + join: ['round', 'bevel', 'miter'].includes(join) ? join as ('round' | 'bevel' | 'miter') : undefined + }; + if ('dasharray' in effects) { + stroke.dasharray = effects.dasharray; + } + if ('offset' in effects) { + stroke.perpendicularOffset = effects.offset; + } + return stroke; }; -const processSymbolSolidFill = (layer: any): any => { +const processSymbolSolidFill = (layer: SymbolLayer): FillSymbolizer | undefined => { let color = layer.color; - if (color !== undefined) { - return { - kind: 'Fill', - opacity: processOpacity(color), - color: processColor(color), - fillOpacity: 1.0, - }; + if (color === undefined) { + return; } + return { + kind: 'Fill', + opacity: processOpacity(color), + color: processColor(color), + fillOpacity: 1.0, + }; }; const processSymbolCharacterMarker = ( - layer: any, - options: { [key: string]: any } + layer: SymbolLayer, + options: Options ): MarkSymbolizer => { const replaceesri = !!options.replaceesri; const fontFamily = layer.fontFamilyName; @@ -111,22 +120,16 @@ const processSymbolCharacterMarker = ( rotate *= -1; } - let fillColor: string; - let fillOpacity: number; - let strokeColor: string; - let strokeWidth: number; - let strokeOpacity: number; - try { - let symbolLayers = layer.symbol.symbolLayers; + let fillColor = '#000000'; + let fillOpacity = 1; + let strokeColor = '#000000'; + let strokeWidth = 0; + let strokeOpacity = 0; + const symbolLayers = layer.symbol.symbolLayers; + if (symbolLayers) { fillColor = extractFillColor(symbolLayers); fillOpacity = extractFillOpacity(symbolLayers); [strokeColor, strokeWidth, strokeOpacity] = extractStroke(symbolLayers); - } catch (e) { - fillColor = '#000000'; - fillOpacity = 1.0; - strokeOpacity = 0; - strokeColor = '#000000'; - strokeWidth = 0.0; } return { @@ -144,7 +147,7 @@ const processSymbolCharacterMarker = ( }; }; -const processSymbolVectorMarker = (layer: any): MarkSymbolizer => { +const processSymbolVectorMarker = (layer: SymbolLayer): MarkSymbolizer => { if (layer.size) { layer.size = ptToPxProp(layer, 'size', 3); } @@ -158,44 +161,41 @@ const processSymbolVectorMarker = (layer: any): MarkSymbolizer => { let maxX: number | null = null; let maxY: number | null = null; - let symbol: Symbolizer; + let symbol: MarkSymbolizer; const markerGraphics = layer.markerGraphics !== undefined ? layer.markerGraphics : []; if (markerGraphics.length > 0) { // TODO: support multiple marker graphics const markerGraphic = markerGraphics[0]; - symbol = processSymbolReference(markerGraphic, {})[0]; - const sublayers = markerGraphic.symbol.symbolLayers.filter( - (sublayer: any) => sublayer.enable - ); - fillColor = extractFillColor(sublayers); - [strokeColor, strokeWidth, strokeOpacity] = extractStroke(sublayers); - markerSize = - symbol.size !== undefined - ? symbol.size - : layer.size !== undefined - ? layer.size - : 10; - if (markerGraphic.symbol.type === 'CIMPointSymbol') { - wellKnownName = symbol.wellKnownName ?? wellKnownName; - } else if ( - ['CIMLineSymbol', 'CIMPolygonSymbol'].includes(markerGraphic.symbol.type) - ) { - let shape = toWKT( - markerGraphic.geometry !== undefined - ? markerGraphic.geometry - : undefined + if (markerGraphic.symbol && markerGraphic.symbol.symbolLayers) { + symbol = processSymbolReference(markerGraphic, {})[0] as MarkSymbolizer; + const subLayers = markerGraphic.symbol.symbolLayers.filter( + (sublayer: SymbolLayer) => sublayer.enable ); - wellKnownName = shape.wellKnownName; - maxX = ptToPxProp(shape, 'maxX', 0); - maxY = ptToPxProp(shape, 'maxY', 0); + fillColor = extractFillColor(subLayers); + [strokeColor, strokeWidth, strokeOpacity] = extractStroke(subLayers); + const layerSize = layer.size !== undefined ? layer.size : 10; + markerSize = typeof symbol.radius === 'number' ? symbol.radius : layerSize; + if (markerGraphic.symbol.type === 'CIMPointSymbol') { + wellKnownName = symbol.wellKnownName ?? wellKnownName; + } else if ( + ['CIMLineSymbol', 'CIMPolygonSymbol'].includes(markerGraphic.symbol.type) + ) { + const geometry = markerGraphic.geometry; + if (geometry) { + const shape = toWKT(geometry); + wellKnownName = shape.wellKnownName; + maxX = ptToPxProp(shape, 'maxX', 0); + maxY = ptToPxProp(shape, 'maxY', 0); + } + } } } - // FIXME marker should support outlineDasharray ? - const marker: any = { - opacity: 1.0, - rotate: 0.0, + // TODO marker should support outlineDasharray ? + const marker: MarkSymbolizer= { + opacity: 1, + rotate: 0, kind: 'Mark', color: fillColor, wellKnownName: wellKnownName, @@ -203,12 +203,14 @@ const processSymbolVectorMarker = (layer: any): MarkSymbolizer => { strokeColor: strokeColor, strokeWidth: strokeWidth, strokeOpacity: strokeOpacity, - fillOpacity: 1.0, + fillOpacity: 1, }; if (maxX !== null) { + // @ts-ignore FIXME see issue #62 marker.maxX = maxX; } if (maxY !== null) { + // @ts-ignore FIXME see issue #62 marker.maxY = maxY; } @@ -219,10 +221,13 @@ const processSymbolVectorMarker = (layer: any): MarkSymbolizer => { : undefined; // Conversion of dash arrays is made on a case-by-case basis if (JSON.stringify(markerPlacement) === JSON.stringify([12, 3])) { + // @ts-ignore FIXME see issue #63 marker.outlineDasharray = '4 0 4 7'; marker.radius = 3; + // @ts-ignore FIXME see issue #63 marker.perpendicularOffset = -3.5; } else if (JSON.stringify(markerPlacement) === JSON.stringify([15])) { + // @ts-ignore FIXME see issue #63 marker.outlineDasharray = '0 5 9 1'; marker.radius = 5; } @@ -230,10 +235,15 @@ const processSymbolVectorMarker = (layer: any): MarkSymbolizer => { return marker; }; -const processSymbolHatchFill = (layer: any): { [key: string]: any } => { - let rotation = layer.rotation || 0; - let symbolLayers = layer.lineSymbol.symbolLayers; - let [color, width, opacity] = extractStroke(symbolLayers); +const processSymbolHatchFill = (layer: SymbolLayer): Symbolizer => { + const rotation = layer.rotation || 0; + const symbolLayers = layer.lineSymbol.symbolLayers; + let color = '#000000'; + let width = 0; + let opacity = 0; + if (symbolLayers) { + [color, width, opacity] = extractStroke(symbolLayers); + } // Use symbol and not rotation because rotation crops the line. let wellKnowName = hatchMarkerForAngle(rotation); @@ -250,27 +260,31 @@ const processSymbolHatchFill = (layer: any): { [key: string]: any } => { ? ptToPx(rawSeparation) : rawSeparation * 2; - let fill: Fill = { + const markSymbolizer: MarkSymbolizer = { + kind: 'Mark', + color: color, + wellKnownName: wellKnowName, + radius: separation, + strokeColor: color, + strokeWidth: width, + strokeOpacity: opacity, + rotate: 0, // no rotation, use the symbol. + }; + + const fillSymbolizer: FillSymbolizer = { kind: 'Fill', opacity: 1.0, - graphicFill: [ - { - kind: 'Mark', - color: color, - wellKnownName: wellKnowName, - size: separation, - strokeColor: color, - strokeWidth: width, - strokeOpacity: opacity, - rotate: 0, // no rotation, use the symbol. - }, - ], - Z: 0, + graphicFill: markSymbolizer, }; + if (!symbolLayers) { + return fillSymbolizer; + } + let effects = extractEffect(symbolLayers[0]); if ('dasharray' in effects) { - fill.graphicFill[0].outlineDasharray = effects.dasharray; + // @ts-ignore FIXME see issue #63 + fillSymbolizer.graphicFill!.outlineDasharray = effects.dasharray; // In case of dash array, the size must be at least as long as the dash pattern sum. if (separation > 0) { @@ -280,9 +294,11 @@ const processSymbolHatchFill = (layer: any): { [key: string]: any } => { // To keep the "original size" given by the separation value, we play with a negative margin. let negativeMargin = ((neededSize - separation) / 2) * -1; if (wellKnowName === getStraightHatchMarker()[0]) { - fill.graphicFillMargin = [negativeMargin, 0, negativeMargin, 0]; + // @ts-ignore FIXME see issue #64 + fillSymbolizer.graphicFillMargin = [negativeMargin, 0, negativeMargin, 0]; } else { - fill.graphicFillMargin = [0, negativeMargin, 0, negativeMargin]; + // @ts-ignore FIXME see issue #64 + fillSymbolizer.graphicFillMargin = [0, negativeMargin, 0, negativeMargin]; } } else { // In case of tilted lines, the trick with the margin is not possible without cropping the pattern. @@ -291,13 +307,13 @@ const processSymbolHatchFill = (layer: any): { [key: string]: any } => { 'Unable to keep the original size of CIMHatchFill for line with rotation' ); } - fill.graphicFill[0].size = neededSize; + markSymbolizer.radius = neededSize; } } - return fill; + return fillSymbolizer; }; -const processSymbolPicture = (layer: any): { [key: string]: any } => { +const processSymbolPicture = (layer: SymbolLayer): Symbolizer => { // let url = layer.url; // if (!existsSync(url)) { // let tokens = url.split(';'); @@ -323,25 +339,24 @@ const processSymbolPicture = (layer: any): { [key: string]: any } => { opacity: 1.0, rotate: 0.0, kind: 'Icon', - color: null, + color: undefined, // image: url, image: 'http://FIXME', size: size, - Z: 0, }; }; -const extractEffect = (layer: Record): Record => { - let effects: Record = {}; +const extractEffect = (layer: SymbolLayer): Effect => { + let effects: Effect = {}; if ('effects' in layer) { - layer.effects.forEach((effect: any) => { + layer.effects.forEach((effect: CIMEffect) => { effects = { ...effects, ...processEffect(effect) }; }); } return effects; }; -const processEffect = (effect: Record): Record => { +const processEffect = (effect: CIMEffect): Effect => { let ptToPxAndCeil = (v: number) => { return Math.ceil(ptToPx(v)); }; @@ -362,15 +377,15 @@ const processEffect = (effect: Record): Record => { return {}; }; -const getStraightHatchMarker = (): any => { +const getStraightHatchMarker = (): WellKnownName[] => { return ['shape://horline', 'shape://vertline']; }; -const getTiltedHatchMarker = (): any => { +const getTiltedHatchMarker = (): WellKnownName[] => { return ['shape://slash', 'shape://backslash']; }; -const hatchMarkerForAngle = (angle: number): any => { +const hatchMarkerForAngle = (angle: number): WellKnownName => { const straightHatchMarkers = getStraightHatchMarker(); const tiltedHatchMarkers = getTiltedHatchMarker(); const quadrant = Math.floor(((angle + 22.5) % 180) / 45.0); @@ -383,7 +398,7 @@ const hatchMarkerForAngle = (angle: number): any => { ][quadrant]; }; -const extractOffset = (symbolLayer: any): undefined | [number, number] => { +const extractOffset = (symbolLayer: SymbolLayer): undefined | [number, number] => { let offsetX = ptToPxProp(symbolLayer, 'offsetX', 0) * OFFSET_FACTOR; let offsetY = ptToPxProp(symbolLayer, 'offsetY', 0) * OFFSET_FACTOR * -1; diff --git a/src/processSymbolReference.ts b/src/processSymbolReference.ts index 19146d3..248fcc9 100644 --- a/src/processSymbolReference.ts +++ b/src/processSymbolReference.ts @@ -1,7 +1,5 @@ import { ESRI_SYMBOLS_FONT, - MarkerPlacementAngle, - MarkerPlacementPosition, POLYGON_FILL_RESIZE_FACTOR, } from './constants.ts'; import { @@ -12,85 +10,118 @@ import { ptToPxProp, toHex, } from './toGeostylerUtils.ts'; import { - MarkerPlacement, Options, - Symbolizer, -} from './badTypes.ts'; +} from './types.ts'; import { processSymbolLayer } from './processSymbolLayer.ts'; +import { + FillSymbolizer, + LineSymbolizer, + MarkSymbolizer, + PointSymbolizer, + Symbolizer, + WellKnownName +} from 'geostyler-style'; +import {CIMSymbolReference} from './esri/types/labeling/CIMSymbolReference.ts'; +import {CIMMarkerPlacement, CIMSymbol, SymbolLayer} from './esri/types/symbols'; +import {fieldToFProperty} from './expressions.ts'; export const processSymbolReference = ( - symbolref: any, + symbolref: CIMSymbolReference, options: Options ): Symbolizer[] => { const symbol = symbolref.symbol; const symbolizers: Symbolizer[] = []; - if (!symbol.symbolLayers) { + if (!symbol || !symbol.symbolLayers) { return symbolizers; } - - for (const layer of symbol.symbolLayers.slice().reverse()) { - // drawing order for geostyler is inverse of rule order + // Drawing order for geostyler is inverse of rule order. + const layers = symbol.symbolLayers.slice().reverse(); + layers.forEach((layer) => { + // Skip not enabled layers. if (!layer.enable) { - continue; + return; } - let symbolizer = processSymbolLayer(layer, symbol.type, options); + // Skip layer without symbolizer. + const symbolizer = processSymbolLayer(layer, symbol.type, options); if (!symbolizer) { - continue; + return; } - if ( - ['CIMVectorMarker', 'CIMPictureFill', 'CIMCharacterMarker'].includes( - layer.type - ) - ) { - if (symbol.type === 'CIMLineSymbol') { - if (layer.type === 'CIMCharacterMarker') { - if (orientedMarkerAtStartOfLine(layer.markerPlacement)) { - symbolizer = processOrientedMarkerAtEndOfLine( - layer, - 'start', - options - ); - symbolizers.push(symbolizer); - } - if (orientedMarkerAtEndOfLine(layer.markerPlacement)) { - symbolizer = processOrientedMarkerAtEndOfLine( - layer, - 'end', - options - ); - symbolizers.push(symbolizer); - } - continue; - } else { - symbolizer = formatLineSymbolizer(symbolizer); + if ([ + 'CIMVectorMarker', + 'CIMPictureFill', + 'CIMCharacterMarker' + ].includes(layer.type)) { + processSymbolLayerIfCharacterMarker(symbol, layer, symbolizer, options); + } + symbolizers.push(symbolizer); + }); + return symbolizers; +}; + +const processSymbolLayerIfCharacterMarker = ( + symbol: CIMSymbol, + layer: SymbolLayer, + symbolizer: Symbolizer, + options: Options +): Symbolizer[] => { + const symbolizers: Symbolizer[] = []; + if (symbol.type === 'CIMPolygonSymbol') { + const markerPlacement = layer.markerPlacement || {}; + const polygonSymbolizer = formatPolygonSymbolizer(symbolizer as MarkSymbolizer, markerPlacement); + if (polygonSymbolizer) { + symbolizers.push(polygonSymbolizer); + } + return symbolizers; + } + if (symbol.type === 'CIMLineSymbol') { + if (layer.type === 'CIMCharacterMarker') { + if (orientedMarkerAtStartOfLine(layer.markerPlacement)) { + const startSymbolizer = processOrientedMarkerAtEndOfLine( + layer, + 'start', + options + ); + if (startSymbolizer) { + symbolizers.push(startSymbolizer); + } + } + if (orientedMarkerAtEndOfLine(layer.markerPlacement)) { + const endSymbolizer = processOrientedMarkerAtEndOfLine( + layer, + 'end', + options + ); + if (endSymbolizer) { + symbolizers.push(endSymbolizer); } - } else if (symbol.type === 'CIMPolygonSymbol') { - const markerPlacement = layer.markerPlacement || {}; - symbolizer = formatPolygonSymbolizer(symbolizer, markerPlacement); } + return symbolizers; } - symbolizers.push(symbolizer); + // Not CIMCharacterMarker + const lineSymbolizer = formatLineSymbolizer(symbolizer as PointSymbolizer); + symbolizers.push(lineSymbolizer); + return symbolizers; } - return symbolizers; }; -const formatLineSymbolizer = (symbolizer: Symbolizer): Symbolizer => { +const formatLineSymbolizer = (symbolizer: PointSymbolizer): LineSymbolizer => { return { kind: 'Line', opacity: 1.0, perpendicularOffset: 0.0, - graphicStroke: [symbolizer], - graphicStrokeInterval: ptToPxProp(symbolizer, 'size', 0) * 2, // TODO + graphicStroke: symbolizer, + // @ts-ignore FIXME see issue #65 + graphicStrokeInterval: ptToPxProp(symbolizer, 'size', 0) * 2, graphicStrokeOffset: 0.0, }; }; const formatPolygonSymbolizer = ( - symbolizer: Symbolizer, - markerPlacement: MarkerPlacement -): Symbolizer | null => { + symbolizer: MarkSymbolizer, + markerPlacement: CIMMarkerPlacement +): FillSymbolizer | LineSymbolizer | null => { const markerPlacementType = markerPlacement.type; if (markerPlacementType === 'CIMMarkerPlacementInsidePolygon') { const margin = processMarkerPlacementInsidePolygon( @@ -100,8 +131,8 @@ const formatPolygonSymbolizer = ( return { kind: 'Fill', opacity: 1.0, - perpendicularOffset: 0.0, - graphicFill: [symbolizer], + graphicFill: symbolizer, + // @ts-ignore FIXME see issue #64 graphicFillMargin: margin, }; } @@ -109,28 +140,30 @@ const formatPolygonSymbolizer = ( return { kind: 'Line', opacity: 1.0, - size: ptToPxProp(symbolizer, 'size', 10), + width: ptToPxProp(symbolizer, 'size', 10), perpendicularOffset: ptToPxProp(symbolizer, 'perpendicularOffset', 0.0), - graphicStroke: [symbolizer], + graphicStroke: symbolizer, }; } return null; }; const processOrientedMarkerAtEndOfLine = ( - layer: Record, + layer: SymbolLayer, orientedMarker: string, - options: Record -): Record | undefined => { - let markerPositionFnc: string, markerRotationFnc: string, rotation: number; + options: Options +): MarkSymbolizer | undefined => { + // let markerPositionFnc: string; + // let markerRotationFnc: string; + let rotation: number; if (orientedMarker === 'start') { - markerPositionFnc = MarkerPlacementPosition.START; - markerRotationFnc = MarkerPlacementAngle.START; + // markerPositionFnc = MarkerPlacementPosition.START; + // markerRotationFnc = MarkerPlacementAngle.START; rotation = layer?.rotation ?? 180; } else if (orientedMarker === 'end') { - markerPositionFnc = MarkerPlacementPosition.END; - markerRotationFnc = MarkerPlacementAngle.END; + // markerPositionFnc = MarkerPlacementPosition.END; + // markerRotationFnc = MarkerPlacementAngle.END; rotation = layer?.rotation ?? 0; } else { return undefined; @@ -141,11 +174,11 @@ const processOrientedMarkerAtEndOfLine = ( const charindex = layer.characterIndex; const hexcode = toHex(charindex); - let name; + let name: WellKnownName; if (fontFamily === ESRI_SYMBOLS_FONT && replaceesri) { name = esriFontToStandardSymbols(charindex); } else { - name = `ttf://${fontFamily}#${hexcode}`; + name = `ttf://${fontFamily}#${hexcode}` as WellKnownName; } let symbolLayers, @@ -156,7 +189,7 @@ const processOrientedMarkerAtEndOfLine = ( strokeOpacity; try { - symbolLayers = layer.symbol.symbolLayers; + symbolLayers = layer.symbol.symbolLayers ?? []; fillColor = extractFillColor(symbolLayers); fillOpacity = extractFillOpacity(symbolLayers); [strokeColor, strokeWidth, strokeOpacity] = extractStroke(symbolLayers); @@ -168,39 +201,48 @@ const processOrientedMarkerAtEndOfLine = ( strokeWidth = 0.0; } + const fProperty = fieldToFProperty('shape', true); return { opacity: 1.0, fillOpacity: fillOpacity, strokeColor: strokeColor, strokeOpacity: strokeOpacity, strokeWidth: strokeWidth, - rotate: ['Add', [markerRotationFnc, ['PropertyName', 'shape']], rotation], + // FIXME see issue #66 use markerRotationFnc ? Previous code was: + // rotate: ['Add', [markerRotationFnc, ['PropertyName', 'shape']], rotation], + rotate: { args: [fProperty, rotation], name: 'add' }, kind: 'Mark', color: fillColor, wellKnownName: name, - size: ptToPxProp(layer, 'size', 10), - Z: 0, - Geometry: [markerPositionFnc, ['PropertyName', 'shape']], + radius: ptToPxProp(layer, 'size', 10), + // @ts-ignore FIXME see issue #66 + geometry: [markerPositionFnc, ['PropertyName', 'shape']], + // @ts-ignore FIXME see issue #66 inclusion: 'mapOnly', }; }; const processMarkerPlacementInsidePolygon = ( - symbolizer: Record, - markerPlacement: Record + symbolizer: MarkSymbolizer, + markerPlacement: CIMMarkerPlacement ): number[] => { - let resizeFactor = symbolizer?.wellKnownName?.startsWith('wkt://POLYGON') + const resizeFactor = symbolizer?.wellKnownName?.startsWith('wkt://POLYGON') ? 1 : POLYGON_FILL_RESIZE_FACTOR; - let size = Math.round((symbolizer?.size ?? 0) * resizeFactor) || 1; - symbolizer.size = size; + const radius = typeof symbolizer.radius === 'number' ? symbolizer.radius : 0; + const size = Math.round(radius * resizeFactor) || 1; + symbolizer.radius = size; - let maxX = size / 2, - maxY = size / 2; - if (symbolizer?.maxX && symbolizer?.maxY) { - maxX = Math.floor(symbolizer.maxX * resizeFactor) || 1; - maxY = Math.floor(symbolizer.maxY * resizeFactor) || 1; + let maxX = size / 2; + let maxY = size / 2; + // @ts-ignore FIXME see issue #62 + const symMaxX = symbolizer?.maxX ?? maxX; + // @ts-ignore FIXME see issue #62 + const symMaxY = symbolizer?.maxY ?? maxY; + if (symMaxX && symMaxY) { + maxX = Math.floor(symMaxX * resizeFactor) || 1; + maxY = Math.floor(symMaxY * resizeFactor) || 1; } let stepX = ptToPxProp(markerPlacement, 'stepX', 0); @@ -214,18 +256,18 @@ const processMarkerPlacementInsidePolygon = ( stepY += maxY * 2; } - let offsetX = ptToPxProp(markerPlacement, 'offsetX', 0); - let offsetY = ptToPxProp(markerPlacement, 'offsetY', 0); + const offsetX = ptToPxProp(markerPlacement, 'offsetX', 0); + const offsetY = ptToPxProp(markerPlacement, 'offsetY', 0); - let right = Math.round(stepX / 2 - maxX - offsetX); - let left = Math.round(stepX / 2 - maxX + offsetX); - let top = Math.round(stepY / 2 - maxY - offsetY); - let bottom = Math.round(stepY / 2 - maxY + offsetY); + const right = Math.round(stepX / 2 - maxX - offsetX); + const left = Math.round(stepX / 2 - maxX + offsetX); + const top = Math.round(stepY / 2 - maxY - offsetY); + const bottom = Math.round(stepY / 2 - maxY + offsetY); return [top, right, bottom, left]; }; -const orientedMarkerAtStartOfLine = (markerPlacement: any): boolean => { +const orientedMarkerAtStartOfLine = (markerPlacement: CIMMarkerPlacement): boolean => { if (markerPlacement?.angleToLine) { if ( markerPlacement.type === 'CIMMarkerPlacementAtRatioPositions' && @@ -244,7 +286,7 @@ const orientedMarkerAtStartOfLine = (markerPlacement: any): boolean => { }; const orientedMarkerAtEndOfLine = ( - markerPlacement: MarkerPlacement + markerPlacement: CIMMarkerPlacement ): boolean => { if (markerPlacement?.angleToLine) { if ( diff --git a/src/processUtils.ts b/src/processUtils.ts index 1a72653..918c3b5 100644 --- a/src/processUtils.ts +++ b/src/processUtils.ts @@ -1,15 +1,17 @@ -export const processOpacity = (color: { values: number[] } | null): number => { - if (color === null) { +import {CIMColor, CIMColorType} from './esri/types/symbols'; + +export const processOpacity = (color: CIMColor | null): number => { + if (color === null || !color.values) { return 0; } return color.values[color.values.length - 1] / 100; }; -export const processColor = (color: any): string => { +export const processColor = (color: CIMColorType): string => { if (color === null) { return '#000000'; } - let values = color.values; + let values = color.values ?? [0, 0, 0]; if (color.type === 'CIMRGBColor') { return rgbaToHex(values); } else if (color.type === 'CIMCMYKColor') { diff --git a/src/toGeostyler.ts b/src/toGeostyler.ts index 4d27468..8973d64 100644 --- a/src/toGeostyler.ts +++ b/src/toGeostyler.ts @@ -1,31 +1,41 @@ -import {Rule as FIXMERULE, Style} from 'geostyler-style'; -import {convertExpression, convertWhereClause, getSimpleFilter, processRotationExpression,} from './expressions.ts'; -import {Options, Rule, Symbolizer} from './badTypes.ts'; +import {Filter, GeoStylerNumberFunction, Rule, Style, Symbolizer} from 'geostyler-style'; +import { + andFilter, + convertExpression, + convertWhereClause, equalFilter, + fieldToFProperty, + orFilter, + processRotationExpression, +} from './expressions.ts'; +import {Options} from './types.ts'; import {extractFillColor, extractFontWeight, ptToPxProp, WARNINGS,} from './toGeostylerUtils.ts'; import {processSymbolReference} from './processSymbolReference.ts'; import { CIMFeatureLayer, CIMLabelClass, CIMLayerDefinition, - CIMLayerDocument, + CIMLayerDocument, CIMRasterLayer, CIMRenderer, CIMUniqueValueRenderer, Group, LabelExpressionEngine, LabelFeatureType, } from './esri/types/index.ts'; import {CIMTextSymbol} from './esri/types/symbols/index.ts'; +import {CIMBreaksRenderer} from './esri/types/renderers/CIMBreaksRenderer.ts'; +import {CIMSimpleRenderer} from './esri/types/renderers/CIMSimpleRenderer.ts'; +import {CIMMaplexRotationProperties} from './esri/types/labeling/CIMMaplexRotationProperties.ts'; const usedIcons: string[] = []; export const convert = ( layerDocument: CIMLayerDocument, - options = undefined -): any => { + options: Options = {} +): [Style, string[], string[]] => { const geoStyler = processLayer(layerDocument?.layerDefinitions?.[0], options); return [geoStyler, usedIcons, WARNINGS]; }; const processLayer = ( layer: CIMLayerDefinition, - options: Options = {} + options: Options ): Style => { const style: Style = { name: layer.name, @@ -38,9 +48,9 @@ const processLayer = ( }; if (layer.type === 'CIMFeatureLayer') { - style.rules = processFeatureLayer(layer, options); + style.rules = processFeatureLayer(layer as CIMFeatureLayer, options); } else if (layer.type === 'CIMRasterLayer') { - style.rules = processRasterLayer(layer); + style.rules = processRasterLayer(layer as CIMRasterLayer); } return style; @@ -49,37 +59,14 @@ const processLayer = ( const processFeatureLayer = ( layer: CIMFeatureLayer, options: Options = {} -): FIXMERULE[] => { +): Rule[] => { const toLowerCase = !!options.toLowerCase; - const renderer = layer.renderer!; - const rules: Rule[] = []; - - if (renderer.type === 'CIMSimpleRenderer') { - rules.push(processSimpleRenderer(renderer, options)); - } else if (renderer.type === 'CIMUniqueValueRenderer') { - if (renderer.groups) { - for (const group of renderer.groups) { - rules.push( - ...processUniqueValueGroup(renderer.fields!, group, options) - ); - } - } else if (renderer.defaultSymbol) { - // This is really a simple renderer - const rule: Rule = { - name: '', - symbolizers: processSymbolReference(renderer.defaultSymbol, options), - }; - rules.push(rule); - } - } else if ( - renderer.type === 'CIMClassBreaksRenderer' && - ['GraduatedColor', 'GraduatedSymbol'].includes(renderer.classBreakType!) - ) { - rules.push(...processClassBreaksRenderer(renderer, options)); - } else { - WARNINGS.push(`Unsupported renderer type: ${renderer}`); + const renderer = layer.renderer; + if (!renderer) { + WARNINGS.push(`No renderer on layer: ${layer.name}`); return []; } + const rules = processRenderer(renderer, options); if (layer.labelVisibility) { for (const labelClass of layer.labelClasses || []) { @@ -89,24 +76,70 @@ const processFeatureLayer = ( const rotation = getSymbolRotationFromVisualVariables(renderer, toLowerCase); if (rotation) { - for (const rule of rules) { - for (const symbolizer of rule.symbolizers ?? []) { - symbolizer.rotate = rotation; - } + rules.forEach((rule) => { + assignRotation(rotation, rule.symbolizers); + }); + } + return rules; +}; + +const processRenderer = (renderer: CIMRenderer, options: Options): Rule[] => { + const rules: Rule[] = []; + // CIMSimpleRenderer + if (renderer.type === 'CIMSimpleRenderer') { + rules.push(processSimpleRenderer(renderer as CIMSimpleRenderer, options)); + return rules; + } + // CIMUniqueValueRenderer + if (renderer.type === 'CIMUniqueValueRenderer') { + const uvRenderer = renderer as CIMUniqueValueRenderer; + if (uvRenderer.groups) { + uvRenderer.groups.forEach((group) => { + rules.push( + ...processUniqueValueGroup(uvRenderer.fields!, group, options) + ); + }); + } else if (uvRenderer.defaultSymbol) { + // This is really a simple renderer + const rule: Rule = { + name: '', + symbolizers: processSymbolReference(uvRenderer.defaultSymbol, options), + }; + rules.push(rule); } + return rules; } - return rules as FIXMERULE[]; + // CIMClassBreaksRenderer + if (renderer.type === 'CIMClassBreaksRenderer') { + const breaksRenderer = renderer as CIMBreaksRenderer; + if (['GraduatedColor', 'GraduatedSymbol'].includes(breaksRenderer.classBreakType)) { + rules.push(...processClassBreaksRenderer(breaksRenderer, options)); + return rules; + } + } + WARNINGS.push(`Unsupported renderer type: ${renderer}`); + return rules; }; -const processRasterLayer = (_layer: any): FIXMERULE[] => { +const processRasterLayer = (_layer: CIMRasterLayer): [] => { WARNINGS.push('CIMRasterLayer are not supported yet.'); // const rules = [{ name: layer.name, symbolizers: [rasterSymbolizer(layer)] }]; // geostyler.rules = rules; return []; }; +const assignRotation = (rotation: GeoStylerNumberFunction, symbolizers: Symbolizer[]) => { + symbolizers.filter(symbolizer => + symbolizer.kind === 'Text' || + symbolizer.kind === 'Icon' || + symbolizer.kind === 'Mark' + ).forEach(symbolizer => { + symbolizer.rotate = rotation; + }); +}; + const processClassBreaksRenderer = ( - renderer: any, + renderer: CIMBreaksRenderer, options: Options = {} ): Rule[] => { const rules: Rule[] = []; @@ -116,13 +149,14 @@ const processClassBreaksRenderer = ( const toLowerCase = !!options.toLowerCase; const rotation = getSymbolRotationFromVisualVariables(renderer, toLowerCase); - for (const classbreak of renderer.breaks || []) { - const symbolizers = processSymbolReference(classbreak.symbol, options); - const upperbound = classbreak.upperBound || 0; + const rendererBreaks = renderer.breaks || []; + rendererBreaks.forEach((rBreak) => { + const symbolizers = processSymbolReference(rBreak.symbol, options); + const upperbound = rBreak.upperBound || 0; - let filt: any[]; + let filter: Filter; if (lastbound !== null) { - filt = [ + filter = [ '&&', [ '>', @@ -136,7 +170,7 @@ const processClassBreaksRenderer = ( ], ]; } else { - filt = [ + filter = [ '<=', toLowerCase ? field.toLowerCase() : field, upperbound, @@ -145,20 +179,18 @@ const processClassBreaksRenderer = ( lastbound = upperbound; if (rotation) { - for (const symbolizer of symbolizers) { - symbolizer.rotate = rotation; - } + assignRotation(rotation, symbolizers); } - const ruledef: Rule = { - name: classbreak.label || 'classbreak', - symbolizers: symbolizers, - filter: filt, + const ruleDef: Rule = { + filter, + symbolizers, + name: rBreak.label || 'classbreak', }; symbolsAscending.push(symbolizers); - rules.push(ruledef); - } + rules.push(ruleDef); + }); if (!renderer.showInAscendingOrder) { rules.reverse(); @@ -174,7 +206,7 @@ const processLabelClass = ( labelClass: CIMLabelClass, toLowerCase: boolean, ): Rule => { - // todo ConvertTextSymbol: + // TODO ConvertTextSymbol: if (labelClass.textSymbol?.symbol?.type !== 'CIMTextSymbol') { return { name: '', symbolizers: [] }; } @@ -187,11 +219,11 @@ const processLabelClass = ( ); const fontFamily = textSymbol?.fontFamilyName || 'Arial'; const fontSize = ptToPxProp(textSymbol, 'height', 12, true); + // @ts-ignore FIXME see issue #68 const color = extractFillColor(textSymbol?.symbol?.symbolLayers ?? []); const fontWeight = extractFontWeight(textSymbol); const rotationProps = - labelClass.maplexLabelPlacementProperties?.rotationProperties || - ({} as any); + labelClass.maplexLabelPlacementProperties?.rotationProperties || {} as CIMMaplexRotationProperties; const rotationField = rotationProps.rotationField; const symbolizer: Symbolizer = { @@ -199,10 +231,10 @@ const processLabelClass = ( anchor: 'right', rotate: 0.0, color: color, - font: fontFamily, + font: [fontFamily], label: expression, size: fontSize, - weight: fontWeight, + fontWeight: fontWeight, }; const stdProperties = labelClass.standardLabelPlacementProperties; @@ -222,6 +254,7 @@ const processLabelClass = ( maplexPlacementType === LabelFeatureType.Line ) { const primaryOffset = ptToPxProp(textSymbol, 'primaryOffset', 0); + // @ts-ignore FIXME see issue #63 symbolizer.perpendicularOffset = primaryOffset + fontSize; } else if ( maplexPlacementType === LabelFeatureType.Point && @@ -229,35 +262,27 @@ const processLabelClass = ( ) { const offset = maplexPrimaryOffset + fontSize / 2; symbolizer.offset = [offset, offset]; - symbolizer.anchorPointX = symbolizer.anchorPointY = 0.0; } else if ( stdPlacementType === LabelFeatureType.Point && stdPointPlacementType === 'AroundPoint' ) { const offset = maplexPrimaryOffset + fontSize / 2; symbolizer.offset = [offset, offset]; - symbolizer.anchorPointX = symbolizer.anchorPointY = 0.0; } else { symbolizer.offset = [0.0, 0.0]; } if (rotationField) { - symbolizer.rotate = [ - 'Mul', - [ - 'PropertyName', - toLowerCase ? rotationField.toLowerCase() : rotationField, - ], - -1, - ]; + const fProperty = fieldToFProperty(rotationField, toLowerCase); + symbolizer.rotate = { args: [fProperty, -1], name: 'mul' }; } else { - symbolizer.rotate = 0.0; + symbolizer.rotate = 0; } const haloSize = ptToPxProp(textSymbol, 'haloSize', 0); if (haloSize && textSymbol.haloSymbol) { - const haloColor = extractFillColor( - textSymbol?.haloSymbol?.symbolLayers ?? [] + // @ts-ignore FIXME see issue #68 + const haloColor = extractFillColor(textSymbol?.haloSymbol?.symbolLayers ?? [] ); Object.assign(symbolizer, { haloColor: haloColor, @@ -266,6 +291,7 @@ const processLabelClass = ( }); } + // @ts-ignore FIXME see issue #67 symbolizer.group = labelClass.maplexLabelPlacementProperties?.thinDuplicateLabels || (maplexPlacementType === LabelFeatureType.Polygon && @@ -289,7 +315,7 @@ const processLabelClass = ( return rule; }; -const processSimpleRenderer = (renderer: any, options: Options): Rule => { +const processSimpleRenderer = (renderer: CIMSimpleRenderer, options: Options): Rule => { return { name: renderer.label || '', symbolizers: processSymbolReference(renderer.symbol, options), @@ -298,67 +324,57 @@ const processSimpleRenderer = (renderer: any, options: Options): Rule => { const processUniqueValueGroup = ( fields: string[], - group: any, + group: Group, options: Options ): Rule[] => { const toLowerCase = options.toLowerCase || false; - - const and = (a: any[], b: any[]): any[] => { - return ['&&', a, b]; - }; - - const or = (listConditions: any[]): any[] => { - const orConditions = listConditions; - orConditions.unshift('||'); - return orConditions; - }; - - const equal = (name: string, val: any): any => { - return getSimpleFilter('==', name, val, toLowerCase); - }; - const rules: Rule[] = []; - for (const clazz of group.classes || []) { - const rule: Rule = { name: clazz.label || 'label' }; - const values = clazz.values; - const conditions: any[] = []; - let ruleFilter: any[] | null = null; - - for (const v of values) { - if ('fieldValues' in v) { - const fieldValues = v.fieldValues!; - let condition = equal(fields[0], fieldValues[0]); + group.classes = group.classes || []; + group.classes.forEach((oneClass) => { + const name = oneClass.label || 'label'; + const values = oneClass.values; + const conditions: Filter[] = []; + + values + .filter((value) => 'fieldValues' in value) + .forEach((value) => { + const fieldValues = value.fieldValues; + let condition = equalFilter(fields[0], fieldValues[0], toLowerCase); for (const [fieldValue, fieldName] of fieldValues .slice(1) .map((fv: unknown, idx: number) => [fv, fields[idx + 1]])) { - condition = and(condition, equal(fieldName, fieldValue)); + condition = andFilter([condition, equalFilter(`${fieldName}`, `${fieldValue}`, toLowerCase)]); } conditions.push(condition); - } - } + }); - if (conditions.length) { - ruleFilter = conditions.length === 1 ? conditions[0] : or(conditions); - rule.filter = ruleFilter; - rule.symbolizers = processSymbolReference(clazz.symbol, options); + let ruleFilter: Filter | null = null; + if (conditions.length) { + ruleFilter = conditions.length === 1 ? conditions[0] : orFilter(conditions); + const rule: Rule = { + name, + filter: ruleFilter, + symbolizers: processSymbolReference(oneClass.symbol, options), + }; const scaleDenominator = processScaleDenominator( - clazz.symbol.minScale, - clazz.symbol.maxScale + oneClass.symbol.minScale, + oneClass.symbol.maxScale ); if (scaleDenominator) { rule.scaleDenominator = scaleDenominator; } rules.push(rule); } - - for (const symbolRef of clazz.alternateSymbols || []) { - const altRule: Rule = { name: rule.name }; + const alternateSymbols = oneClass.alternateSymbols || []; + alternateSymbols.forEach((symbolRef) => { + const altRule: Rule = { + name, + symbolizers: processSymbolReference(symbolRef, options) + }; if (ruleFilter) { altRule.filter = ruleFilter; } - altRule.symbolizers = processSymbolReference(symbolRef, options); - const scaleDenominator = processScaleDenominator( symbolRef.minScale, symbolRef.maxScale @@ -367,26 +383,27 @@ const processUniqueValueGroup = ( altRule.scaleDenominator = scaleDenominator; } rules.push(altRule); - } - } + }); + }); return rules; }; const getSymbolRotationFromVisualVariables = ( - renderer: any, + renderer: CIMRenderer | null, toLowerCase: boolean -) => { +): GeoStylerNumberFunction | null => { const visualVariables = renderer?.visualVariables ?? []; - for (const visualVariable of visualVariables) { - if (visualVariable.type === 'CIMRotationVisualVariable') { - const expression = - visualVariable.visualVariableInfoZ?.valueExpressionInfo?.expression || - visualVariable.visualVariableInfoZ?.expression; - const rotationType = visualVariable.rotationTypeZ; - return processRotationExpression(expression, rotationType, toLowerCase); + visualVariables.find(visualVariable => { + if (visualVariable.type !== 'CIMRotationVisualVariable') { + return false; } - } + const expression = + visualVariable.visualVariableInfoZ?.valueExpressionInfo?.expression || + visualVariable.visualVariableInfoZ?.expression; + const rotationType = visualVariable.rotationTypeZ; + return processRotationExpression(expression, rotationType, toLowerCase); + }); return null; }; diff --git a/src/toGeostylerUtils.ts b/src/toGeostylerUtils.ts index d4634cc..419e31d 100644 --- a/src/toGeostylerUtils.ts +++ b/src/toGeostylerUtils.ts @@ -1,6 +1,7 @@ -import { ptToPx } from './constants.ts'; -import { processColor, processOpacity } from './processUtils.ts'; -import { WellKnownName } from 'geostyler-style'; +import {ptToPx} from './constants.ts'; +import {processColor, processOpacity} from './processUtils.ts'; +import {CIMTextSymbol, SymbolLayer} from './esri/types/symbols'; +import {WellKnownName} from 'geostyler-style'; export const WARNINGS: string[] = []; @@ -33,22 +34,25 @@ export const esriFontToStandardSymbols = (charIndex: number): WellKnownName => { }; export const ptToPxProp = ( - obj: { - [key: string]: any; - }, + obj: unknown, prop: string, defaultValue: number, asFloat: boolean = true ): number => { - if (obj[prop] === undefined) { + if (!(obj !== null && typeof obj === 'object' && obj.hasOwnProperty(prop))) { return defaultValue; } - let value = ptToPx(parseFloat(obj[prop])); + const validObj = obj as Record; + const rawValue = parseFloat(validObj[prop] as string); + if (isNaN(rawValue)) { + return defaultValue; + } + const value = ptToPx(rawValue); return asFloat ? value : Math.round(value); }; export const extractStroke = ( - symbolLayers: any[] + symbolLayers: SymbolLayer[] ): [string, number, number] => { for (let sl of symbolLayers) { if (sl.type === 'CIMSolidStroke') { @@ -61,27 +65,34 @@ export const extractStroke = ( return ['#000000', 0, 0]; }; -export const extractFillColor = (symbolLayers: any[]): string => { +export const extractFillColor = (symbolLayers: SymbolLayer[]): string => { let color: string = '#ffffff'; - for (let sl of symbolLayers) { + symbolLayers.some(sl => { + if (!sl.type) { + return false; + } if (sl.type === 'CIMSolidFill') { color = processColor(sl.color ?? ''); + return true; } else if (sl.type === 'CIMCharacterMarker') { - color = extractFillColor(sl.symbol.symbolLayers); + if (sl.symbol.symbolLayers) { + color = extractFillColor(sl.symbol.symbolLayers); + return true; + } } - } + return false; + }); return color; }; -export const extractFillOpacity = (symbolLayers: any[]): number => { - for (let sl of symbolLayers) { - if (sl.type === 'CIMSolidFill') { - return processOpacity(sl.color); - } +export const extractFillOpacity = (symbolLayers: SymbolLayer[]): number => { + const symbolLayer = symbolLayers.find(sl => sl.type === 'CIMSolidFill'); + if (symbolLayer) { + return processOpacity(symbolLayer.color); } return 1.0; }; -export const extractFontWeight = (textSymbol: any): string => { +export const extractFontWeight = (textSymbol: CIMTextSymbol): ('bold'|'normal') => { return textSymbol.fontStyleName === 'Bold' ? 'bold' : 'normal'; }; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d210946 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,10 @@ +export interface Options { + [key: string]: unknown; + toLowerCase?: boolean; +} + +export interface Effect { + dasharrayValues?: number[]; + dasharray?: number[]; + offset?: number; +} diff --git a/src/wktGeometries.ts b/src/wktGeometries.ts index ab6b4fe..4c8de1f 100644 --- a/src/wktGeometries.ts +++ b/src/wktGeometries.ts @@ -1,10 +1,5 @@ import {WellKnownName} from 'geostyler-style'; - -type Geometry = { - rings?: number[][][]; - paths?: number[][][]; - curveRings?: { a?: number[][]; c?: number[][] }[][]; -}; +import {Geometry} from './esri/types'; export const toWKT = (geometry: Geometry): { wellKnownName: WellKnownName; maxX?: number; maxY?: number } => { const defaultMarker = {wellKnownName: 'circle' as WellKnownName}; @@ -32,7 +27,9 @@ export const toWKT = (geometry: Geometry): { wellKnownName: WellKnownName; maxX? if (!curve) {return defaultMarker;} const endPoint = curve[0]; const centerPoint = curve[1]; - if (endPoint !== startPoint) {return defaultMarker;} + if (endPoint !== startPoint) { + return defaultMarker; + } const radius = distanceBetweenPoints(startPoint as number[], centerPoint); return { wellKnownName: 'circle' as WellKnownName,