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,