├── .github └── workflows │ └── release.yml ├── .gitignore ├── CHANGES.md ├── CONTRIBUTING.md ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── docs ├── arcgis.md ├── arcgis_chart_example.png ├── mapserver.md └── qgis.md ├── pyproject.toml ├── src └── bridgestyle │ ├── __init__.py │ ├── arcgis │ ├── __init__.py │ ├── constants.py │ ├── expressions.py │ ├── fromgeostyler.py │ ├── togeostyler.py │ └── wkt_geometries.py │ ├── geostyler │ ├── __init__.py │ └── custom_properties.py │ ├── mapboxgl │ ├── __init__.py │ ├── fromgeostyler.py │ └── togeostyler.py │ ├── mapserver │ ├── __init__.py │ ├── fromgeostyler.py │ └── togeostyler.py │ ├── qgis │ ├── __init__.py │ ├── expressions.py │ ├── fromgeostyler.py │ └── togeostyler.py │ ├── sld │ ├── __init__.py │ ├── fromgeostyler.py │ ├── parsecdata.py │ ├── togeostyler.py │ └── transformations.py │ ├── style2style.py │ └── version.py └── test ├── arcgistogeostyler.py ├── context.py ├── data ├── arcgis │ ├── Cartographic Line.lyrx │ ├── Cities.lyrx │ ├── Countries.lyrx │ ├── Hash Line.lyrx │ ├── Marker Line.lyrx │ └── test.lyrx ├── qgis │ ├── line │ │ ├── basic.geostyler │ │ ├── basic.qml │ │ ├── labeloffset.geostyler │ │ ├── labeloffset.qml │ │ └── testlayer.gpkg │ ├── points │ │ ├── categorized.qml │ │ ├── else_rule.qml │ │ ├── fontmarker.geostyler │ │ ├── fontmarker.qml │ │ ├── fontmarker2chars.geostyler │ │ ├── fontmarker2chars.qml │ │ ├── heatmap.qml │ │ ├── heatmap_color.qml │ │ ├── honeycomb.qml │ │ ├── label_buffer.qml │ │ ├── labels_offset.qml │ │ ├── map_units.qml │ │ ├── nosymbols.geostyler │ │ ├── nosymbols.qml │ │ ├── offset.geostyler │ │ ├── offset.qml │ │ ├── rotated_font_marker_and_circle.qml │ │ ├── rotated_svg.qml │ │ ├── rule_no_filter.qml │ │ ├── simplemarker.geostyler │ │ ├── simplemarker.qml │ │ ├── size_based_on_expression.qml │ │ ├── size_based_on_property.qml │ │ ├── testlayer.gpkg │ │ ├── transparentfill.geostyler │ │ └── transparentfill.qml │ └── single_polygon │ │ ├── geometry_generator_centroid.qml │ │ ├── outline_simple_line.qml │ │ ├── outline_simple_line_with_offset.qml │ │ ├── point_pattern_fill.qml │ │ ├── point_pattern_fill_svg.qml │ │ ├── simple.geostyler │ │ ├── simple.qml │ │ ├── simple_cross_hatch.qml │ │ ├── simple_dash_outline.geostyler │ │ ├── simple_dash_outline.qml │ │ ├── simple_empty_fill.geostyler │ │ ├── simple_empty_fill.qml │ │ ├── simple_hairstyle_width.geostyler │ │ ├── simple_hairstyle_width.qml │ │ ├── simple_horline_fill.geostyler │ │ ├── simple_horline_fill.qml │ │ ├── simple_noborder.geostyler │ │ ├── simple_noborder.qml │ │ ├── simple_nofill.geostyler │ │ ├── simple_nofill.qml │ │ ├── testlayer.gpkg │ │ └── two_simple_fill_symbol_layers.qml └── sample.geostyler └── qgistogeostyler.py /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish 2 | 3 | # Run on PR requests. And on master itself. 4 | on: 5 | push: 6 | branches: 7 | - main # just build the sdist skip release 8 | tags: 9 | - "*" 10 | pull_request: # also build on PRs touching some files 11 | paths: 12 | - ".github/workflows/release.yml" 13 | - "MANIFEST.in" 14 | - "pyproject.toml" 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout source 23 | uses: actions/checkout@v4 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: "3.13" 29 | 30 | - name: Build a source tarball 31 | run: | 32 | python -m pip install build 33 | python -m build 34 | 35 | - uses: actions/upload-artifact@v4 36 | with: 37 | path: ./dist/* 38 | retention-days: 5 39 | 40 | publish: 41 | name: Publish on GitHub and PyPI 42 | needs: [build] 43 | runs-on: ubuntu-latest 44 | # release on every tag 45 | if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/') 46 | steps: 47 | - uses: actions/download-artifact@v4 48 | with: 49 | name: artifact 50 | path: dist 51 | 52 | - name: Upload Github release 53 | id: upload-release-asset 54 | uses: softprops/action-gh-release@v2 55 | 56 | - name: Upload Release Assets to PyPI 57 | uses: pypa/gh-action-pypi-publish@release/v1 58 | with: 59 | password: ${{ secrets.PYPI_UPLOAD_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # PyCharm 107 | .idea/ 108 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Changelog of bridge-style 2 | ========================= 3 | 4 | ## 0.1.3 (unreleased) 5 | --------------------- 6 | 7 | - Use `pyproject.toml` instead of `setup.py` 8 | 9 | - Use src layout instead of flat layout 10 | 11 | - Make package PyPI-compatible 12 | 13 | - Add GH Actions release workflow 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | h1. Contributing to Bridge Style 2 | 3 | Thank you! Taking part is the only way open source works for everyone. 4 | 5 | By making a contribution you understand that: 6 | 7 | * *bridge-style* is open source, distributed with an [MIT License](License) 8 | * Library documentation also open, distributed with a [Creative Commons Attribution License](https://creativecommons.org/licenses/by/4.0/) 9 | 10 | h2. Release Notes 11 | 12 | We would like to be sure to thank you in the release notes. 13 | 14 | Issues asked to to have an developer focused title: 15 | 16 | * Good: MapBox Style line-offset skipped for None (and default of 0) 17 | * Poor: mapbox style fixes 18 | 19 | H2. Fixes 20 | 21 | Bug fixes sholuld be covered by a test-case: 22 | 23 | * In many cases you can quickly add an addiitonal check to an existing test-case. 24 | * Double check any example styles (it is easy to accidentially include sensitve information) 25 | 26 | h2. Features 27 | 28 | If you are continbuting a new feature, or output format, please add *any* documentation describing your change: 29 | 30 | * Quickly adding an example to an existing page works well for existing formats 31 | * Feel free to make a new page when adding an output format or capability 32 | 33 | Because this is a cartogrpahic library visual examples are encouraged. 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2019 GeoCat bv 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Don't include tests since they do not work, just src and docs 2 | graft src 3 | graft docs 4 | 5 | # Exclude byte-compiled code 6 | prune __pycache__ 7 | recursive-exclude * *.py[co] 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bridge-style 2 | 3 | A Python library to convert map styles between multiple formats. 4 | 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md) 6 | 7 | The library uses [GeoStyler](https://geostyler.org/) as an intermediate format, and uses a two-step approach: 8 | 9 | 1. Converting from the original format into the GeoStyler format. 10 | 2. Converting from GeoStyler into a supported destination format. 11 | 12 | 13 | ## Supported formats 14 | 15 | These are the formats currently supported: 16 | 17 | - GeoStyler 18 | - SLD (with GeoServer vendor options) 19 | - MapLibre GL JS 20 | - Mapfile (for MapServer) 21 | - ArcGIS Pro CIM (.lyrx) 22 | 23 | The library can also be run from GIS applications, so it can convert from the data objects corresponding to map layer and features in those applications via GeoStyler into something else. 24 | If you wish to use the library in QGIS, we recommend using the [QGIS Bridge](https://github.com/GeoCat/qgis-bridge-plugin) plugin. ArcGIS Pro users can create a Python toolbox that utilizes `bridgestyle` either directly (requires installation so you can import it) or by calling `style2style` (CLI) using `subprocess` for example. 25 | 26 | So far, all formats can be exported from QGIS, but the inverse conversion is not available for all of them. The same applies to ArcGIS Pro styles (.lyrx). 27 | To see which QGIS symbology features are correctly converted to GeoStyler and supported by the other target formats, see [this document](docs/qgis.md). 28 | 29 | ## Example usage 30 | 31 | Here is an example of how to export the symbology of the currently selected QGIS layer into a zip file containing an SLD style file and all the icons files (SVG, PNG, etc.) used by the layer. 32 | 33 | ```python 34 | from bridgestyle.qgis import saveLayerStyleAsZippedSld 35 | warnings = saveLayerStyleAsZippedSld(iface.activeLayer(), "/my/path/mystyle.zip") 36 | ``` 37 | 38 | The `warnings` variable will contain a list of strings with the issues found during the conversion. 39 | 40 | Conversion can be performed outside of QGIS, just using the library as a standalone element. Each format has its own Python package, which contains two modules: `togeostyler` and `fromgeostyler`, each of them with a `convert` method to do the conversion work. It returns the converted style as a string, and a list of strings with issues found during the conversion (such as unsupported symbology elements that could not be correctly converted). 41 | 42 | Here's, for instance, how to convert a GeoStyler file into a SLD file. 43 | 44 | ```python 45 | from bridgestyle import sld 46 | input_file = "/my/path/input.geostyler" 47 | output_file = "/my/path/output.sld" 48 | 49 | # We load the GeoStyler code from the input file 50 | with open(input_file) as f: 51 | geostyler = json.load(f) 52 | 53 | ''' 54 | We pass it to the fromgeostyler.convert method from the sld package. 55 | There is one such module and function for each supported format, which 56 | takes a Python object representing the GeoStyler json object and returns 57 | a string with the style in the destination format 58 | ''' 59 | converted, warnings, obj = sld.fromgeostyler.convert(geostyler) 60 | 61 | # We save the resulting string in the destination file 62 | with open(output_file) as f: 63 | f.write(f) 64 | ``` 65 | 66 | A command line tool (CLI) is also available. When the library is installed in your Python installation, you will have a `style2style` script available to be run in your console, with the following syntax: 67 | 68 | ``` 69 | style2style original_style_file.ext destination_style_file.ext 70 | ``` 71 | 72 | The file format is inferred from the file extension. 73 | 74 | The example conversion shown above would be run with the console tool as follows: 75 | 76 | ``` 77 | style2style /my/path/input.geostyler /my/path/output.sld 78 | ``` 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /docs/arcgis.md: -------------------------------------------------------------------------------- 1 | # ArcGIS CIMChartRenderer 2 | 3 | **Added**: support for ArcGIS **CIMChartRenderer** 4 | (pie, bar, stacked-bar). The renderer is converted to a GeoStyler **Icon** 5 | symbolizer and exported back to SLD as an `` with 6 | `Format="application/chart"`. 7 | 8 | *Note*: by default, sld doesn't support chart styling. 9 | To support charts, GeoServer allows the installation of a chart extension. 10 | Without the extension, the generated styles won't be displayed on the map. 11 | For more information, please read the following documentation: 12 | https://docs.geoserver.geo-solutions.it/edu/en/pretty_maps/charting.html 13 | 14 | > Example result in GeoServer for a Pie Chart after conversion: 15 | 16 | 17 | 18 | gidulim 19 | 20 | gidulim 21 | 22 | 23 | Chart 24 | Chart 25 | 26 | 27 | 28 | 29 | application/chart 30 | 31 | 1.0 32 | 32 33 | 0 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ![Pie chart example](arcgis_chart_example.png) 43 | -------------------------------------------------------------------------------- /docs/arcgis_chart_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoCat/bridge-style/fef7b784b1630bbe21b3691fc1709860d6472747/docs/arcgis_chart_example.png -------------------------------------------------------------------------------- /docs/mapserver.md: -------------------------------------------------------------------------------- 1 | # Mapserver support in bridge style 2 | 3 | Bridge-style is able to create layer definitions for [mapserver](https://mapserver.org) mapfiles. 4 | Use bridge-style to convert geostyler to mapfile syntax and combine the output with a mapfile 5 | header/footer and run it in mapserver 6 | 7 | ## Mapserver on Windows 8 | 9 | For windows you best install [MS4W](https://www.ms4w.com). MS4W installs mapserver and apache preconfigured to instantly run. 10 | 11 | ## Mapserver on Docker 12 | 13 | Various [prepared images](https://hub.docker.com/r/mapserver/mapserver) are available on Docker hub. 14 | 15 | ## Mapserver on Apple 16 | 17 | Mapserver is available via Homebrew (requires python). Try: 18 | 19 | ``` 20 | brew install mapserver 21 | ``` 22 | 23 | ## Running mapserver from command line 24 | 25 | Mapserver offers some capabilities via command line. You can run from command line for example: 26 | 27 | ``` 28 | mapserv QUERY_STRING="map=example.map&service=wms&request=getcapabilities" 29 | ``` 30 | 31 | Add the -nh (no header) switch if you want to pipe the result of a getmap request to an image 32 | 33 | ``` 34 | mapserv QUERY_STRING="SERVICE=WMS&REQUEST=GetMap&VERSION=1.3.0&service=WMS&layers=definition&bbox=4.70,51.89,4.71,51.90&width=800&height=800&map=groups.map&crs=epsg:4326&format=image/png" -nh > dummy.png 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/qgis.md: -------------------------------------------------------------------------------- 1 | # QGIS supported functionality 2 | 3 | The following elements are supported and correctly exported to the Geostyler format (and from there to any other format, if it also supports that kind of element): 4 | 5 | ## Renderers 6 | 7 | Most renderers and elements are supported. Here is a short list of the main ones not supported yet: 8 | 9 | - Shapeburst fills 10 | - Point placement 11 | - Point clusters 12 | - Heatmaps 13 | - Vector field markers 14 | - Line pattern polygon fills (use point pattern fills with lines or crosses as markers for a similar effect) 15 | 16 | ## Geometry generators 17 | 18 | Geoetry generators are supported, although not all geometry functions are available (see 'Expressions') 19 | 20 | 21 | ## size units 22 | 23 | Size values can be used in milimeters, pixels or real world meters. In this last case, expressions cannot be used, only fixed values. 24 | 25 | Notice that it's, however, a safer option to use pixels instead of milimeters (which are the default unit in QGIS), since pixels is the assumed unit for formats like SLD, and, therefore, no conversion is needed. 26 | 27 | ## Expressions 28 | 29 | Expressions are supported whenever they are available in QGIS. Not all functions and operators are supported. See [here](qgisfunctions.md) for a list of supported ones. 30 | 31 | There are two exceptions to this: 32 | 33 | - Expressions are not supported for color values 34 | - Expressions are not supported for size measurements, when those measures are not expressed in pixels or mm (that is, if you are using map units or real word meters for a size that changes with the current map scale) -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools", 4 | ] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "bridgestyle" 9 | authors = [ 10 | {name = "GeoCat BV", email = "bridge@geocat.net"}, 11 | ] 12 | description = "A Python library to convert between different map style formats" 13 | keywords = ["GeoCat", "Bridge", "style", "symbology", "styling", "mapping", "SLD", "cartography", 14 | "Mapfile", "CIM", "GeoStyler", "Mapbox", "QGIS", "GeoServer", "Esri"] 15 | license = "MIT" 16 | readme = "README.md" 17 | dynamic = ["version"] 18 | 19 | [project.urls] 20 | Repository = "https://github.com/GeoCat/bridge-style" 21 | 22 | [project.scripts] 23 | style2style = "bridgestyle.style2style:main" 24 | 25 | [tool.setuptools.dynamic] 26 | version = {attr = "bridgestyle.__version__"} 27 | 28 | [tool.zest-releaser] 29 | python-file-with-version = "src/bridgestyle/version.py" 30 | history-file = "CHANGES.md" 31 | release = false 32 | upload-pypi = false 33 | -------------------------------------------------------------------------------- /src/bridgestyle/__init__.py: -------------------------------------------------------------------------------- 1 | from .version import __version__ -------------------------------------------------------------------------------- /src/bridgestyle/arcgis/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from . import fromgeostyler 4 | from . import togeostyler 5 | 6 | 7 | def toGeostyler(style, options=None): 8 | return togeostyler.convert(json.loads(style), options) 9 | 10 | 11 | def fromGeostyler(style, options=None): 12 | return fromgeostyler.convert(style, options) 13 | 14 | -------------------------------------------------------------------------------- /src/bridgestyle/arcgis/constants.py: -------------------------------------------------------------------------------- 1 | ESRI_SYMBOLS_FONT = "ESRI Default Marker" 2 | PT_TO_PX_FACTOR = 4/3 3 | POLYGON_FILL_RESIZE_FACTOR = 2/3 4 | OFFSET_FACTOR = 4/3 5 | 6 | def pt_to_px(pt): 7 | return pt * PT_TO_PX_FACTOR 8 | -------------------------------------------------------------------------------- /src/bridgestyle/arcgis/expressions.py: -------------------------------------------------------------------------------- 1 | # For now, this is limited to compound labels using the python, VB or Arcade syntax 2 | from ..geostyler.custom_properties import WellKnownText 3 | 4 | 5 | def convertExpression(expression, engine, tolowercase): 6 | if engine == "Arcade": 7 | expression = convertArcadeExpression(expression) 8 | if tolowercase: 9 | expression = expression.lower() 10 | if "+" in expression or "&" in expression: 11 | if "+" in expression: 12 | tokens = expression.split("+")[::-1] 13 | else: 14 | tokens = expression.split("&")[::-1] 15 | addends = [] 16 | for token in tokens: 17 | if "[" in token: 18 | addends.append( 19 | ["PropertyName", processPropertyName(token)] 20 | ) 21 | else: 22 | literal = token.replace('"', "") 23 | addends.append(replaceSpecialLiteral(literal)) 24 | allOps = addends[0] 25 | for attr in addends[1:]: 26 | allOps = ["Concatenate", attr, allOps] 27 | expression = allOps 28 | else: 29 | expression = ["PropertyName", processPropertyName(expression)] 30 | return expression 31 | 32 | 33 | def replaceSpecialLiteral(literal): 34 | if literal == "vbnewline": 35 | return WellKnownText.NEW_LINE 36 | return literal 37 | 38 | 39 | def processPropertyName(token): 40 | return token.replace("[", "").replace("]", "").strip() 41 | 42 | 43 | def convertArcadeExpression(expression): 44 | return expression.replace("$feature.", "") 45 | 46 | 47 | def stringToParameter(s, tolowercase): 48 | s = s.strip() 49 | if "'" in s or '"' in s: 50 | return s.strip("'\"") 51 | else: 52 | if s.isalpha(): 53 | if tolowercase: 54 | s = s.lower() 55 | return ["PropertyName", s] 56 | else: 57 | return s 58 | 59 | 60 | # For now, limited to = or IN statements 61 | # There is no formal parsing, just a naive conversion 62 | def convertWhereClause(clause, tolowercase): 63 | clause = clause.replace("(", "").replace(")", "") 64 | if " AND " in clause: 65 | expression = ["And"] 66 | subexpressions = [s.strip() for s in clause.split(" AND ")] 67 | expression.extend([convertWhereClause(s, tolowercase) 68 | for s in subexpressions]) 69 | return expression 70 | if "=" in clause: 71 | tokens = [t.strip() for t in clause.split("=")] 72 | expression = [ 73 | "PropertyIsEqualTo", 74 | stringToParameter(tokens[0], tolowercase), 75 | stringToParameter(tokens[1], tolowercase), 76 | ] 77 | return expression 78 | if "<>" in clause: 79 | tokens = [t.strip() for t in clause.split("<>")] 80 | expression = [ 81 | "PropertyIsNotEqualTo", 82 | stringToParameter(tokens[0], tolowercase), 83 | stringToParameter(tokens[1], tolowercase), 84 | ] 85 | return expression 86 | if ">" in clause: 87 | tokens = [t.strip() for t in clause.split(">")] 88 | expression = [ 89 | "PropertyIsGreaterThan", 90 | stringToParameter(tokens[0], tolowercase), 91 | stringToParameter(tokens[1], tolowercase), 92 | ] 93 | return expression 94 | elif " in " in clause.lower(): 95 | clause = clause.replace(" IN ", " in ") 96 | tokens = clause.split(" in ") 97 | attribute = tokens[0] 98 | values = tokens[1].strip("() ").split(",") 99 | subexpressions = [] 100 | for v in values: 101 | subexpressions.append( 102 | [ 103 | "PropertyIsEqualTo", 104 | stringToParameter(attribute, tolowercase), 105 | stringToParameter(v, tolowercase), 106 | ] 107 | ) 108 | expression = [] 109 | if len(values) == 1: 110 | return subexpressions[0] 111 | else: 112 | accum = ["Or", subexpressions[0], subexpressions[1]] 113 | for subexpression in subexpressions[2:]: 114 | accum = ["Or", accum, subexpression] 115 | return accum 116 | 117 | return clause 118 | 119 | 120 | def processRotationExpression(expression, rotationType, tolowercase): 121 | if "$feature" in expression: 122 | field = convertArcadeExpression(expression) 123 | else: 124 | field = processPropertyName(expression) 125 | propertyNameExpression = ["PropertyName", 126 | field.lower() if tolowercase else field] 127 | if rotationType == "Arithmetic": 128 | return [ 129 | "Mul", 130 | propertyNameExpression, 131 | -1, 132 | ] 133 | elif rotationType == "Geographic": 134 | return [ 135 | "Sub", 136 | propertyNameExpression, 137 | 90, 138 | ] 139 | -------------------------------------------------------------------------------- /src/bridgestyle/arcgis/fromgeostyler.py: -------------------------------------------------------------------------------- 1 | 2 | def convert(geostyler, options=None): 3 | return {}, [] # (dictionary with ArcGIS Pro style, list of warnings) 4 | -------------------------------------------------------------------------------- /src/bridgestyle/arcgis/wkt_geometries.py: -------------------------------------------------------------------------------- 1 | def to_wkt(geometry): 2 | if geometry.get("rings"): 3 | rings = geometry["rings"][0] 4 | coordinates = ", ".join([" ".join([str(i) for i in j]) for j in rings]) 5 | return { 6 | "wellKnownName": f"wkt://POLYGON(({coordinates}))", 7 | "maxX": max([coord[0] for coord in rings]), 8 | "maxY": max([coord[1] for coord in rings]), 9 | } 10 | # The following corresponds to the geometry of the line symbolizer in ArcGIS 11 | elif geometry.get("paths") and geometry["paths"][0] == [[2, 0], [-2, 0]]: 12 | return {"wellKnownName": "wkt://MULTILINESTRING((0 2, 0 0))"} 13 | return {"wellKnownName": "circle"} 14 | -------------------------------------------------------------------------------- /src/bridgestyle/geostyler/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def toGeostyler(style, options=None): 5 | return json.loads(style), [], [] 6 | 7 | 8 | def fromGeostyler(style, options=None): 9 | return json.dumps(style), [], [] 10 | -------------------------------------------------------------------------------- /src/bridgestyle/geostyler/custom_properties.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | # A less custom literal would be great, see: https://github.com/geostyler/geostyler-style/issues/548 5 | class WellKnownText(Enum): 6 | NEW_LINE = '[newline]' 7 | -------------------------------------------------------------------------------- /src/bridgestyle/mapboxgl/__init__.py: -------------------------------------------------------------------------------- 1 | from . import fromgeostyler 2 | from . import togeostyler 3 | 4 | 5 | def toGeostyler(style, options=None): 6 | return togeostyler.convert(style, options) # TODO 7 | 8 | 9 | def fromGeostyler(style, options=None): 10 | return fromgeostyler.convert(style, options)[:2] 11 | -------------------------------------------------------------------------------- /src/bridgestyle/mapboxgl/togeostyler.py: -------------------------------------------------------------------------------- 1 | def convert(style, options=None): 2 | pass # TODO 3 | -------------------------------------------------------------------------------- /src/bridgestyle/mapserver/__init__.py: -------------------------------------------------------------------------------- 1 | from . import fromgeostyler 2 | from . import togeostyler 3 | 4 | 5 | def toGeostyler(style, options=None): 6 | return togeostyler.convert(style, options) # TODO 7 | 8 | 9 | def fromGeostyler(style, options=None): 10 | mb, symbols, warnings = fromgeostyler.convert(style, options) 11 | return mb, warnings 12 | -------------------------------------------------------------------------------- /src/bridgestyle/mapserver/fromgeostyler.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from ..qgis.expressions import ( 4 | OGC_PROPERTYNAME, 5 | OGC_IS_EQUAL_TO, 6 | OGC_CONCAT, 7 | OGC_SUB 8 | ) 9 | 10 | _warnings = [] 11 | 12 | 13 | def convertToDict(geostyler): 14 | global _warnings 15 | _warnings = [] 16 | global _symbols 17 | _symbols = [] 18 | layer = processLayer(geostyler) 19 | return layer, _symbols, _warnings 20 | 21 | 22 | def convert(geostyler, options=None): 23 | d, _, _ = convertToDict(geostyler) 24 | mapfile = convertDictToMapfile(d) 25 | symbols = convertDictToMapfile({"SYMBOLS": _symbols}) 26 | return mapfile, symbols, _warnings 27 | 28 | 29 | def convertDictToMapfile(d): 30 | def _toString(element, indent): 31 | s = "" 32 | INDENT = " " * indent 33 | for k, v in element.items(): 34 | if isinstance(v, dict): 35 | s += "%s%s\n" % (INDENT, k) 36 | s += _toString(v, indent + 1) 37 | s += INDENT + "END\n" 38 | elif isinstance(v, list): 39 | for item in v: 40 | s += _toString(item, indent) 41 | elif isinstance(v, tuple): 42 | s += "%s%s %s\n" % (INDENT, k, " ".join([str(item) for item in v])) 43 | else: 44 | s += "%s%s %s\n" % (INDENT, k, v) 45 | 46 | return s 47 | 48 | return _toString(d, 0) 49 | 50 | 51 | def processLayer(layer): 52 | classes = [] 53 | 54 | for rule in layer.get("rules", []): 55 | clazz = processRule(rule) 56 | classes.append(clazz) 57 | 58 | layerData = { 59 | "LAYER": { 60 | "NAME": _quote(layer.get("name", "")), 61 | "STATUS": "ON", 62 | "SIZEUNITS": "pixels", 63 | "CLASSES": classes, 64 | } 65 | } 66 | return layerData 67 | 68 | 69 | def processRule(rule): 70 | d = {"NAME": _quote(rule.get("name", "") or "default")} 71 | name = rule.get("name", "rule") 72 | 73 | expression = convertExpression(rule.get("filter", None)) 74 | if expression is not None: 75 | d["EXPRESSION"] = expression 76 | 77 | styles = [{"STYLE": processSymbolizer(s)} for s in rule["symbolizers"]] 78 | 79 | if "scaleDenominator" in rule: 80 | scale = rule["scaleDenominator"] 81 | if "max" in scale: 82 | d["MAXSCALEDENOM"] = scale["max"] 83 | if "min" in scale: 84 | d["MINSCALEDENOM"] = scale["min"] 85 | 86 | d["STYLES"] = styles 87 | 88 | return {"CLASS": d} 89 | 90 | 91 | func = { 92 | "Or": "OR", 93 | "And": "AND", 94 | OGC_IS_EQUAL_TO: "=", 95 | "PropertyIsNotEqualTo": "!=", 96 | "PropertyIsLessThanOrEqualTo": "<=", 97 | "PropertyIsGreaterThanOrEqualTo": ">=", 98 | "PropertyIsLessThan": "<", 99 | "PropertyIsGreaterThan": ">", 100 | "Add": "+", 101 | OGC_CONCAT: "+", 102 | OGC_SUB: "-", 103 | "Mul": "*", 104 | "Div": "/", 105 | "Not": "!", 106 | OGC_PROPERTYNAME: OGC_PROPERTYNAME, 107 | } # TODO 108 | 109 | 110 | def convertExpression(exp): 111 | if exp is None: 112 | return None 113 | if isinstance(exp, list): 114 | funcName = func.get(exp[0], None) 115 | if funcName is None: 116 | _warnings.append( 117 | "Unsupported expression function for MapServer conversion: '%s'" 118 | % exp[0] 119 | ) 120 | return None 121 | elif funcName == OGC_PROPERTYNAME: 122 | return '"[%s]"' % exp[1] 123 | else: 124 | arg1 = convertExpression(exp[1]) 125 | if len(exp) == 3: 126 | arg2 = convertExpression(exp[2]) 127 | return "(%s %s %s)" % (arg1, funcName, arg2) 128 | else: 129 | return "%s(%s)" % (funcName, arg1) 130 | else: 131 | try: 132 | f = float(exp) 133 | return exp 134 | except: 135 | return _quote(exp) 136 | 137 | 138 | def processSymbolizer(sl): 139 | symbolizerType = sl["kind"] 140 | if symbolizerType == "Icon": 141 | symbolizer = _iconSymbolizer(sl) 142 | if symbolizerType == "Line": 143 | symbolizer = _lineSymbolizer(sl) 144 | if symbolizerType == "Fill": 145 | symbolizer = _fillSymbolizer(sl) 146 | if symbolizerType == "Mark": 147 | symbolizer = _markSymbolizer(sl) 148 | if symbolizerType == "Text": 149 | symbolizer = _textSymbolizer(sl) 150 | if symbolizerType == "Raster": 151 | symbolizer = _rasterSymbolizer(sl) 152 | 153 | geom = _geometryFromSymbolizer(sl) 154 | if geom is not None: 155 | _warnings.append("Derived geometries are not supported in mapbox gl") 156 | 157 | return symbolizer 158 | 159 | 160 | def _symbolProperty(sl, name, default=None): 161 | if name in sl: 162 | return convertExpression(sl[name]) 163 | else: 164 | return default 165 | 166 | 167 | def _textSymbolizer(sl): 168 | style = {} 169 | color = _symbolProperty(sl, "color") 170 | fontFamily = _symbolProperty(sl, "font") 171 | label = _symbolProperty(sl, "label") 172 | size = _symbolProperty(sl, "size") 173 | if "offset" in sl: 174 | offset = sl["offset"] 175 | offsetx = convertExpression(offset[0]) 176 | offsety = convertExpression(offset[1]) 177 | style["OFFSET"] = (offsetx, offsety) 178 | 179 | style["TEXT"] = label 180 | style["SIZE"] = size 181 | style["FONT"] = fontFamily 182 | style["TYPE"] = "truetype" 183 | style["COLOR"] = color 184 | 185 | """ 186 | if "haloColor" in sl and "haloSize" in sl: 187 | paint["text-halo-width"] = _symbolProperty(sl, "haloSize") 188 | paint["text-halo-color"] = _symbolProperty(sl, "haloColor") 189 | 190 | rotation = -1 * float(qgisLayer.customProperty("labeling/angleOffset")) 191 | layout["text-rotate"] = rotation 192 | 193 | ["text-opacity"] = (255 - int(qgisLayer.layerTransparency())) / 255.0 194 | 195 | if str(qgisLayer.customProperty("labeling/scaleVisibility")).lower() == "true": 196 | layer["minzoom"] = _toZoomLevel(float(qgisLayer.customProperty("labeling/scaleMin"))) 197 | layer["maxzoom"] = _toZoomLevel(float(qgisLayer.customProperty("labeling/scaleMax"))) 198 | """ 199 | 200 | return {"LABEL": style} 201 | 202 | 203 | def _lineSymbolizer(sl, graphicStrokeLayer=0): 204 | opacity = _symbolProperty(sl, "opacity", 1.0) * 100 205 | color = _symbolProperty(sl, "color") 206 | graphicStroke = sl.get("graphicStroke", None) 207 | width = _symbolProperty(sl, "width") 208 | dasharray = _symbolProperty(sl, "dasharray") 209 | cap = _symbolProperty(sl, "cap") 210 | join = _symbolProperty(sl, "join") 211 | offset = _symbolProperty(sl, "offset") 212 | 213 | style = {} 214 | if graphicStroke is not None: 215 | name = _createSymbol(graphicStroke[0]) # TODO: support multiple symbol layers 216 | style["SYMBOL"] = _quote(name) 217 | if color is not None: 218 | style["WIDTH"] = width 219 | style["OPACITY"] = opacity 220 | style["COLOR"] = color 221 | style["LINECAP"] = cap 222 | style["LINEJOIN"] = join 223 | if dasharray is not None: 224 | style["PATTERN"] = dasharray 225 | if offset is not None: 226 | style["OFFSET"] = "%s -99" % str(offset) 227 | 228 | return style 229 | 230 | 231 | def _geometryFromSymbolizer(sl): 232 | geomExpr = convertExpression(sl.get("geometry", None)) 233 | return geomExpr 234 | 235 | 236 | def _createSymbol(sl): 237 | name = "" 238 | symbolizerType = sl["kind"] 239 | if symbolizerType == "Icon": 240 | path = os.path.basename(sl["image"]) 241 | name = "icon_" + os.path.splitext(path)[0] 242 | _symbols.append( 243 | {"SYMBOL": {"TYPE": "PIXMAP", "IMAGE": _quote(path), "NAME": _quote(name)}} 244 | ) 245 | elif symbolizerType == "Mark": 246 | shape = sl["wellKnownName"] 247 | if shape.startswith("file://"): 248 | svgFilename = shape.split("//")[-1] 249 | svgName = os.path.splitext(svgFilename)[0] 250 | name = "svgicon_" + svgName 251 | _symbols.append( 252 | { 253 | "SYMBOL": { 254 | "TYPE": "svg", 255 | "IMAGE": _quote(svgFilename), 256 | "NAME": _quote(name), 257 | } 258 | } 259 | ) 260 | elif shape.startswith("ttf://"): 261 | token = shape.split("//")[-1] 262 | font, code = token.split("#") 263 | character = chr(int(code, 16)) 264 | name = "txtmarker_%s_%s" % (font, character) 265 | _symbols.append( 266 | { 267 | "SYMBOL": { 268 | "TYPE": "TRUETYPE", 269 | "CHARACTER": _quote(character), 270 | "FONT": _quote(font), 271 | "NAME": _quote(name), 272 | } 273 | } 274 | ) 275 | else: 276 | name = shape 277 | 278 | return name 279 | 280 | 281 | def _iconSymbolizer(sl): 282 | rotation = _symbolProperty(sl, "rotate") or 0 283 | size = _symbolProperty(sl, "size") 284 | color = _symbolProperty(sl, "color") 285 | name = _createSymbol(sl) 286 | 287 | style = {"SYMBOL": _quote(name), "ANGLE": rotation, "SIZE": size} 288 | 289 | return style 290 | 291 | 292 | def _markSymbolizer(sl): 293 | # outlineDasharray = _symbolProperty(sl, "outlineDasharray") 294 | # opacity = _symbolProperty(sl, "opacity") 295 | size = _symbolProperty(sl, "size") 296 | rotation = _symbolProperty(sl, "rotate") or 0 297 | color = _symbolProperty(sl, "color") 298 | outlineColor = _symbolProperty(sl, "strokeColor") 299 | outlineWidth = _symbolProperty(sl, "strokeWidth") 300 | name = _createSymbol(sl) 301 | style = {"SYMBOL": _quote(name), "COLOR": color, "SIZE": size, "ANGLE": rotation} 302 | if outlineColor is not None: 303 | style["OUTLINECOLOR"] = outlineColor 304 | style["OUTLINEWIDTH"] = outlineWidth 305 | 306 | return style 307 | 308 | 309 | def _fillSymbolizer(sl): 310 | style = {} 311 | opacity = _symbolProperty(sl, "opacity", 1.0) * 100 312 | color = _symbolProperty(sl, "color") 313 | graphicFill = sl.get("graphicFill", None) 314 | if graphicFill is not None: 315 | name = _createSymbol(graphicFill[0]) # TODO: support multiple symbol layers 316 | style["SYMBOL"] = _quote(name) 317 | style["OPACITY"] = opacity 318 | if color is not None: 319 | style["COLOR"] = color 320 | 321 | outlineColor = _symbolProperty(sl, "outlineColor") 322 | if outlineColor is not None: 323 | outlineWidth = _symbolProperty(sl, "outlineWidth") 324 | style["OUTLINECOLOR"] = outlineColor 325 | style["OUTLINEWIDTH"] = outlineWidth 326 | 327 | return style 328 | 329 | 330 | def _rasterSymbolizer(sl): 331 | return None 332 | 333 | 334 | def _quote(t): 335 | return '"%s"' % t 336 | -------------------------------------------------------------------------------- /src/bridgestyle/mapserver/togeostyler.py: -------------------------------------------------------------------------------- 1 | def convert(style, options=None): 2 | raise NotImplementedError("togeostyler.convert() has not been implemented") # TODO 3 | -------------------------------------------------------------------------------- /src/bridgestyle/qgis/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import zipfile 3 | from shutil import copyfile 4 | 5 | from .. import mapboxgl 6 | from .. import mapserver 7 | from .. import sld 8 | from . import fromgeostyler 9 | from . import togeostyler 10 | 11 | 12 | def layerStyleAsSld(layer): 13 | geostyler, icons, sprites, warnings = togeostyler.convert(layer) 14 | sldString, sldWarnings = sld.fromgeostyler.convert(geostyler) 15 | warnings.extend(sldWarnings) 16 | return sldString, icons, warnings 17 | 18 | 19 | def saveLayerStyleAsSld(layer, filename): 20 | sldstring, icons, warnings = layerStyleAsSld(layer) 21 | with open(filename, "w", encoding='utf-8') as f: 22 | f.write(sldstring) 23 | return warnings 24 | 25 | 26 | def saveLayerStyleAsZippedSld(layer, filename): 27 | sldstring, icons, warnings = layerStyleAsSld(layer) 28 | z = zipfile.ZipFile(filename, "w") 29 | for icon in icons.keys(): 30 | if icon: 31 | z.write(icon, os.path.basename(icon)) 32 | z.writestr(layer.name() + ".sld", sldstring) 33 | z.close() 34 | return warnings 35 | 36 | 37 | def layerStyleAsMapbox(layer): 38 | geostyler, icons, sprites, warnings = togeostyler.convert(layer) 39 | mbox, mbWarnings = mapboxgl.fromgeostyler.convert(geostyler) 40 | warnings.extend(mbWarnings) 41 | return mbox, icons, warnings 42 | 43 | 44 | def layerStyleAsMapboxFolder(layer, folder): 45 | geostyler, icons, sprites, warnings = togeostyler.convert(layer) 46 | mbox, mbWarnings = mapboxgl.fromgeostyler.convert(geostyler) 47 | filename = os.path.join(folder, "style.mapbox") 48 | with open(filename, "w", encoding='utf-8') as f: 49 | f.write(mbox) 50 | # saveSpritesSheet(icons, folder) 51 | return warnings 52 | 53 | 54 | def layerStyleAsMapfile(layer): 55 | geostyler, icons, sprites, warnings = togeostyler.convert(layer) 56 | mserver, mserverSymbols, msWarnings = mapserver.fromgeostyler.convert(geostyler) 57 | warnings.extend(msWarnings) 58 | return mserver, mserverSymbols, icons, warnings 59 | 60 | 61 | def layerStyleAsMapfileFolder(layer, folder, additional=None): 62 | geostyler, icons, sprites, warnings = togeostyler.convert(layer) 63 | mserverDict, mserverSymbolsDict, msWarnings = mapserver.fromgeostyler.convertToDict(geostyler) 64 | warnings.extend(msWarnings) 65 | additional = additional or {} 66 | mserverDict["LAYER"].update(additional) 67 | mapfile = mapserver.fromgeostyler.convertDictToMapfile(mserverDict) 68 | symbols = mapserver.fromgeostyler.convertDictToMapfile({"SYMBOLS": mserverSymbolsDict}) 69 | filename = os.path.join(folder, layer.name() + ".txt") 70 | with open(filename, "w", encoding='utf-8') as f: 71 | f.write(mapfile) 72 | filename = os.path.join(folder, layer.name() + "_symbols.txt") 73 | with open(filename, "w", encoding='utf-8') as f: 74 | f.write(symbols) 75 | for icon in icons: 76 | dst = os.path.join(folder, os.path.basename(icon)) 77 | copyfile(icon, dst) 78 | return warnings 79 | -------------------------------------------------------------------------------- /src/bridgestyle/qgis/expressions.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Any 2 | 3 | try: 4 | from qgis.core import ( 5 | QgsExpressionNode, QgsExpression, QgsExpressionNodeBinaryOperator, 6 | QgsMapLayer, QgsVectorLayer, QgsFeatureRequest, QgsExpressionContext, 7 | QgsExpressionContextUtils, QgsFields, QgsFeature 8 | ) 9 | except (ImportError, ModuleNotFoundError): 10 | QgsExpressionNodeBinaryOperator = None 11 | QgsExpressionNode = None 12 | 13 | 14 | class UnsupportedExpressionException(Exception): 15 | """ Exception raised for unsupported expressions. """ 16 | pass 17 | 18 | 19 | class CompatibilityException(Exception): 20 | """ Exception raised for compatibility issues. """ 21 | pass 22 | 23 | 24 | OGC_PROPERTYNAME = "PropertyName" 25 | OGC_IS_EQUAL_TO = "PropertyIsEqualTo" 26 | OGC_IS_NULL = "PropertyIsNull" 27 | OGC_IS_NOT_NULL = "PropertyIsNotNull" 28 | OGC_IS_LIKE = "PropertyIsLike" 29 | OGC_CONCAT = "Concatenate" 30 | OGC_SUB = "Sub" 31 | 32 | _qbo = None # BinaryOperator 33 | _nt = None # NodeType 34 | BINOPS_MAP = {} # Mapping of QGIS binary operators to OGC operators (where possible) 35 | 36 | if QgsExpressionNode is None or QgsExpressionNodeBinaryOperator is None: 37 | raise CompatibilityException("Your QGIS version is not compatible with bridgestyle") 38 | 39 | # Make sure that we can find the binary operator types 40 | if hasattr(QgsExpressionNodeBinaryOperator, "boOr"): 41 | _qbo = QgsExpressionNodeBinaryOperator 42 | elif hasattr(getattr(QgsExpressionNodeBinaryOperator, "BinaryOperator", object()), "boOr"): 43 | _qbo = QgsExpressionNodeBinaryOperator.BinaryOperator 44 | 45 | # Make sure that we can find the node types 46 | if hasattr(QgsExpressionNode, "ntBinaryOperator"): 47 | _nt = QgsExpressionNode 48 | elif hasattr(getattr(QgsExpressionNode, "NodeType", object()), "ntBinaryOperator"): 49 | _nt = QgsExpressionNode.NodeType 50 | 51 | if _qbo is None or _nt is None: 52 | raise CompatibilityException("Your QGIS version is not compatible with bridgestyle") 53 | 54 | # Mapping of QGIS binary operators to OGC operators (where possible) 55 | BINOPS_MAP = { 56 | _qbo.boOr: "Or", 57 | _qbo.boAnd: "And", 58 | _qbo.boEQ: OGC_IS_EQUAL_TO, 59 | _qbo.boNE: "PropertyIsNotEqualTo", 60 | _qbo.boLE: "PropertyIsLessThanOrEqualTo", 61 | _qbo.boGE: "PropertyIsGreaterThanOrEqualTo", 62 | _qbo.boLT: "PropertyIsLessThan", 63 | _qbo.boGT: "PropertyIsGreaterThan", 64 | _qbo.boRegexp: None, 65 | _qbo.boLike: OGC_IS_LIKE, 66 | _qbo.boNotLike: None, 67 | _qbo.boILike: None, 68 | _qbo.boNotILike: None, 69 | _qbo.boIs: None, 70 | _qbo.boIsNot: None, 71 | _qbo.boPlus: "Add", # + operator can also be used for concatenation! [#93] 72 | _qbo.boMinus: OGC_SUB, 73 | _qbo.boMul: "Mul", 74 | _qbo.boDiv: "Div", 75 | _qbo.boIntDiv: None, 76 | _qbo.boMod: None, 77 | _qbo.boPow: None, 78 | _qbo.boConcat: OGC_CONCAT, # translates || operator 79 | } 80 | 81 | # Mapping of QGIS unary operators to OGC operators (where possible) 82 | # Note that this is not a dict but a list, uoMinus is at index 1 and uoNot is at index 0 and there aren't any others 83 | # See https://qgis.org/pyqgis/master/core/QgsExpressionNodeUnaryOperator.html 84 | UNOPS_MAP = ["Not", OGC_SUB] 85 | 86 | # QGIS function names mapped to OGC/WFS2.0 function names 87 | # See https://docs.geoserver.org/stable/en/user/filter/function_reference.html 88 | FUNCTION_MAP = { 89 | "radians": "toRadians", 90 | "degrees": "toDegrees", 91 | "floor": "floor", 92 | "ceil": "ceil", 93 | "area": "area", 94 | "buffer": "buffer", 95 | "centroid": "centroid", 96 | "if": "if_then_else", 97 | "bounds": "envelope", 98 | "distance": "distance", 99 | "convex_hull": "convexHull", 100 | "end_point": "endPoint", 101 | "start_point": "startPoint", 102 | "x": "getX", 103 | "y": "getY", 104 | "concat": OGC_CONCAT, 105 | "substr": "strSubstr", 106 | "lower": "strToLower", 107 | "upper": "strToUpper", 108 | "replace": "strReplace", 109 | "exterior_ring": "exteriorRing", 110 | "intersects": "intersects", 111 | "overlaps": "overlaps", 112 | "touches": "touches", 113 | "within": "within", 114 | "relates": "relates", 115 | "crosses": "crosses", 116 | "disjoint": "disjoint", 117 | "geom_from_wkt": "geomFromWKT", 118 | "perimeter": "geomLength", 119 | "union": "union", 120 | "acos": "acos", 121 | "asin": "asin", 122 | "atan": "atan", 123 | "atan2": "atan2", 124 | "sin": "sin", 125 | "cos": "cos", 126 | "tan": "tan", 127 | "ln": "log", 128 | "title": "strCapitalize", 129 | "translate": "offset", 130 | "min": "min", 131 | "max": "max", 132 | "to_int": "parseLong", 133 | "to_real": "parseDouble", 134 | "to_string": "to_string", # Not mapped to function, but required by MapBox GL 135 | } # TODO: test/improve 136 | 137 | 138 | class ExpressionConverter: 139 | """ Converts QGIS expressions to OGC/WFS2.0 expressions. """ 140 | 141 | layer: Optional[QgsMapLayer] = None 142 | fields: Optional[QgsFields] = None 143 | context: Optional[QgsExpressionContext] = None 144 | warnings: set[str] = set() 145 | 146 | def __init__(self, layer: QgsMapLayer): 147 | """ Initializes a new expression converter instances to work with the given layer. 148 | If that layer is a vector layer, it will sample the first feature and use it 149 | for the field list and the expression context. """ 150 | 151 | self.layer = layer 152 | 153 | feature = self._get_feature(layer) 154 | if feature is None: 155 | # Not a vector layer or no features found 156 | return 157 | 158 | # Get fields 159 | self.fields = feature.fields() 160 | 161 | # Set context to the first feature 162 | try: 163 | context = QgsExpressionContext() 164 | context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer)) 165 | context.setFeature(feature) 166 | self.context = context 167 | except Exception as e: 168 | self.warnings.add(f"Can't get expression context for layer '{layer.name()}': {str(e)}") 169 | 170 | def _get_feature(self, layer: QgsMapLayer) -> Optional[QgsFeature]: 171 | """ Returns the first feature of the given vector layer, or None if there aren't any. """ 172 | if not isinstance(layer, QgsVectorLayer): 173 | # Can't sample features from non-vector layers 174 | return None 175 | 176 | try: 177 | feature = None 178 | for ft in layer.getFeatures(QgsFeatureRequest().setLimit(10)): 179 | # Sample 10 features and use the first valid one 180 | if ft and ft.isValid(): 181 | feature = ft 182 | break 183 | if not feature: 184 | raise ValueError("no valid feature found") 185 | except Exception as e: 186 | self.warnings.add(f"Can't get sample feature for layer '{layer.name()}': {str(e)}") 187 | return None 188 | 189 | return feature 190 | 191 | def __del__(self): 192 | """ Cleans up the expression converter instance. """ 193 | self.layer = None 194 | self.fields = None 195 | if isinstance(self.context, QgsExpressionContext): 196 | self.context.clearCachedValues() 197 | self.context = None 198 | self.warnings.clear() 199 | 200 | def convert(self, expression: QgsExpression) -> Any: 201 | """ Kicks off the recursive expression walker and returns the converted result. """ 202 | if isinstance(expression, QgsExpression): 203 | if not expression.isValid(): 204 | self.warnings.add(f"Invalid expression: {expression.expression()}") 205 | return None 206 | return self._walk(expression.rootNode(), expression) 207 | self.warnings.add(f"Invalid expression type: {type(expression).__name__}") 208 | return None 209 | 210 | def _walk(self, node, parent, null_allowed=False, cast_to=None): 211 | exp = None 212 | cast_to = str(cast_to).lower() 213 | if node.nodeType() == _nt.ntBinaryOperator: 214 | exp = self._handle_binary_op(node, parent) 215 | elif node.nodeType() == _nt.ntUnaryOperator: 216 | exp = self._handle_unary_op(node, parent) 217 | elif node.nodeType() == _nt.ntInOperator: 218 | exp = self._handle_in_op(node) 219 | elif node.nodeType() == _nt.ntFunction: 220 | exp = self._handle_function(node, parent) 221 | elif node.nodeType() == _nt.ntLiteral: 222 | exp = self._handle_literal(node) 223 | if exp is None and null_allowed: 224 | return exp 225 | if cast_to == 'string': 226 | exp = str(exp) 227 | elif cast_to.startswith('integer'): 228 | exp = int(exp) 229 | elif cast_to == 'real': 230 | exp = float(exp) 231 | elif node.nodeType() == _nt.ntColumnRef: 232 | exp = self._handle_column_ref(node) 233 | if exp is None: 234 | raise UnsupportedExpressionException( 235 | "Unsupported operator in expression: '%s'" % str(node) 236 | ) 237 | return exp 238 | 239 | def _handle_in_op(self, node): 240 | """ 241 | Handles IN expression. Converts to a series of (A='a') OR (B='b'). 242 | """ 243 | if node.isNotIn(): 244 | raise UnsupportedExpressionException("expression NOT IN is unsupported") 245 | # convert this expression to another (equivalent Expression) 246 | if node.node().nodeType() != _nt.ntColumnRef: 247 | raise UnsupportedExpressionException("expression IN doesn't refer to a column") 248 | if node.list().count() == 0: 249 | raise UnsupportedExpressionException("expression IN contains no values") 250 | 251 | colRef = self._handle_column_ref(node.node()) 252 | propEqualsExprs = [] # one for each of the literals in the expression 253 | for item in node.list().list(): 254 | if item.nodeType() != _nt.ntLiteral: 255 | raise UnsupportedExpressionException("expression IN isn't literal") 256 | # equals_expr = QgsExpressionNodeBinaryOperator(2,colRef,item) #2 is "=" 257 | equals_expr = [BINOPS_MAP[_qbo.boEQ], colRef, self._handle_literal(item)] # 2 is "=" 258 | propEqualsExprs.append(equals_expr) 259 | 260 | # build into single expression 261 | if len(propEqualsExprs) == 1: 262 | return propEqualsExprs[0] # handle 1 item in the list 263 | accum = [BINOPS_MAP[_qbo.boOr], propEqualsExprs[0], propEqualsExprs[1]] # 0="OR" 264 | for idx in range(2, len(propEqualsExprs)): 265 | accum = [BINOPS_MAP[_qbo.boOr], accum, propEqualsExprs[idx]] # 0="OR" 266 | return accum 267 | 268 | def _handle_binary_op(self, node: QgsExpressionNodeBinaryOperator, parent: QgsExpression): 269 | op = node.op() 270 | retOp = BINOPS_MAP[op] 271 | left = node.opLeft() 272 | right = node.opRight() 273 | 274 | if op == _qbo.boPlus and self.context is not None: 275 | # Detect special case where ADD (+) is used to concatenate strings [#93] 276 | result = left.eval(parent, self.context) 277 | if isinstance(result, str): 278 | # If the evaluated result is a string, then retOp should become OGC_CONCAT. 279 | # TODO: because a 3-item list is returned, this may result in multiple nested Concatenate expressions! 280 | retOp = OGC_CONCAT 281 | 282 | retLeft = self._walk(left, parent) 283 | castTo = None 284 | if left.nodeType() == _nt.ntColumnRef: 285 | fields = [f for f in self.fields if f.name() == retLeft[-1]] 286 | if len(fields) == 1: 287 | # Field has been found, get its type 288 | castTo = fields[0].typeName() 289 | retRight = self._walk(right, parent, True, castTo) 290 | if retOp is retRight is None: 291 | if op == _qbo.boIs: 292 | # Special case for IS NULL 293 | retOp = OGC_IS_NULL 294 | elif op == _qbo.boIsNot: 295 | # Special case for IS NOT NULL 296 | retOp = OGC_IS_NOT_NULL 297 | return [retOp, retLeft, retRight] 298 | 299 | def _handle_unary_op(self, node, parent): 300 | op = node.op() 301 | operand = node.operand() 302 | retOp = UNOPS_MAP[op] 303 | retOperand = self._walk(operand, parent) 304 | if retOp == OGC_SUB: # handle the particular case of a minus in a negative number 305 | return [retOp, 0, retOperand] 306 | else: 307 | return [retOp, retOperand] 308 | 309 | @staticmethod 310 | def _handle_literal(node): 311 | val = node.value() 312 | if isinstance(val, str): 313 | val = val.replace("\n", "\\n") 314 | return val 315 | 316 | def _handle_column_ref(self, node): 317 | if self.layer is not None: 318 | attrName = node.name().casefold() 319 | for field in self.fields: 320 | if field.name().casefold() == attrName: 321 | return [OGC_PROPERTYNAME, field.name()] 322 | return [OGC_PROPERTYNAME, node.name()] 323 | 324 | def _handle_function(self, node, parent): 325 | fnIndex = node.fnIndex() 326 | func = QgsExpression.Functions()[fnIndex].name() 327 | if func == "$geometry": 328 | return [OGC_PROPERTYNAME, "geom"] 329 | fname = FUNCTION_MAP.get(func) 330 | if fname is not None: 331 | elems = [fname] 332 | args = node.args() 333 | if args is not None: 334 | args = args.list() 335 | for arg in args: 336 | elems.append(self._walk(arg, parent)) 337 | return elems 338 | else: 339 | raise UnsupportedExpressionException( 340 | f"Unsupported function in expression: '{func}'" 341 | ) 342 | -------------------------------------------------------------------------------- /src/bridgestyle/qgis/fromgeostyler.py: -------------------------------------------------------------------------------- 1 | def convert(style, options=None): 2 | pass # TODO 3 | -------------------------------------------------------------------------------- /src/bridgestyle/sld/__init__.py: -------------------------------------------------------------------------------- 1 | from . import fromgeostyler 2 | from . import togeostyler 3 | 4 | 5 | def toGeostyler(style, options=None): 6 | return togeostyler.convert(style, options) # TODO 7 | 8 | 9 | def fromGeostyler(style, options=None): 10 | return fromgeostyler.convert(style, options) 11 | -------------------------------------------------------------------------------- /src/bridgestyle/sld/parsecdata.py: -------------------------------------------------------------------------------- 1 | # Hack to add cdata with xml.etree 2 | from xml.etree import ElementTree 3 | 4 | ElementTree._original_serialize_xml = ElementTree._serialize_xml 5 | 6 | 7 | def _serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs): 8 | if elem.tag == '![CDATA[': 9 | write("<{}{}]]>".format(elem.tag, elem.text)) 10 | if elem.tail: 11 | write(ElementTree._escape_cdata(elem.tail)) 12 | else: 13 | return ElementTree._original_serialize_xml(write, elem, qnames, namespaces,short_empty_elements, **kwargs) 14 | 15 | 16 | ElementTree._serialize_xml = ElementTree._serialize['xml'] = _serialize_xml -------------------------------------------------------------------------------- /src/bridgestyle/sld/togeostyler.py: -------------------------------------------------------------------------------- 1 | def convert(geostyler, options=None): 2 | return {}, [] 3 | -------------------------------------------------------------------------------- /src/bridgestyle/sld/transformations.py: -------------------------------------------------------------------------------- 1 | from xml.etree.ElementTree import Element, SubElement 2 | 3 | 4 | def _addLiteral(parent, v): 5 | elem = SubElement(parent, "ogc:Literal") 6 | elem.text = str(v) 7 | 8 | 9 | def processTransformation(transformation): 10 | root = Element("Transformation") 11 | trans = SubElement(root, "ogc:Function", name=transformation["type"]) 12 | 13 | if transformation["type"] == "vec:Heatmap": 14 | data = SubElement(trans, "ogc:Function", name="parameter") 15 | _addLiteral(data, "data") 16 | weight = SubElement(trans, "ogc:Function", name="parameter") 17 | _addLiteral(weight, "weightAttr") 18 | _addLiteral(weight, transformation["weightAttr"]) 19 | radius = SubElement(trans, "ogc:Function", name="parameter") 20 | _addLiteral(radius, "radiusPixels") 21 | _addLiteral(radius, transformation["radiusPixels"]) 22 | 23 | def _addEnvParam(paramName, envParamName): 24 | param = SubElement(trans, "ogc:Function", name="parameter") 25 | _addLiteral(param, paramName) 26 | env = SubElement(param, "ogc:Function", name="env") 27 | _addLiteral(env, envParamName) 28 | 29 | _addEnvParam("outputBBOX", "wms_bbox") 30 | _addEnvParam("outputWidth", "wms_width") 31 | _addEnvParam("outputHeight", "wms_height") 32 | 33 | return root 34 | -------------------------------------------------------------------------------- /src/bridgestyle/style2style.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import shutil 4 | 5 | from . import arcgis 6 | from . import geostyler 7 | from . import mapboxgl 8 | from . import sld 9 | 10 | _exts = {"sld": sld, "geostyler": geostyler, "mapbox": mapboxgl, "lyrx": arcgis} 11 | 12 | 13 | def convert(fileA, fileB, options): 14 | extA = os.path.splitext(fileA)[1][1:] 15 | extB = os.path.splitext(fileB)[1][1:] 16 | if extA not in _exts: 17 | print("Unsupported style type: '%s'" % extA) 18 | return 19 | if extB not in _exts: 20 | print("Unsupported style type: '%s'" % extB) 21 | return 22 | 23 | with open(fileA) as f: 24 | styleA = f.read() 25 | 26 | geostyler, icons, geostylerwarnings = _exts[extA].toGeostyler(styleA, options) 27 | if geostyler.get("rules", []): 28 | styleB, warningsB = _exts[extB].fromGeostyler(geostyler, options) 29 | outputfolder = os.path.dirname(fileB) 30 | for f in icons: 31 | dst = os.path.join(outputfolder, os.path.basename(f)) 32 | shutil.copy(f, dst) 33 | 34 | with open(fileB, "w") as f: 35 | f.write(styleB) 36 | 37 | for w in geostylerwarnings + warningsB: 38 | print(f"WARNING: {w}") 39 | else: 40 | for w in geostylerwarnings: 41 | print(f"WARNING: {w}") 42 | print("ERROR: Empty geostyler result (This is most likely caused by the " 43 | "original style containing only unsupported elements)") 44 | 45 | 46 | def main(): 47 | parser = argparse.ArgumentParser() 48 | parser.add_argument('-c', action='store_true', 49 | help="Convert attribute names to lower case", 50 | dest="tolowercase") 51 | parser.add_argument('-e', action='store_true', 52 | help="Replace Esri font markers with standard symbols", 53 | dest="replaceesri") 54 | parser.add_argument('src') 55 | parser.add_argument('dst') 56 | args = parser.parse_args() 57 | 58 | argsdict = dict(vars(args)) 59 | del argsdict["src"] 60 | del argsdict["dst"] 61 | convert(args.src, args.dst, argsdict) 62 | -------------------------------------------------------------------------------- /src/bridgestyle/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.3.dev0" 2 | -------------------------------------------------------------------------------- /test/arcgistogeostyler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | 5 | from bridgestyle.arcgis import togeostyler 6 | 7 | test_data_folder = os.path.join(os.path.dirname(__file__), "data", "arcgis") 8 | 9 | def resource_path(name): 10 | return os.path.join(test_data_folder, name) 11 | 12 | class ArcgisTestTest(unittest.TestCase): 13 | 14 | def test_conversion(self): 15 | for filename in os.listdir(test_data_folder): 16 | path = os.path.join(test_data_folder, filename) 17 | with open(path) as f: 18 | arcgis = json.load(f) 19 | ret = togeostyler.convert(arcgis) 20 | print(ret) 21 | 22 | if __name__ == '__main__': 23 | unittest.main() -------------------------------------------------------------------------------- /test/context.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) 4 | -------------------------------------------------------------------------------- /test/data/arcgis/Cartographic Line.lyrx: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "CIMLayerDocument", 3 | "version" : "2.5.0", 4 | "build" : 22081, 5 | "layers" : [ 6 | "CIMPATH=layers/cartographic_line.xml" 7 | ], 8 | "layerDefinitions" : [ 9 | { 10 | "type" : "CIMFeatureLayer", 11 | "name" : "Cartographic line", 12 | "uRI" : "CIMPATH=layers/cartographic_line.xml", 13 | "sourceModifiedTime" : { 14 | "type" : "TimeInstant" 15 | }, 16 | "metadataURI" : "CIMPATH=Metadata/ddbdc063dd30b1dc5c93267d7a263866.xml", 17 | "useSourceMetadata" : false, 18 | "description" : "Symbology with cartographic line symbol.", 19 | "expanded" : true, 20 | "layerType" : "Operational", 21 | "showLegends" : true, 22 | "visibility" : true, 23 | "displayCacheType" : "Permanent", 24 | "maxDisplayCacheAge" : 5, 25 | "showPopups" : true, 26 | "serviceLayerID" : -1, 27 | "refreshRate" : -1, 28 | "refreshRateUnit" : "esriTimeUnitsSeconds", 29 | "autoGenerateFeatureTemplates" : true, 30 | "featureTable" : { 31 | "type" : "CIMFeatureTable", 32 | "displayField" : "NAME", 33 | "editable" : true, 34 | "timeFields" : { 35 | "type" : "CIMTimeTableDefinition" 36 | }, 37 | "timeDefinition" : { 38 | "type" : "CIMTimeDataDefinition" 39 | }, 40 | "timeDisplayDefinition" : { 41 | "type" : "CIMTimeDisplayDefinition", 42 | "timeInterval" : 0, 43 | "timeIntervalUnits" : "esriTimeUnitsHours", 44 | "timeOffsetUnits" : "esriTimeUnitsYears" 45 | }, 46 | "dataConnection" : { 47 | "type" : "CIMStandardDataConnection", 48 | "workspaceConnectionString" : "DATABASE=..\\..\\..\\map_data", 49 | "workspaceFactory" : "Shapefile", 50 | "dataset" : "borders_europe", 51 | "datasetType" : "esriDTFeatureClass" 52 | }, 53 | "studyAreaSpatialRel" : "esriSpatialRelUndefined", 54 | "searchOrder" : "esriSearchOrderSpatial" 55 | }, 56 | "htmlPopupEnabled" : true, 57 | "htmlPopupFormat" : { 58 | "type" : "CIMHtmlPopupFormat", 59 | "htmlUseCodedDomainValues" : true, 60 | "htmlPresentationStyle" : "TwoColumnTable" 61 | }, 62 | "isFlattened" : true, 63 | "selectable" : true, 64 | "selectionSymbol" : { 65 | "type" : "CIMSymbolReference", 66 | "symbol" : { 67 | "type" : "CIMLineSymbol", 68 | "symbolLayers" : [ 69 | { 70 | "type" : "CIMSolidStroke", 71 | "enable" : true, 72 | "capStyle" : "Round", 73 | "joinStyle" : "Round", 74 | "lineStyle3D" : "Strip", 75 | "miterLimit" : 10, 76 | "width" : 2, 77 | "color" : { 78 | "type" : "CIMRGBColor", 79 | "values" : [ 80 | 0, 81 | 255, 82 | 255, 83 | 100 84 | ] 85 | } 86 | } 87 | ] 88 | } 89 | }, 90 | "featureCacheType" : "None", 91 | "labelClasses" : [ 92 | { 93 | "type" : "CIMLabelClass", 94 | "expression" : "[NAME]", 95 | "expressionEngine" : "VBScript", 96 | "featuresToLabel" : "AllVisibleFeatures", 97 | "maplexLabelPlacementProperties" : { 98 | "type" : "CIMMaplexLabelPlacementProperties", 99 | "featureType" : "Line", 100 | "avoidPolygonHoles" : true, 101 | "canOverrunFeature" : true, 102 | "canPlaceLabelOutsidePolygon" : true, 103 | "canRemoveOverlappingLabel" : true, 104 | "canStackLabel" : true, 105 | "connectionType" : "Unambiguous", 106 | "constrainOffset" : "NoConstraint", 107 | "contourAlignmentType" : "Page", 108 | "contourLadderType" : "Straight", 109 | "contourMaximumAngle" : 90, 110 | "enableConnection" : true, 111 | "featureWeight" : 100, 112 | "fontHeightReductionLimit" : 4, 113 | "fontHeightReductionStep" : 0.5, 114 | "fontWidthReductionLimit" : 90, 115 | "fontWidthReductionStep" : 5, 116 | "graticuleAlignmentType" : "Straight", 117 | "labelBuffer" : 15, 118 | "labelLargestPolygon" : true, 119 | "labelPriority" : -1, 120 | "labelStackingProperties" : { 121 | "type" : "CIMMaplexLabelStackingProperties", 122 | "stackAlignment" : "ChooseBest", 123 | "maximumNumberOfLines" : 3, 124 | "minimumNumberOfCharsPerLine" : 3, 125 | "maximumNumberOfCharsPerLine" : 24 126 | }, 127 | "lineFeatureType" : "General", 128 | "linePlacementMethod" : "OffsetCurvedFromLine", 129 | "maximumLabelOverrun" : 36, 130 | "maximumLabelOverrunUnit" : "Point", 131 | "minimumFeatureSizeUnit" : "Map", 132 | "multiPartOption" : "OneLabelPerPart", 133 | "offsetAlongLineProperties" : { 134 | "type" : "CIMMaplexOffsetAlongLineProperties", 135 | "placementMethod" : "BestPositionAlongLine", 136 | "labelAnchorPoint" : "CenterOfLabel", 137 | "distanceUnit" : "Percentage", 138 | "useLineDirection" : true 139 | }, 140 | "pointExternalZonePriorities" : { 141 | "type" : "CIMMaplexExternalZonePriorities", 142 | "aboveLeft" : 4, 143 | "aboveCenter" : 2, 144 | "aboveRight" : 1, 145 | "centerRight" : 3, 146 | "belowRight" : 5, 147 | "belowCenter" : 7, 148 | "belowLeft" : 8, 149 | "centerLeft" : 6 150 | }, 151 | "pointPlacementMethod" : "AroundPoint", 152 | "polygonAnchorPointType" : "GeometricCenter", 153 | "polygonBoundaryWeight" : 200, 154 | "polygonExternalZones" : { 155 | "type" : "CIMMaplexExternalZonePriorities", 156 | "aboveLeft" : 4, 157 | "aboveCenter" : 2, 158 | "aboveRight" : 1, 159 | "centerRight" : 3, 160 | "belowRight" : 5, 161 | "belowCenter" : 7, 162 | "belowLeft" : 8, 163 | "centerLeft" : 6 164 | }, 165 | "polygonFeatureType" : "General", 166 | "polygonInternalZones" : { 167 | "type" : "CIMMaplexInternalZonePriorities", 168 | "center" : 1 169 | }, 170 | "polygonPlacementMethod" : "CurvedInPolygon", 171 | "primaryOffset" : 1, 172 | "primaryOffsetUnit" : "Point", 173 | "removeExtraWhiteSpace" : true, 174 | "repetitionIntervalUnit" : "Map", 175 | "rotationProperties" : { 176 | "type" : "CIMMaplexRotationProperties", 177 | "rotationType" : "Arithmetic", 178 | "alignmentType" : "Straight" 179 | }, 180 | "secondaryOffset" : 100, 181 | "strategyPriorities" : { 182 | "type" : "CIMMaplexStrategyPriorities", 183 | "stacking" : 1, 184 | "overrun" : 2, 185 | "fontCompression" : 3, 186 | "fontReduction" : 4, 187 | "abbreviation" : 5 188 | }, 189 | "thinningDistanceUnit" : "Map", 190 | "truncationMarkerCharacter" : ".", 191 | "truncationMinimumLength" : 1, 192 | "truncationPreferredCharacters" : "aeiou" 193 | }, 194 | "name" : "Default", 195 | "priority" : 2, 196 | "standardLabelPlacementProperties" : { 197 | "type" : "CIMStandardLabelPlacementProperties", 198 | "featureType" : "Line", 199 | "featureWeight" : "None", 200 | "labelWeight" : "High", 201 | "numLabelsOption" : "OneLabelPerPart", 202 | "lineLabelPosition" : { 203 | "type" : "CIMStandardLineLabelPosition", 204 | "above" : true, 205 | "inLine" : true, 206 | "parallel" : true 207 | }, 208 | "lineLabelPriorities" : { 209 | "type" : "CIMStandardLineLabelPriorities", 210 | "aboveStart" : 3, 211 | "aboveAlong" : 3, 212 | "aboveEnd" : 3, 213 | "centerStart" : 3, 214 | "centerAlong" : 3, 215 | "centerEnd" : 3, 216 | "belowStart" : 3, 217 | "belowAlong" : 3, 218 | "belowEnd" : 3 219 | }, 220 | "pointPlacementMethod" : "AroundPoint", 221 | "pointPlacementPriorities" : { 222 | "type" : "CIMStandardPointPlacementPriorities", 223 | "aboveLeft" : 2, 224 | "aboveCenter" : 2, 225 | "aboveRight" : 1, 226 | "centerLeft" : 3, 227 | "centerRight" : 2, 228 | "belowLeft" : 3, 229 | "belowCenter" : 3, 230 | "belowRight" : 2 231 | }, 232 | "rotationType" : "Arithmetic", 233 | "polygonPlacementMethod" : "AlwaysHorizontal" 234 | }, 235 | "textSymbol" : { 236 | "type" : "CIMSymbolReference", 237 | "symbol" : { 238 | "type" : "CIMTextSymbol", 239 | "blockProgression" : "TTB", 240 | "compatibilityMode" : true, 241 | "depth3D" : 1, 242 | "drawSoftHyphen" : true, 243 | "extrapolateBaselines" : true, 244 | "flipAngle" : 90, 245 | "fontEffects" : "Normal", 246 | "fontEncoding" : "Unicode", 247 | "fontFamilyName" : "Arial", 248 | "fontStyleName" : "Regular", 249 | "fontType" : "Unspecified", 250 | "haloSize" : 1, 251 | "height" : 8, 252 | "hinting" : "Default", 253 | "horizontalAlignment" : "Center", 254 | "kerning" : true, 255 | "letterWidth" : 100, 256 | "ligatures" : true, 257 | "lineGapType" : "ExtraLeading", 258 | "shadowColor" : { 259 | "type" : "CIMRGBColor", 260 | "values" : [ 261 | 0, 262 | 0, 263 | 0, 264 | 100 265 | ] 266 | }, 267 | "symbol" : { 268 | "type" : "CIMPolygonSymbol", 269 | "symbolLayers" : [ 270 | { 271 | "type" : "CIMSolidFill", 272 | "enable" : true, 273 | "color" : { 274 | "type" : "CIMRGBColor", 275 | "values" : [ 276 | 0, 277 | 0, 278 | 0, 279 | 100 280 | ] 281 | } 282 | } 283 | ] 284 | }, 285 | "textCase" : "Normal", 286 | "textDirection" : "LTR", 287 | "verticalAlignment" : "Bottom", 288 | "verticalGlyphOrientation" : "Right", 289 | "wordSpacing" : 100, 290 | "billboardMode3D" : "FaceNearPlane" 291 | } 292 | }, 293 | "useCodedValue" : true, 294 | "visibility" : true, 295 | "iD" : -1 296 | } 297 | ], 298 | "renderer" : { 299 | "type" : "CIMSimpleRenderer", 300 | "patch" : "Default", 301 | "symbol" : { 302 | "type" : "CIMSymbolReference", 303 | "symbol" : { 304 | "type" : "CIMLineSymbol", 305 | "symbolLayers" : [ 306 | { 307 | "type" : "CIMSolidStroke", 308 | "effects" : [ 309 | { 310 | "type" : "CIMGeometricEffectDashes", 311 | "dashTemplate" : [ 312 | 0, 313 | 2, 314 | 1, 315 | 1, 316 | 1, 317 | 10 318 | ], 319 | "lineDashEnding" : "NoConstraint", 320 | "controlPointEnding" : "NoConstraint" 321 | } 322 | ], 323 | "enable" : true, 324 | "capStyle" : "Butt", 325 | "joinStyle" : "Miter", 326 | "lineStyle3D" : "Strip", 327 | "miterLimit" : 10, 328 | "width" : 2, 329 | "color" : { 330 | "type" : "CIMRGBColor", 331 | "values" : [ 332 | 0, 333 | 0, 334 | 0, 335 | 100 336 | ] 337 | } 338 | } 339 | ] 340 | } 341 | } 342 | }, 343 | "scaleSymbols" : true, 344 | "snappable" : true 345 | } 346 | ], 347 | "binaryReferences" : [ 348 | { 349 | "type" : "CIMBinaryReference", 350 | "uRI" : "CIMPATH=Metadata/ddbdc063dd30b1dc5c93267d7a263866.xml", 351 | "data" : "\r\n20200514142110001.0TRUECartographic lineSymbology with cartographic line symbol.\r\n" 352 | } 353 | ], 354 | "rGBColorProfile" : "sRGB IEC61966-2-1 noBPC", 355 | "cMYKColorProfile" : "U.S. Web Coated (SWOP) v2" 356 | } -------------------------------------------------------------------------------- /test/data/arcgis/Hash Line.lyrx: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "CIMLayerDocument", 3 | "version" : "2.5.0", 4 | "build" : 22081, 5 | "layers" : [ 6 | "CIMPATH=layers/hash_line.xml" 7 | ], 8 | "layerDefinitions" : [ 9 | { 10 | "type" : "CIMFeatureLayer", 11 | "name" : "Hash line", 12 | "uRI" : "CIMPATH=layers/hash_line.xml", 13 | "sourceModifiedTime" : { 14 | "type" : "TimeInstant" 15 | }, 16 | "metadataURI" : "CIMPATH=Metadata/24c82a378d13647fa1d93b1e2aaf37ac.xml", 17 | "useSourceMetadata" : false, 18 | "description" : "Symbology with hash line symbol.", 19 | "expanded" : true, 20 | "layerType" : "Operational", 21 | "showLegends" : true, 22 | "visibility" : true, 23 | "displayCacheType" : "Permanent", 24 | "maxDisplayCacheAge" : 5, 25 | "showPopups" : true, 26 | "serviceLayerID" : -1, 27 | "refreshRate" : -1, 28 | "refreshRateUnit" : "esriTimeUnitsSeconds", 29 | "autoGenerateFeatureTemplates" : true, 30 | "featureTable" : { 31 | "type" : "CIMFeatureTable", 32 | "displayField" : "NAME", 33 | "editable" : true, 34 | "timeFields" : { 35 | "type" : "CIMTimeTableDefinition" 36 | }, 37 | "timeDefinition" : { 38 | "type" : "CIMTimeDataDefinition" 39 | }, 40 | "timeDisplayDefinition" : { 41 | "type" : "CIMTimeDisplayDefinition", 42 | "timeInterval" : 0, 43 | "timeIntervalUnits" : "esriTimeUnitsHours", 44 | "timeOffsetUnits" : "esriTimeUnitsYears" 45 | }, 46 | "dataConnection" : { 47 | "type" : "CIMStandardDataConnection", 48 | "workspaceConnectionString" : "DATABASE=..\\..\\..\\map_data", 49 | "workspaceFactory" : "Shapefile", 50 | "dataset" : "borders_europe", 51 | "datasetType" : "esriDTFeatureClass" 52 | }, 53 | "studyAreaSpatialRel" : "esriSpatialRelUndefined", 54 | "searchOrder" : "esriSearchOrderSpatial" 55 | }, 56 | "htmlPopupEnabled" : true, 57 | "htmlPopupFormat" : { 58 | "type" : "CIMHtmlPopupFormat", 59 | "htmlUseCodedDomainValues" : true, 60 | "htmlPresentationStyle" : "TwoColumnTable" 61 | }, 62 | "isFlattened" : true, 63 | "selectable" : true, 64 | "selectionSymbol" : { 65 | "type" : "CIMSymbolReference", 66 | "symbol" : { 67 | "type" : "CIMLineSymbol", 68 | "symbolLayers" : [ 69 | { 70 | "type" : "CIMSolidStroke", 71 | "enable" : true, 72 | "capStyle" : "Round", 73 | "joinStyle" : "Round", 74 | "lineStyle3D" : "Strip", 75 | "miterLimit" : 10, 76 | "width" : 2, 77 | "color" : { 78 | "type" : "CIMRGBColor", 79 | "values" : [ 80 | 0, 81 | 255, 82 | 255, 83 | 100 84 | ] 85 | } 86 | } 87 | ] 88 | } 89 | }, 90 | "featureCacheType" : "None", 91 | "labelClasses" : [ 92 | { 93 | "type" : "CIMLabelClass", 94 | "expression" : "[NAME]", 95 | "expressionEngine" : "VBScript", 96 | "featuresToLabel" : "AllVisibleFeatures", 97 | "maplexLabelPlacementProperties" : { 98 | "type" : "CIMMaplexLabelPlacementProperties", 99 | "featureType" : "Line", 100 | "avoidPolygonHoles" : true, 101 | "canOverrunFeature" : true, 102 | "canPlaceLabelOutsidePolygon" : true, 103 | "canRemoveOverlappingLabel" : true, 104 | "canStackLabel" : true, 105 | "connectionType" : "Unambiguous", 106 | "constrainOffset" : "NoConstraint", 107 | "contourAlignmentType" : "Page", 108 | "contourLadderType" : "Straight", 109 | "contourMaximumAngle" : 90, 110 | "enableConnection" : true, 111 | "featureWeight" : 100, 112 | "fontHeightReductionLimit" : 4, 113 | "fontHeightReductionStep" : 0.5, 114 | "fontWidthReductionLimit" : 90, 115 | "fontWidthReductionStep" : 5, 116 | "graticuleAlignmentType" : "Straight", 117 | "labelBuffer" : 15, 118 | "labelLargestPolygon" : true, 119 | "labelPriority" : -1, 120 | "labelStackingProperties" : { 121 | "type" : "CIMMaplexLabelStackingProperties", 122 | "stackAlignment" : "ChooseBest", 123 | "maximumNumberOfLines" : 3, 124 | "minimumNumberOfCharsPerLine" : 3, 125 | "maximumNumberOfCharsPerLine" : 24 126 | }, 127 | "lineFeatureType" : "General", 128 | "linePlacementMethod" : "OffsetCurvedFromLine", 129 | "maximumLabelOverrun" : 36, 130 | "maximumLabelOverrunUnit" : "Point", 131 | "minimumFeatureSizeUnit" : "Map", 132 | "multiPartOption" : "OneLabelPerPart", 133 | "offsetAlongLineProperties" : { 134 | "type" : "CIMMaplexOffsetAlongLineProperties", 135 | "placementMethod" : "BestPositionAlongLine", 136 | "labelAnchorPoint" : "CenterOfLabel", 137 | "distanceUnit" : "Percentage", 138 | "useLineDirection" : true 139 | }, 140 | "pointExternalZonePriorities" : { 141 | "type" : "CIMMaplexExternalZonePriorities", 142 | "aboveLeft" : 4, 143 | "aboveCenter" : 2, 144 | "aboveRight" : 1, 145 | "centerRight" : 3, 146 | "belowRight" : 5, 147 | "belowCenter" : 7, 148 | "belowLeft" : 8, 149 | "centerLeft" : 6 150 | }, 151 | "pointPlacementMethod" : "AroundPoint", 152 | "polygonAnchorPointType" : "GeometricCenter", 153 | "polygonBoundaryWeight" : 200, 154 | "polygonExternalZones" : { 155 | "type" : "CIMMaplexExternalZonePriorities", 156 | "aboveLeft" : 4, 157 | "aboveCenter" : 2, 158 | "aboveRight" : 1, 159 | "centerRight" : 3, 160 | "belowRight" : 5, 161 | "belowCenter" : 7, 162 | "belowLeft" : 8, 163 | "centerLeft" : 6 164 | }, 165 | "polygonFeatureType" : "General", 166 | "polygonInternalZones" : { 167 | "type" : "CIMMaplexInternalZonePriorities", 168 | "center" : 1 169 | }, 170 | "polygonPlacementMethod" : "CurvedInPolygon", 171 | "primaryOffset" : 1, 172 | "primaryOffsetUnit" : "Point", 173 | "removeExtraWhiteSpace" : true, 174 | "repetitionIntervalUnit" : "Map", 175 | "rotationProperties" : { 176 | "type" : "CIMMaplexRotationProperties", 177 | "rotationType" : "Arithmetic", 178 | "alignmentType" : "Straight" 179 | }, 180 | "secondaryOffset" : 100, 181 | "strategyPriorities" : { 182 | "type" : "CIMMaplexStrategyPriorities", 183 | "stacking" : 1, 184 | "overrun" : 2, 185 | "fontCompression" : 3, 186 | "fontReduction" : 4, 187 | "abbreviation" : 5 188 | }, 189 | "thinningDistanceUnit" : "Map", 190 | "truncationMarkerCharacter" : ".", 191 | "truncationMinimumLength" : 1, 192 | "truncationPreferredCharacters" : "aeiou" 193 | }, 194 | "name" : "Default", 195 | "priority" : 1, 196 | "standardLabelPlacementProperties" : { 197 | "type" : "CIMStandardLabelPlacementProperties", 198 | "featureType" : "Line", 199 | "featureWeight" : "None", 200 | "labelWeight" : "High", 201 | "numLabelsOption" : "OneLabelPerPart", 202 | "lineLabelPosition" : { 203 | "type" : "CIMStandardLineLabelPosition", 204 | "above" : true, 205 | "inLine" : true, 206 | "parallel" : true 207 | }, 208 | "lineLabelPriorities" : { 209 | "type" : "CIMStandardLineLabelPriorities", 210 | "aboveStart" : 3, 211 | "aboveAlong" : 3, 212 | "aboveEnd" : 3, 213 | "centerStart" : 3, 214 | "centerAlong" : 3, 215 | "centerEnd" : 3, 216 | "belowStart" : 3, 217 | "belowAlong" : 3, 218 | "belowEnd" : 3 219 | }, 220 | "pointPlacementMethod" : "AroundPoint", 221 | "pointPlacementPriorities" : { 222 | "type" : "CIMStandardPointPlacementPriorities", 223 | "aboveLeft" : 2, 224 | "aboveCenter" : 2, 225 | "aboveRight" : 1, 226 | "centerLeft" : 3, 227 | "centerRight" : 2, 228 | "belowLeft" : 3, 229 | "belowCenter" : 3, 230 | "belowRight" : 2 231 | }, 232 | "rotationType" : "Arithmetic", 233 | "polygonPlacementMethod" : "AlwaysHorizontal" 234 | }, 235 | "textSymbol" : { 236 | "type" : "CIMSymbolReference", 237 | "symbol" : { 238 | "type" : "CIMTextSymbol", 239 | "blockProgression" : "TTB", 240 | "compatibilityMode" : true, 241 | "depth3D" : 1, 242 | "drawSoftHyphen" : true, 243 | "extrapolateBaselines" : true, 244 | "flipAngle" : 90, 245 | "fontEffects" : "Normal", 246 | "fontEncoding" : "Unicode", 247 | "fontFamilyName" : "Arial", 248 | "fontStyleName" : "Regular", 249 | "fontType" : "Unspecified", 250 | "haloSize" : 1, 251 | "height" : 8, 252 | "hinting" : "Default", 253 | "horizontalAlignment" : "Center", 254 | "kerning" : true, 255 | "letterWidth" : 100, 256 | "ligatures" : true, 257 | "lineGapType" : "ExtraLeading", 258 | "shadowColor" : { 259 | "type" : "CIMRGBColor", 260 | "values" : [ 261 | 0, 262 | 0, 263 | 0, 264 | 100 265 | ] 266 | }, 267 | "symbol" : { 268 | "type" : "CIMPolygonSymbol", 269 | "symbolLayers" : [ 270 | { 271 | "type" : "CIMSolidFill", 272 | "enable" : true, 273 | "color" : { 274 | "type" : "CIMRGBColor", 275 | "values" : [ 276 | 0, 277 | 0, 278 | 0, 279 | 100 280 | ] 281 | } 282 | } 283 | ] 284 | }, 285 | "textCase" : "Normal", 286 | "textDirection" : "LTR", 287 | "verticalAlignment" : "Bottom", 288 | "verticalGlyphOrientation" : "Right", 289 | "wordSpacing" : 100, 290 | "billboardMode3D" : "FaceNearPlane" 291 | } 292 | }, 293 | "useCodedValue" : true, 294 | "visibility" : true, 295 | "iD" : -1 296 | } 297 | ], 298 | "renderer" : { 299 | "type" : "CIMSimpleRenderer", 300 | "patch" : "Default", 301 | "symbol" : { 302 | "type" : "CIMSymbolReference", 303 | "symbol" : { 304 | "type" : "CIMLineSymbol", 305 | "symbolLayers" : [ 306 | { 307 | "type" : "CIMVectorMarker", 308 | "enable" : true, 309 | "anchorPointUnits" : "Relative", 310 | "dominantSizeAxis3D" : "Z", 311 | "rotation" : 90, 312 | "size" : 2, 313 | "billboardMode3D" : "None", 314 | "markerPlacement" : { 315 | "type" : "CIMMarkerPlacementAlongLineSameSize", 316 | "angleToLine" : true, 317 | "controlPointPlacement" : "NoConstraint", 318 | "endings" : "NoConstraint", 319 | "offsetAlongLine" : 1.5, 320 | "placementTemplate" : [ 321 | 2 322 | ] 323 | }, 324 | "frame" : { 325 | "xmin" : -1, 326 | "ymin" : -1, 327 | "xmax" : 1, 328 | "ymax" : 1 329 | }, 330 | "markerGraphics" : [ 331 | { 332 | "type" : "CIMMarkerGraphic", 333 | "geometry" : { 334 | "paths" : [ 335 | [ 336 | [ 337 | 1, 338 | 0 339 | ], 340 | [ 341 | -1, 342 | 0 343 | ] 344 | ] 345 | ] 346 | }, 347 | "symbol" : { 348 | "type" : "CIMLineSymbol", 349 | "symbolLayers" : [ 350 | { 351 | "type" : "CIMSolidStroke", 352 | "enable" : true, 353 | "capStyle" : "Round", 354 | "joinStyle" : "Round", 355 | "lineStyle3D" : "Strip", 356 | "miterLimit" : 10, 357 | "width" : 1, 358 | "color" : { 359 | "type" : "CIMRGBColor", 360 | "values" : [ 361 | 0, 362 | 0, 363 | 0, 364 | 100 365 | ] 366 | } 367 | } 368 | ] 369 | } 370 | } 371 | ], 372 | "scaleSymbolsProportionally" : true, 373 | "respectFrame" : true 374 | } 375 | ] 376 | } 377 | } 378 | }, 379 | "scaleSymbols" : true, 380 | "snappable" : true 381 | } 382 | ], 383 | "binaryReferences" : [ 384 | { 385 | "type" : "CIMBinaryReference", 386 | "uRI" : "CIMPATH=Metadata/24c82a378d13647fa1d93b1e2aaf37ac.xml", 387 | "data" : "\r\n20200514142110001.0TRUEHash lineSymbology with hash line symbol.\r\n" 388 | } 389 | ], 390 | "rGBColorProfile" : "sRGB IEC61966-2-1 noBPC", 391 | "cMYKColorProfile" : "U.S. Web Coated (SWOP) v2" 392 | } -------------------------------------------------------------------------------- /test/data/arcgis/test.lyrx: -------------------------------------------------------------------------------- 1 | { 2 | "type" : "CIMLayerDocument", 3 | "version" : "2.5.0", 4 | "build" : 22081, 5 | "layers" : [ 6 | "CIMPATH=layers/countries2.xml" 7 | ], 8 | "layerDefinitions" : [ 9 | { 10 | "type" : "CIMFeatureLayer", 11 | "name" : "Countries", 12 | "uRI" : "CIMPATH=layers/countries2.xml", 13 | "sourceModifiedTime" : { 14 | "type" : "TimeInstant" 15 | }, 16 | "metadataURI" : "CIMPATH=Metadata/cdff59922ff1d6fe7d9407d4a7940880.xml", 17 | "useSourceMetadata" : false, 18 | "description" : "Features all labelled the same way, labels in uppercase.", 19 | "expanded" : true, 20 | "layerType" : "Operational", 21 | "showLegends" : true, 22 | "visibility" : true, 23 | "displayCacheType" : "Permanent", 24 | "maxDisplayCacheAge" : 5, 25 | "showPopups" : true, 26 | "serviceLayerID" : -1, 27 | "refreshRate" : -1, 28 | "refreshRateUnit" : "esriTimeUnitsSeconds", 29 | "autoGenerateFeatureTemplates" : true, 30 | "featureTable" : { 31 | "type" : "CIMFeatureTable", 32 | "displayField" : "NAME", 33 | "editable" : true, 34 | "timeFields" : { 35 | "type" : "CIMTimeTableDefinition" 36 | }, 37 | "timeDefinition" : { 38 | "type" : "CIMTimeDataDefinition" 39 | }, 40 | "timeDisplayDefinition" : { 41 | "type" : "CIMTimeDisplayDefinition", 42 | "timeInterval" : 0, 43 | "timeIntervalUnits" : "esriTimeUnitsHours", 44 | "timeOffsetUnits" : "esriTimeUnitsYears" 45 | }, 46 | "dataConnection" : { 47 | "type" : "CIMStandardDataConnection", 48 | "workspaceConnectionString" : "DATABASE=..\\..\\..\\map_data", 49 | "workspaceFactory" : "Shapefile", 50 | "dataset" : "countries_europe", 51 | "datasetType" : "esriDTFeatureClass" 52 | }, 53 | "studyAreaSpatialRel" : "esriSpatialRelUndefined", 54 | "searchOrder" : "esriSearchOrderSpatial" 55 | }, 56 | "htmlPopupEnabled" : true, 57 | "htmlPopupFormat" : { 58 | "type" : "CIMHtmlPopupFormat", 59 | "htmlUseCodedDomainValues" : true, 60 | "htmlPresentationStyle" : "TwoColumnTable" 61 | }, 62 | "isFlattened" : true, 63 | "selectable" : true, 64 | "selectionSymbol" : { 65 | "type" : "CIMSymbolReference", 66 | "symbol" : { 67 | "type" : "CIMPolygonSymbol", 68 | "symbolLayers" : [ 69 | { 70 | "type" : "CIMSolidStroke", 71 | "enable" : true, 72 | "capStyle" : "Round", 73 | "joinStyle" : "Round", 74 | "lineStyle3D" : "Strip", 75 | "miterLimit" : 10, 76 | "width" : 2, 77 | "color" : { 78 | "type" : "CIMRGBColor", 79 | "values" : [ 80 | 0, 81 | 255, 82 | 255, 83 | 100 84 | ] 85 | } 86 | } 87 | ] 88 | } 89 | }, 90 | "featureCacheType" : "None", 91 | "labelClasses" : [ 92 | { 93 | "type" : "CIMLabelClass", 94 | "expression" : "[NAME]", 95 | "expressionEngine" : "VBScript", 96 | "featuresToLabel" : "AllVisibleFeatures", 97 | "maplexLabelPlacementProperties" : { 98 | "type" : "CIMMaplexLabelPlacementProperties", 99 | "featureType" : "Line", 100 | "avoidPolygonHoles" : true, 101 | "canOverrunFeature" : true, 102 | "canPlaceLabelOutsidePolygon" : true, 103 | "canRemoveOverlappingLabel" : true, 104 | "canStackLabel" : true, 105 | "connectionType" : "Unambiguous", 106 | "constrainOffset" : "NoConstraint", 107 | "contourAlignmentType" : "Page", 108 | "contourLadderType" : "Straight", 109 | "contourMaximumAngle" : 90, 110 | "enableConnection" : true, 111 | "featureWeight" : 100, 112 | "fontHeightReductionLimit" : 4, 113 | "fontHeightReductionStep" : 0.5, 114 | "fontWidthReductionLimit" : 90, 115 | "fontWidthReductionStep" : 5, 116 | "graticuleAlignmentType" : "Straight", 117 | "labelBuffer" : 15, 118 | "labelLargestPolygon" : true, 119 | "labelPriority" : -1, 120 | "labelStackingProperties" : { 121 | "type" : "CIMMaplexLabelStackingProperties", 122 | "stackAlignment" : "ChooseBest", 123 | "maximumNumberOfLines" : 3, 124 | "minimumNumberOfCharsPerLine" : 3, 125 | "maximumNumberOfCharsPerLine" : 24 126 | }, 127 | "lineFeatureType" : "General", 128 | "linePlacementMethod" : "OffsetCurvedFromLine", 129 | "maximumLabelOverrun" : 36, 130 | "maximumLabelOverrunUnit" : "Point", 131 | "minimumFeatureSizeUnit" : "Map", 132 | "multiPartOption" : "OneLabelPerPart", 133 | "offsetAlongLineProperties" : { 134 | "type" : "CIMMaplexOffsetAlongLineProperties", 135 | "placementMethod" : "BestPositionAlongLine", 136 | "labelAnchorPoint" : "CenterOfLabel", 137 | "distanceUnit" : "Percentage", 138 | "useLineDirection" : true 139 | }, 140 | "pointExternalZonePriorities" : { 141 | "type" : "CIMMaplexExternalZonePriorities", 142 | "aboveLeft" : 4, 143 | "aboveCenter" : 2, 144 | "aboveRight" : 1, 145 | "centerRight" : 3, 146 | "belowRight" : 5, 147 | "belowCenter" : 7, 148 | "belowLeft" : 8, 149 | "centerLeft" : 6 150 | }, 151 | "pointPlacementMethod" : "AroundPoint", 152 | "polygonAnchorPointType" : "GeometricCenter", 153 | "polygonBoundaryWeight" : 200, 154 | "polygonExternalZones" : { 155 | "type" : "CIMMaplexExternalZonePriorities", 156 | "aboveLeft" : 4, 157 | "aboveCenter" : 2, 158 | "aboveRight" : 1, 159 | "centerRight" : 3, 160 | "belowRight" : 5, 161 | "belowCenter" : 7, 162 | "belowLeft" : 8, 163 | "centerLeft" : 6 164 | }, 165 | "polygonFeatureType" : "General", 166 | "polygonInternalZones" : { 167 | "type" : "CIMMaplexInternalZonePriorities", 168 | "center" : 1 169 | }, 170 | "polygonPlacementMethod" : "CurvedInPolygon", 171 | "primaryOffset" : 1, 172 | "primaryOffsetUnit" : "Point", 173 | "removeExtraWhiteSpace" : true, 174 | "repetitionIntervalUnit" : "Map", 175 | "rotationProperties" : { 176 | "type" : "CIMMaplexRotationProperties", 177 | "rotationType" : "Arithmetic", 178 | "alignmentType" : "Straight" 179 | }, 180 | "secondaryOffset" : 100, 181 | "strategyPriorities" : { 182 | "type" : "CIMMaplexStrategyPriorities", 183 | "stacking" : 1, 184 | "overrun" : 2, 185 | "fontCompression" : 3, 186 | "fontReduction" : 4, 187 | "abbreviation" : 5 188 | }, 189 | "thinningDistanceUnit" : "Map", 190 | "truncationMarkerCharacter" : ".", 191 | "truncationMinimumLength" : 1, 192 | "truncationPreferredCharacters" : "aeiou" 193 | }, 194 | "name" : "Default", 195 | "priority" : 7, 196 | "standardLabelPlacementProperties" : { 197 | "type" : "CIMStandardLabelPlacementProperties", 198 | "featureType" : "Line", 199 | "featureWeight" : "None", 200 | "labelWeight" : "High", 201 | "numLabelsOption" : "OneLabelPerPart", 202 | "lineLabelPosition" : { 203 | "type" : "CIMStandardLineLabelPosition", 204 | "above" : true, 205 | "inLine" : true, 206 | "parallel" : true 207 | }, 208 | "lineLabelPriorities" : { 209 | "type" : "CIMStandardLineLabelPriorities", 210 | "aboveStart" : 3, 211 | "aboveAlong" : 3, 212 | "aboveEnd" : 3, 213 | "centerStart" : 3, 214 | "centerAlong" : 3, 215 | "centerEnd" : 3, 216 | "belowStart" : 3, 217 | "belowAlong" : 3, 218 | "belowEnd" : 3 219 | }, 220 | "pointPlacementMethod" : "AroundPoint", 221 | "pointPlacementPriorities" : { 222 | "type" : "CIMStandardPointPlacementPriorities", 223 | "aboveLeft" : 2, 224 | "aboveCenter" : 2, 225 | "aboveRight" : 1, 226 | "centerLeft" : 3, 227 | "centerRight" : 2, 228 | "belowLeft" : 3, 229 | "belowCenter" : 3, 230 | "belowRight" : 2 231 | }, 232 | "rotationType" : "Arithmetic", 233 | "polygonPlacementMethod" : "AlwaysHorizontal", 234 | "placeOnlyInsidePolygon" : true 235 | }, 236 | "textSymbol" : { 237 | "type" : "CIMSymbolReference", 238 | "symbol" : { 239 | "type" : "CIMTextSymbol", 240 | "blockProgression" : "TTB", 241 | "compatibilityMode" : true, 242 | "depth3D" : 1, 243 | "drawSoftHyphen" : true, 244 | "extrapolateBaselines" : true, 245 | "flipAngle" : 90, 246 | "fontEffects" : "Normal", 247 | "fontEncoding" : "Unicode", 248 | "fontFamilyName" : "Arial", 249 | "fontStyleName" : "Regular", 250 | "fontType" : "Unspecified", 251 | "haloSize" : 1, 252 | "height" : 14, 253 | "hinting" : "Default", 254 | "horizontalAlignment" : "Left", 255 | "kerning" : true, 256 | "letterSpacing" : 56, 257 | "letterWidth" : 100, 258 | "ligatures" : true, 259 | "lineGapType" : "ExtraLeading", 260 | "shadowColor" : { 261 | "type" : "CIMRGBColor", 262 | "values" : [ 263 | 214, 264 | 213, 265 | 213, 266 | 100 267 | ] 268 | }, 269 | "shadowOffsetX" : 1, 270 | "shadowOffsetY" : -1, 271 | "symbol" : { 272 | "type" : "CIMPolygonSymbol", 273 | "symbolLayers" : [ 274 | { 275 | "type" : "CIMSolidFill", 276 | "enable" : true, 277 | "color" : { 278 | "type" : "CIMRGBColor", 279 | "values" : [ 280 | 107, 281 | 107, 282 | 108, 283 | 100 284 | ] 285 | } 286 | } 287 | ] 288 | }, 289 | "textCase" : "Allcaps", 290 | "textDirection" : "LTR", 291 | "verticalAlignment" : "Bottom", 292 | "verticalGlyphOrientation" : "Right", 293 | "wordSpacing" : 100, 294 | "billboardMode3D" : "FaceNearPlane" 295 | } 296 | }, 297 | "useCodedValue" : true, 298 | "visibility" : true, 299 | "iD" : -1 300 | } 301 | ], 302 | "labelVisibility" : true, 303 | "renderer" : { 304 | "type" : "CIMSimpleRenderer", 305 | "patch" : "Default", 306 | "symbol" : { 307 | "type" : "CIMSymbolReference", 308 | "symbol" : { 309 | "type" : "CIMPolygonSymbol", 310 | "symbolLayers" : [ 311 | { 312 | "type" : "CIMSolidStroke", 313 | "enable" : true, 314 | "capStyle" : "Round", 315 | "joinStyle" : "Round", 316 | "lineStyle3D" : "Strip", 317 | "miterLimit" : 10, 318 | "width" : 0.40000000000000002, 319 | "color" : { 320 | "type" : "CIMRGBColor", 321 | "values" : [ 322 | 110, 323 | 110, 324 | 110, 325 | 100 326 | ] 327 | } 328 | }, 329 | { 330 | "type" : "CIMSolidFill", 331 | "enable" : true, 332 | "color" : { 333 | "type" : "CIMRGBColor", 334 | "values" : [ 335 | 204, 336 | 204, 337 | 204, 338 | 100 339 | ] 340 | } 341 | } 342 | ] 343 | } 344 | } 345 | }, 346 | "scaleSymbols" : true, 347 | "snappable" : true 348 | } 349 | ], 350 | "binaryReferences" : [ 351 | { 352 | "type" : "CIMBinaryReference", 353 | "uRI" : "CIMPATH=Metadata/cdff59922ff1d6fe7d9407d4a7940880.xml", 354 | "data" : "\r\n20200514142027001.0TRUECountriesFeatures all labelled the same way, labels in uppercase.\r\n" 355 | } 356 | ], 357 | "rGBColorProfile" : "sRGB IEC61966-2-1 noBPC", 358 | "cMYKColorProfile" : "U.S. Web Coated (SWOP) v2" 359 | } -------------------------------------------------------------------------------- /test/data/qgis/line/basic.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testdata", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Line", 9 | "color": "#becf50", 10 | "opacity": 1.0, 11 | "width": 0.982677165366, 12 | "perpendicularOffset": 0.0, 13 | "cap": "square", 14 | "join": "bevel" 15 | } 16 | ] 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /test/data/qgis/line/basic.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 1 6 | 1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 0 47 | 0 48 | 1 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 76 | 77 | 78 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 0 119 | 120 | 137 | 0 138 | generatedlayout 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | fid 149 | 150 | 1 151 | 152 | -------------------------------------------------------------------------------- /test/data/qgis/line/labeloffset.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testdata", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Line", 9 | "color": "#becf50", 10 | "opacity": 1.0, 11 | "width": 0.982677165366, 12 | "perpendicularOffset": 0.0, 13 | "cap": "square", 14 | "join": "bevel" 15 | } 16 | ] 17 | }, 18 | { 19 | "symbolizers": [ 20 | { 21 | "kind": "Text", 22 | "perpendicularOffset": 5.0, 23 | "color": "#000000", 24 | "font": "MS Shell Dlg 2", 25 | "label": [ 26 | "PropertyName", 27 | "text" 28 | ], 29 | "size": 10.0 30 | } 31 | ], 32 | "name": "labeling" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /test/data/qgis/line/labeloffset.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 1 6 | 1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 1 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 93 | 94 | 95 | 96 | 97 | 98 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 0 139 | 140 | 157 | 0 158 | generatedlayout 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | fid 169 | 170 | 1 171 | 172 | -------------------------------------------------------------------------------- /test/data/qgis/line/testlayer.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoCat/bridge-style/fef7b784b1630bbe21b3691fc1709860d6472747/test/data/qgis/line/testlayer.gpkg -------------------------------------------------------------------------------- /test/data/qgis/points/else_rule.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 184 | 185 | 186 | 187 | 188 | 189 | 0 190 | 0 191 | 0 192 | 193 | -------------------------------------------------------------------------------- /test/data/qgis/points/fontmarker.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "points", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Mark", 9 | "color": "#b7484b", 10 | "wellKnownName": "ttf://Wingdings#0x5e", 11 | "size": 7.5590551182 12 | } 13 | ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/data/qgis/points/fontmarker.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 0 40 | 41 | -------------------------------------------------------------------------------- /test/data/qgis/points/fontmarker2chars.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "points", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Text", 9 | "size": 7.5590551182, 10 | "label": "^a", 11 | "font": "Wingdings", 12 | "color": "#b7484b" 13 | } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /test/data/qgis/points/fontmarker2chars.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 0 40 | 41 | -------------------------------------------------------------------------------- /test/data/qgis/points/heatmap.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 0 12 | 0 13 | 0 14 | 15 | -------------------------------------------------------------------------------- /test/data/qgis/points/heatmap_color.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 0 12 | 0 13 | 0 14 | 15 | -------------------------------------------------------------------------------- /test/data/qgis/points/honeycomb.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 0 67 | 0 68 | 69 | -------------------------------------------------------------------------------- /test/data/qgis/points/label_buffer.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | 0 59 | 0 60 | 0 61 | 62 | -------------------------------------------------------------------------------- /test/data/qgis/points/labels_offset.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 56 | 57 | 58 | 0 59 | 0 60 | 0 61 | 62 | -------------------------------------------------------------------------------- /test/data/qgis/points/map_units.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 0 40 | 0 41 | 42 | -------------------------------------------------------------------------------- /test/data/qgis/points/nosymbols.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "points", 3 | "rules": [] 4 | } -------------------------------------------------------------------------------- /test/data/qgis/points/nosymbols.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0 5 | 0 6 | 0 7 | 8 | -------------------------------------------------------------------------------- /test/data/qgis/points/offset.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "opacity": 1.0, 9 | "rotate": 0.0, 10 | "offset": [ 11 | 10.0, 12 | -10.0 13 | ], 14 | "kind": "Mark", 15 | "color": "#729b6f", 16 | "wellKnownName": "circle", 17 | "size": 7.5590551182, 18 | "strokeColor": "#232323", 19 | "strokeWidth": 1, 20 | "strokeOpacity": 1.0, 21 | "fillOpacity": 1.0 22 | } 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /test/data/qgis/points/offset.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1 5 | 1 6 | 1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 0 49 | 0 50 | 1 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 79 | 80 | 81 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 0 122 | 123 | 140 | 0 141 | generatedlayout 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | fid 152 | 153 | 0 154 | 155 | -------------------------------------------------------------------------------- /test/data/qgis/points/rotated_font_marker_and_circle.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 0 65 | 0 66 | 0 67 | 68 | -------------------------------------------------------------------------------- /test/data/qgis/points/rotated_svg.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 0 40 | 41 | -------------------------------------------------------------------------------- /test/data/qgis/points/rule_no_filter.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 0 40 | 0 41 | 0 42 | 43 | -------------------------------------------------------------------------------- /test/data/qgis/points/simplemarker.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "points", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "opacity": 1.0, 9 | "rotate": 0.0, 10 | "kind": "Mark", 11 | "color": "#e77148", 12 | "wellKnownName": "circle", 13 | "size": 7.5590551182, 14 | "strokeColor": "#232323", 15 | "strokeWidth": 1 16 | } 17 | ] 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /test/data/qgis/points/simplemarker.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 0 40 | 0 41 | 42 | -------------------------------------------------------------------------------- /test/data/qgis/points/size_based_on_expression.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 0 45 | 0 46 | 0 47 | 48 | -------------------------------------------------------------------------------- /test/data/qgis/points/size_based_on_property.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 0 45 | 0 46 | 0 47 | 48 | -------------------------------------------------------------------------------- /test/data/qgis/points/testlayer.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoCat/bridge-style/fef7b784b1630bbe21b3691fc1709860d6472747/test/data/qgis/points/testlayer.gpkg -------------------------------------------------------------------------------- /test/data/qgis/points/transparentfill.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer points", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "opacity": 1.0, 9 | "rotate": 0.0, 10 | "kind": "Mark", 11 | "color": "#987db7", 12 | "wellKnownName": "circle", 13 | "size": 7.5590551182, 14 | "strokeColor": "#232323", 15 | "strokeWidth": 1, 16 | "strokeOpacity": 1.0, 17 | "fillOpacity": 0.0 18 | } 19 | ] 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/data/qgis/points/transparentfill.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 0 40 | 0 41 | 42 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/geometry_generator_centroid.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 2 54 | 55 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/outline_simple_line.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 2 39 | 40 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/outline_simple_line_with_offset.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 0 37 | 0 38 | 2 39 | 40 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/point_pattern_fill.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 0 70 | 0 71 | 2 72 | 73 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/point_pattern_fill_svg.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 0 69 | 0 70 | 2 71 | 72 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "color": "#e15989", 11 | "outlineColor": "#232323", 12 | "outlineWidth": 3.7795275591 13 | } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_cross_hatch.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_dash_outline.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "color": "#e15989", 11 | "outlineColor": "#232323", 12 | "outlineWidth": 0.982677165366, 13 | "outlineDasharray": "5 2" 14 | } 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_dash_outline.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_empty_fill.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "outlineColor": "#232323", 11 | "outlineWidth": 1 12 | } 13 | ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_empty_fill.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_hairstyle_width.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "color": "#e15989", 11 | "outlineColor": "#232323", 12 | "outlineWidth": 1 13 | } 14 | ] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_hairstyle_width.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_horline_fill.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "graphicFill": [ 11 | { 12 | "kind": "Mark", 13 | "color": "#e15989", 14 | "wellKnownName": "horline", 15 | "size": 10, 16 | "strokeColor": "#e15989", 17 | "strokeWidth": 1, 18 | "rotate": 0 19 | } 20 | ], 21 | "graphicFillDistanceX": 5.0, 22 | "graphicFillDistanceY": 5.0, 23 | "outlineColor": "#232323", 24 | "outlineWidth": 0.982677165366 25 | } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_horline_fill.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_noborder.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "color": "#e15989" 11 | } 12 | ] 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_noborder.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_nofill.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testlayer", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "outlineColor": "#232323", 11 | "outlineWidth": 0.982677165366 12 | } 13 | ] 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/simple_nofill.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 0 32 | 0 33 | 2 34 | 35 | -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/testlayer.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoCat/bridge-style/fef7b784b1630bbe21b3691fc1709860d6472747/test/data/qgis/single_polygon/testlayer.gpkg -------------------------------------------------------------------------------- /test/data/qgis/single_polygon/two_simple_fill_symbol_layers.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 0 52 | 0 53 | 2 54 | 55 | -------------------------------------------------------------------------------- /test/data/sample.geostyler: -------------------------------------------------------------------------------- 1 | { 2 | "name": "World Map", 3 | "rules": [ 4 | { 5 | "name": "", 6 | "symbolizers": [ 7 | { 8 | "kind": "Fill", 9 | "opacity": 1.0, 10 | "color": "#cfcfca", 11 | "outlineColor": "#232323", 12 | "outlineWidth": 0.982677165366 13 | } 14 | ] 15 | }, 16 | { 17 | "name": "", 18 | "symbolizers": [ 19 | { 20 | "opacity": 1.0, 21 | "rotate": 0.0, 22 | "kind": "Mark", 23 | "color": "#b80808", 24 | "wellKnownName": "star", 25 | "size": 24.94488189006, 26 | "strokeColor": "#b80808", 27 | "strokeWidth": 0.75590551182, 28 | "Geometry": [ 29 | "centroid", 30 | [ 31 | "PropertyName", 32 | "geom" 33 | ] 34 | ] 35 | }, 36 | { 37 | "opacity": 1.0, 38 | "rotate": 34.0, 39 | "kind": "Mark", 40 | "color": "#ff0000", 41 | "wellKnownName": "star", 42 | "size": 27.21259842552, 43 | "strokeColor": "#ff0000", 44 | "strokeWidth": 0.75590551182, 45 | "Geometry": [ 46 | "centroid", 47 | [ 48 | "PropertyName", 49 | "geom" 50 | ] 51 | ] 52 | } 53 | ], 54 | "filter": [ 55 | "Or", 56 | [ 57 | "PropertyIsEqualTo", 58 | [ 59 | "PropertyName", 60 | "NAME" 61 | ], 62 | "India" 63 | ], 64 | [ 65 | "PropertyIsEqualTo", 66 | [ 67 | "PropertyName", 68 | "NAME" 69 | ], 70 | "China" 71 | ] 72 | ] 73 | }, 74 | { 75 | "name": "", 76 | "symbolizers": [ 77 | { 78 | "opacity": 1.0, 79 | "rotate": 0.0, 80 | "kind": "Mark", 81 | "color": "#ffffff", 82 | "wellKnownName": "hexagon", 83 | "size": [ 84 | "Mul", 85 | 3.7795275591, 86 | [ 87 | "Div", 88 | [ 89 | "PropertyName", 90 | "POP_EST" 91 | ], 92 | 10000000 93 | ] 94 | ], 95 | "strokeColor": "#fa8b39", 96 | "strokeWidth": 3.7795275591, 97 | "Geometry": [ 98 | "centroid", 99 | [ 100 | "PropertyName", 101 | "geom" 102 | ] 103 | ] 104 | }, 105 | { 106 | "opacity": 1.0, 107 | "rotate": 0.0, 108 | "kind": "Mark", 109 | "color": "#fab07c", 110 | "wellKnownName": "hexagon", 111 | "size": [ 112 | "Mul", 113 | 3.7795275591, 114 | [ 115 | "Mul", 116 | 0.409091, 117 | [ 118 | "Div", 119 | [ 120 | "PropertyName", 121 | "POP_EST" 122 | ], 123 | 10000000 124 | ] 125 | ] 126 | ], 127 | "strokeColor": "#fab07c", 128 | "strokeWidth": 0.75590551182, 129 | "Geometry": [ 130 | "centroid", 131 | [ 132 | "PropertyName", 133 | "geom" 134 | ] 135 | ] 136 | } 137 | ], 138 | "filter": [ 139 | "Not", 140 | [ 141 | "Or", 142 | [ 143 | "PropertyIsEqualTo", 144 | [ 145 | "PropertyName", 146 | "NAME" 147 | ], 148 | "India" 149 | ], 150 | [ 151 | "PropertyIsEqualTo", 152 | [ 153 | "PropertyName", 154 | "NAME" 155 | ], 156 | "China" 157 | ] 158 | ] 159 | ] 160 | } 161 | ] 162 | } -------------------------------------------------------------------------------- /test/qgistogeostyler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import unittest 4 | 5 | from qgis.core import QgsRasterLayer, QgsVectorLayer 6 | 7 | from bridgestyle import qgis 8 | 9 | 10 | class QgisToStylerTest(unittest.TestCase): 11 | pass 12 | 13 | 14 | _layers = {} 15 | 16 | 17 | def load_layer(file): 18 | if file not in _layers: 19 | name = os.path.basename(file) 20 | layer = QgsRasterLayer(file, "testlayer", "gdal") 21 | if not layer.isValid(): 22 | layer = QgsVectorLayer(file, "testlayer", "ogr") 23 | _layers[file] = layer 24 | return _layers[file] 25 | 26 | 27 | def test_function(datafile, stylefile, expected): 28 | def test(self): 29 | layer = load_layer(datafile) 30 | layer.loadNamedStyle(stylefile) 31 | geostyler, icons, warnings = qgis.togeostyler.convert(layer) 32 | expected[ 33 | "name" 34 | ] = "testlayer" # just in case the expected geostyler was created 35 | # from a layer with a different name 36 | self.assertEqual(geostyler, expected) 37 | 38 | return test 39 | 40 | 41 | def run_tests(): 42 | """ 43 | This methods dinamically create tests based on the contents of the 'qgis' 44 | subfolder. 45 | The structure of files in the subfolder must be as follows: 46 | - For each dataset that you want to use, create a subfolder under 'qgis' 47 | - Add the layer file in that subfolder. It must be named 'testlayer.gpkg' 48 | or 'testlayer.tiff' depending of whether it is a vector or a raster layer 49 | - In the same subfolder, along with the layer file, you can add as many 50 | qml files as you want to test. The names of these files will be used 51 | to set the name of the corresponding test 52 | - For each qml file, a .geostyler file with the same name must exist in 53 | the subfolder. It must contain the expected geostyler representation 54 | of the style in the qml file. 55 | - The test will load the testlayer file, assign the qml to it, generate 56 | a geostyler representation from it, and then compare to the expected 57 | geostyler result. 58 | """ 59 | suite = unittest.TestSuite() 60 | main_folder = os.path.join(os.path.dirname(__file__), "data", "qgis") 61 | for subfolder in os.listdir(main_folder): 62 | datafile = os.path.join(main_folder, subfolder, "testlayer.gpkg") 63 | if not os.path.exists(datafile): 64 | datafile = os.path.join(main_folder, subfolder, "testlayer.tiff") 65 | subfolder_path = os.path.join(main_folder, subfolder) 66 | for style in os.listdir(subfolder_path): 67 | if style.lower().endswith("qml"): 68 | stylefile = os.path.join(subfolder_path, style) 69 | name = os.path.splitext(stylefile)[0] 70 | expectedfile = name + ".geostyler" 71 | with open(expectedfile) as f: 72 | expected = json.load(f) 73 | setattr( 74 | QgisToStylerTest, 75 | "test_" + name, 76 | test_function(datafile, stylefile, expected), 77 | ) 78 | 79 | suite = unittest.defaultTestLoader.loadTestsFromTestCase(QgisToStylerTest) 80 | unittest.TextTestRunner().run(suite) 81 | 82 | 83 | if __name__ == "__main__": 84 | run_tests() 85 | --------------------------------------------------------------------------------