Skip to content

Commit

Permalink
fix: make the QGISStyleParser write either old or new QML
Browse files Browse the repository at this point in the history
  • Loading branch information
olsen232 authored and KaiVolland committed Dec 12, 2024
1 parent 57acf11 commit 2a4e919
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 98 deletions.
2 changes: 1 addition & 1 deletion data/qmls_old/line_simple.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="QGIS Simple Symbol"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/no_symbolizer.qml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="nullSymbol"/>
</qgis>
2 changes: 1 addition & 1 deletion data/qmls_old/point_external_graphic.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="QGIS Simple Symbol"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/point_label.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="nullSymbol"/>
<labeling type="rule-based">
<rules key="labeling_rules">
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/point_multiple_symbols.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="QGIS Simple Symbol"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/point_ranges.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 forceraster="0" graduatedMethod="GraduatedColor" type="graduatedSymbol" attr="PlotNr" symbollevels="0" enableorderby="0">
<ranges>
<range symbol="0" upper="20.000000000000000" label=" 1,0000 - 20,0000 " lower="1.000000000000000" render="true"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/point_rules.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="Bildpositi = 1" scalemindenom="100" scalemaxdenom="2000" filter="Bildpositi = 1"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/point_simple.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="QGIS Simple Symbol"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/polygon_simple.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="QGIS Simple Symbol"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/polygon_simple_nostyle.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="RuleRenderer">
<rules key="renderer_rules">
<rule key="renderer_rule_0" symbol="0" label="QGIS Simple Symbol"/>
Expand Down
2 changes: 1 addition & 1 deletion data/qmls_old/text_text_buffer.qml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis>
<qgis version="3.22.16-Białowieża">
<renderer-v2 type="nullSymbol"/>
<labeling type="rule-based">
<rules key="labeling_rules">
Expand Down
148 changes: 76 additions & 72 deletions src/QGISStyleParser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ describe('QMLStyleParser implements StyleParser', () => {
let styleParser: QGISStyleParser;

beforeEach(() => {
styleParser = new QGISStyleParser();
styleParser = (expect.getState().currentTestName.includes('>=3.28.0'))
? new QGISStyleParser()
: new QGISStyleParser({qgisVersion: '3.22.16-Białowieża'});
});

const QML_FOLDERS = [
Expand Down Expand Up @@ -120,83 +122,85 @@ describe('QMLStyleParser implements StyleParser', () => {
});
});

describe('#writeStyle', () => {
it('is defined', () => {
expect(styleParser.writeStyle).toBeDefined();
});
describe('PointSymbolizer', () => {
it('can write a simple QML PointSymbol', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/point_simple.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_simple);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
it('can write a QML PointSymbolizer with an external graphic', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/point_external_graphic.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_external_graphic);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
it('can write a QML PointSymbolizer with multiple symbols', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/point_multiple_symbols.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_multiple_symbols);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
});
describe('TextSymbolizer', () => {
it('can write some basics of the QML Labeling for Points', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/point_label.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_label);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
QML_FOLDERS.forEach(qmlVersionFolder => {
const [qmlVersion, qmlFolder] = qmlVersionFolder;
describe(`#writeStyle ${qmlVersion}`, () => {
it('is defined', () => {
expect(styleParser.writeStyle).toBeDefined();
});
it('can write QML with text-buffer', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/text_text_buffer.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(text_text_buffer);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
describe('PointSymbolizer', () => {
it('can write a simple QML PointSymbol', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/point_simple.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_simple);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
it('can write a QML PointSymbolizer with an external graphic', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/point_external_graphic.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_external_graphic);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
it('can write a QML PointSymbolizer with multiple symbols', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/point_multiple_symbols.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_multiple_symbols);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
});
});
describe('LineSymbolizer', () => {
it('can write a simple QML LineSymbol', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/line_simple.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(line_simple);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
describe('TextSymbolizer', () => {
it('can write some basics of the QML Labeling for Points', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/point_label.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_label);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
it('can write QML with text-buffer', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/text_text_buffer.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(text_text_buffer);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
});
});
describe('FillSymbolizer', () => {
it('can write a simple QML FillSymbol', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/polygon_simple.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(polygon_simple);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
describe('LineSymbolizer', () => {
it('can write a simple QML LineSymbol', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/line_simple.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(line_simple);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
});
});
describe('Filter Parsing', () => {
it('can write a rule based QML PointSymbolizer', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/point_rules.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_rules);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
describe('FillSymbolizer', () => {
it('can write a simple QML FillSymbol', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/polygon_simple.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(polygon_simple);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
});
it('can write QML with no symbolizers', async () => {
expect.assertions(2);
const qml = fs.readFileSync('./data/qmls/no_symbolizer.qml', 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(no_symbolizer);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
describe('Filter Parsing', () => {
it('can write a rule based QML PointSymbolizer', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/point_rules.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(point_rules);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
it('can write QML with no symbolizers', async () => {
expect.assertions(2);
const qml = fs.readFileSync(`./data/${qmlFolder}/no_symbolizer.qml`, 'utf8');
const { output: qgisStyle } = await styleParser.writeStyle(no_symbolizer);
expect(qgisStyle).toBeDefined();
expect(qgisStyle).toEqual(qml.trim());
});
});
});
});

});
74 changes: 59 additions & 15 deletions src/QGISStyleParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ import {
Builder
} from 'xml2js';

const OUTPUT_VERSION = '3.28.0-Firenze';

const get = (obj: any, path: any, defaultValue = undefined) => {
const travel = (regexp: RegExp) =>
String.prototype.split
Expand All @@ -36,6 +34,10 @@ const get = (obj: any, path: any, defaultValue = undefined) => {
return result === undefined || result === obj ? defaultValue : result;
};

export type ConstructorParams = {
qgisVersion?: string;
};

type SymbolizerMap = {
[key: string]: Symbolizer[];
};
Expand Down Expand Up @@ -130,6 +132,27 @@ export class QGISStyleParser implements StyleParser {

title = 'QGIS Style Parser';

qgisVersion: string;

// QGIS 3.28 and later use <Option> instead of <prop> to serialize individual properties.
writePropsAsOptions: boolean;

constructor(opts: ConstructorParams = {}) {
this.qgisVersion = opts.qgisVersion ?? '3.28.0-Firenze';
this.writePropsAsOptions = this.compareVersions('3.28.0-Firenze', this.qgisVersion) >= 0;
}

compareVersions(version1: string, version2: string): number {
try {
const parseVersion = (version: string) => version.split('-')[0].split('.').map(Number);
const [major1, minor1, patch1] = parseVersion(version1);
const [major2, minor2, patch2] = parseVersion(version2);
return Math.sign(major2 - major1) || Math.sign(minor2 - minor1) || Math.sign(patch2 - patch1);
} catch (error) {
return 0;
}
}

/**
* The readStyle implementation of the GeoStyler-Style StyleParser interface.
* It reads a QML as a string and returns a Promise containing the
Expand Down Expand Up @@ -837,7 +860,7 @@ export class QGISStyleParser implements StyleParser {
$: {
class: 'SimpleLine'
},
Option: this.propsObjectToQmlSymbolOptions(qmlProps)
...this.propsObjectToQmlSymbolOptions(qmlProps)
};
}

Expand Down Expand Up @@ -869,7 +892,7 @@ export class QGISStyleParser implements StyleParser {
$: {
class: 'SimpleFill'
},
Option: this.propsObjectToQmlSymbolOptions(qmlProps)
...this.propsObjectToQmlSymbolOptions(qmlProps)
};
}

Expand Down Expand Up @@ -919,7 +942,7 @@ export class QGISStyleParser implements StyleParser {
$: {
class: 'SvgMarker'
},
Option: this.propsObjectToQmlSymbolOptions(qmlProps)
...this.propsObjectToQmlSymbolOptions(qmlProps)
};
}

Expand Down Expand Up @@ -957,15 +980,21 @@ export class QGISStyleParser implements StyleParser {
$: {
class: 'SimpleMarker'
},
Option: this.propsObjectToQmlSymbolOptions(qmlProps)
...this.propsObjectToQmlSymbolOptions(qmlProps)
};
}

/**
*
* @param properties
* @param {object} properties Object containing key-value pairs to serialize as QML.
* @return an XML object representing either <Option type="map"><repeated Option /></Option> or <repeated prop... />
*/
propsObjectToQmlSymbolOptions(properties: any): QmlMapOption {
propsObjectToQmlSymbolOptions(properties: any): {Option: QmlMapOption} | {prop: QmlProp[]} {
return this.writePropsAsOptions
? this.propsObjectToQmlSymbol_NewerOptions(properties)
: this.propsObjectToQmlSymbol_OlderProps(properties);
}

propsObjectToQmlSymbol_NewerOptions(properties: any): {Option: QmlMapOption} {
const options = Object.keys(properties).map(k => {
const v = properties[k];
return {
Expand All @@ -977,13 +1006,28 @@ export class QGISStyleParser implements StyleParser {
};
}).filter(o => o.$.value !== undefined);
return {
$: {
type: 'Map'
},
Option: options
Option: {
$: {
type: 'Map'
},
Option: options
}
};
}

propsObjectToQmlSymbol_OlderProps(properties: any): {prop: QmlProp[]} {
const props = Object.keys(properties).map(k => {
const v = properties[k];
return {
$: {
k: k,
v: v,
}
};
}).filter(p => p.$.v !== undefined);
return {prop: props};
}

/**
* Get the QML Object (readable with xml2js) from an GeoStyler-Style Style
*
Expand All @@ -998,7 +1042,7 @@ export class QGISStyleParser implements StyleParser {
return {
qgis: {
$: {
version: OUTPUT_VERSION
version: this.qgisVersion
},
'renderer-v2': [{
$: {
Expand All @@ -1020,7 +1064,7 @@ export class QGISStyleParser implements StyleParser {
return {
qgis: {
$: {
version: OUTPUT_VERSION
version: this.qgisVersion
},
'renderer-v2': [{
$: {
Expand Down

0 comments on commit 2a4e919

Please sign in to comment.