#!/usr/bin/env python
# coding: UTF-8
'''
Catacombs maps using SVG source map with codes inside it.
The program allows to produce:
* 2D "readable" maps with symbols changed to larger ones, second level shifted to avoid superimposition of corridors, enlarged zooms, shadowing etc.
* 3D maps to be used in a 3D visualization program, a webGL server, or the CataZoom app.
Requirements
============
* Having inkscape installed on the system and available in the PATH.
A recent version of inkscape (1.0 at least) is recommended to avoid units and
scaling problems.
**Inkscape is needed for 2D maps renderings**, it is not needed for the 3D
mode.
* Either (**for 2D maps**):
* the Pillow (PIL) python module (see later)
* or ImageMagick "convert" tool to convert PNG to JPEG. If Pillow is present,
then convert will not be
used.
**Warning:** https://github.com/ImageMagick/ImageMagick/issues/396
ImageMagick cache (disk limit) size is too small.
Edit /etc/ImageMagick-6/policy.xml and change disk resource limit::
<policy domain="resource" name="memory" value="12GiB"/>
<policy domain="resource" name="map" value="20GiiB"/>
<policy domain="resource" name="width" value="50KP"/>
<policy domain="resource" name="height" value="50KP"/>
<policy domain="resource" name="area" value="20GiB"/>
<policy domain="resource" name="disk" value="80GiB"/>
Python modules:
These can be installed using the command::
python -m pip install numpy scipy Pillow
* :mod:`~catamap.svg_to_mesh` submodule and its requirements (part of this
project)
* xml ElementTree
* numpy
* scipy
* Pillow (PIL) optionally for PNG/JPEG image conversion. Otherwise ImageMagick
"convert" tool will be used (see above)
* pyclipper, used in **2D maps** and only when **zoomed zones** are used. You
can install it via pip.
The 3D part has additional requirements:
* soma.aims (https://github.com/brainvisa/aims-free)
* anatomist (https://github.com/brainvisa/anatomist-free and
https://github.com/brainvisa/anatomist-gpl)
* json
The maps differences tool (:mod:`~catamap.diff_svg`) also needs:
* yaml (pip module ``pyyaml``)
Usage
=====
* set the ``python`` subdirectory of the project in your ``PYTHONPATH`` environment variable. Under Unix sh/bash shells, this would be::
export PYTHONPATH="~/fdc_catamaps/python:$PYTHONPATH"
(it can be set in a ``.bash_profile`` or ``.bashrc`` init file)
* get or make the source SVG file with codes inside, for instance ``plan_14_fdc_2021_04_29.svg``
main
* go to the directory containing it
* run the module ``catamap`` as a program::
python -m catamap --2d plan_14_fdc_2021_04_29.svg
It should work using python3 (python2 support has been dropped).
The 2D maps options will produce files with suffixes in the current directory:
modified .svg files, .pdf and .jpg files.
The 3D maps option will produce meshes in a subdirectory.
Use the commandline with the '-h' option to get all parameters help.
Inkscape SVG files
==================
SVG files may (should) contain additional XML properties that can be set in the XML editor in the Inkscape software. They allow to specify how objects should be parsed, grouped, represented etc. For now they are mainly used in the 3D part, but the 2D part also uses ``level``, ``label``, and ``private`` information information.
Codes correspond to properties used in the program, and can be set in XML elements in the SVG file.
Bool properties can be written ``true``, ``True``, ``1``, or ``false``, ``False``, ``0``.
Properties are inherited from parent (group/layer) to children. Thus if a layer has the property ``corridor`` set to ``true``, all objects in this layer will be marked as ``corridor``.
In the program, elements are grouped into meshes: a mesh will be the concatenation of all elements of the same kind, to avoid having dozens of thousands of mesh files to load. An object "kind" (a group making a single mesh) is identified by several of its properties, but not all. Namely the identifier is::
<label>_<level>_<public/private>_<accessible/inaccessible>[_<category>]
Other properties are not discriminative, so in some conditions may not be taken into account. For instance if a layer defines a label, level, public and accessible states, and marks items as corridors, if one element inside adds a property ``block``, it will be part of the same mesh group anyway, and the ``block`` property will be ignored.
Properties list
---------------
**alt_colors:** JSON dict (**2D and 3D maps**)
color to be used alternatively in maps. This is a string representing a
JSON format dictionary. Keys are colorsets names, values are a second level
of dictionary which specifies color properties, a bit like for SVG elements
style. The
default colorset for 3D maps is ``map_3d``.
Ex::
{"map_3d": "#cccccc4c"}
see also: :ref:`colorsets`
**arrow:** bool (**3D maps**)
arrows join a text label to a location on map. They are filar meshes. Most
of them are just a segment (2 points) but they may contain more points.
Points orientation is important as the arrow goes from the text (above the
meshes) down to the pointed item below.
**arrow_base_height_shift:** float (**3D maps**)
z shift of the base of an arrow element in the upper layer depth. The arrow
head shift is specified using height_shift.
**block:** bool (**3D maps**)
block elements have a ceiling and walls. Meshes are extruded and tesselated
to be filled.
**border:** bool (**3D maps**)
not used if I remember...
**camera_light:** str (**3D maps**)
if set to ``off``, the camera light (headlamp) is disabled.
**category:** str (**3D maps**)
the category string the element will be associated in the 3D views.
Categories can be displayed/hidden using buttons or menus in 3D views.
**catflap:** bool (**3D maps**)
In catflaps, paths are replaced with striped tubes.
**ceil_texture:** JSON dict (**3D maps**)
Texture definition for the ceiling part of elements. See :ref:`texturing`
below for details.
.. _colorsets_prop:
**colorsets:** JSON dict (**2D maps**)
used as a property of the ``metadata`` layer only, it specifies which
default colorset is associated with specific map types, for instance::
{"private": "black", "igc": "igc"}
see also: :ref:`colorsets`
**colorset_inheritance:** JSON dict (**2D and 3D maps**)
used as a property of the ``metadata`` layer only, it specifies colorsets
which can inherit the colors defined by another, in order to minimally
specialize it.
see also: :ref:`colorsets`
**contrast_floor:** str (**3D maps**)
if true (which is the default), corridor or block top and bottom meshes are
contrasted compared to wall meshes: the wall colors (given as the color
properties) are lightened or darkened before being assigned to floor or
ceilings. If you don't want this, set this property to false.
A third value is allowed: ``if_no_tex``, meaning that contrasting colors is
enabled only in non-textured mode (if the commandline option ``--texture``
is not set).
**corridor:** bool (**3D maps**)
corridors have a floor and walls. Meshes are extruded and tesselated to be
filled.
**date:** str (**2D maps**)
the date text will be replaced with the date of the map build.
**default_categories:** JSON list (**3D maps**)
list of categories which are visible by default (checked in the viewer)
**depth_map:** bool (**3D maps**)
the layer is a depth map. It should contain a ``level`` property to define
the level it maps. See :ref:`depth_maps` for details.
**floor_texture:** JSON dict (**3D maps**)
Texture definition for the floor part of elements. See :ref:`texturing`
below for details.
**glabel:** str (**2D maps**)
"group label" used to replace elements with ones from the legends layer.
The ``glabel`` is associated to one legends element if the legends element
``label`` property has the value of the ``glabel`` property of the element
plus the duffix ``_proto``. Ex, in the legends::
label: colim_proto
in the map elements::
glabel: colim
**gltf_properties:** JSON dict (**3D maps**)
optionally set on texture image elements, they specify how the texture
image is mapped and repeaded on the meshes. See :ref:`texturing` for
details.
**height_map:** bool (**3D maps**)
the layer is a height map. This is the same as **depth_map** except that
heights are inverted compared to depths, and that a height map may be
relative to another depth map (see the ``relative_to`` property). All
related attributes are the same as for **depth_map** otherwise.
**height_shift:** float (**3D maps**)
z shift of the element (esp. for corridor, block, wall elements, but also
wells and arrows). An element with a height shift will not begin on the
ground, but above or below as specified.
**hidden:** bool (**2D and 3D maps**)
removed from 3D maps.
**inaccessible:** bool (**3D maps**)
inaccessible elements will be separated from others, and set in the
``Inaccessible`` display category.
**item_height:** float (**3D maps**)
height of the element (esp. for corridor, block, wall elements).
**WARNING:** it is ``item_height``, not ``height`` as height is already an
official SVG attribute for some elements (rectangles for instance).
**label:** str (**2D and 3D maps**)
name of the element type
**label_alt_colors:** JSON dict (**2D and 3D maps**)
Like alt_colors, except that the dict has an upper-level which keys area
object labels. The dict can be applied hierarchically, thus put in a layer.
Ex::
{"repetiteur": {"black": {"bg": "#cccccc4c"}}}
see also: :ref:`colorsets`
**legend:** bool (**2D maps**)
set on a layer, indicates that this layer contains the legend symbols and
can be searched for replacement symbols (wells, etc) which will replace
those in the map to enlarge them and ease view from a bit further.
**level:** str (**2D and 3D maps**)
depth level name (sup, inf, surf, tech, metro, esc...). Levels are an
"open" property, there is no predefined list of levels, apart for 2
exceptions:
* items which don't specify a level are set by default to a level named
``sup`` (generally superior corridor level).
* a ``surf`` level represents the ground surface, and is automatically
built. It is flat, altitude 0 by default, but can be associated to an
altitude map and geolocalisation information. See :ref:`altitude_maps`
later.
Levels are associated to depth maps, so for each level used (except
``surf``), a ``depth_map`` layer is supposed to be defined. See
:ref:`depth_maps`.
**main_clip_rect_id:** JSON dict (**2D maps**)
set in the Inkscape metadata layer, it indicates the clip rectangle ID (in
the XML elements IDs) for each map type. A default (fallback) one can be
given under the key ``default`` in the dict. Ex::
{"default": "clip_general", "private": "clip_all"}
so that, in this example, the ``private`` map gets a different field of
view from the other ones.
**map_transform:** SVG transformation spec (**2D maps**)
additional transformation applied to elements, used for
instance to shift lower level parts when they are superimposed under an
upper level. Without this shift, they would not be seen because they are
hidden by the upper level. The transform is specified in the same way as
the standard SVG ``transform`` property.
This propery can be set to paths, groups, and arrows (which will thus point
to a shifted location).
**maps_def:** JSON dict (**2D maps**)
**In the SVG metadtadata layer**.
Definition of 2D maps types (to be used with the -m commandline option).
See :ref:`maps types later <maps_types>` for details.
**marker:** str (**3D maps**)
set on a layer to specify that this layer is a **markers layer**. Three
types of markers are currently recognized: ``sounds``, ``photos``, and
``lights``.
See :ref:`markers` for more details.
**markers_base_url:** bool (**3D maps**)
set on a :ref:`marker layer <markers>` to specify that marekrs in this
layer have URLS which point to the server directory specified here.
Individual markers will be appended to this base URL. The URL may be
absolute (``https://photos.google.com/xyzsomething``) or relative to the
map server (``photos/photo_dir``). If not specified, the default base url
is the markers type (``photos``, ``sounds``), in relative mode.
**markers_map:** str (**3D maps**)
Set on a :ref:`marker layer <markers>`: correspondence map file name (CSV
file) for markers text. See :ref:`markers` for more details.
**non_visibility:** str (**2D maps**)
list of maps types (json-like), for which this layer is removed. The
inverse of **visibility** (see below).
**private:** bool (**2D and 3D maps**)
private elements will only be visible if a code is provided
**proto_scale:** float (**2D maps**)
Only in the legend layers elements which are prototypes for symbols
replacements, this is the scale to be applied when replacing. The default
scale is 0.5 (replaced elements will be half the size they have in the
legend).
**radius:** float (**3D maps)
used in a "marker" layer to specify the max radius between the user click
and the marker position for it to be triggered. It can be set on the marker
layer itself, or on any marker item inside.
See :ref:`markers` for more details.
**relative_to:** str (**3D maps**)
for height maps or depth maps layers, specify to which depth map the height
is relative to. The default is surface depth (which means 0 or the ground
altitude map).
**replace_children:** bool (**2D maps**)
should be set only in replacement symbols in the legend layer (wells signs,
etc) to indicate that children of matching elements should be parsed
recursively and their children may be replaced also.
**shadow:** bool (**2D maps**)
marks the element (or layer...) to have a halo / shadow around it
**shadow_scale:** float (2D maps)
in the metadata layer, sets a global shadow scale applied to all shadow
operations.
**symbol:** bool (**3D maps**)
not sure it is used, after all...
**symbol_scale:** float (**3D maps**)
in the metadata layer, scale of symbol meshes (markers, fontis, etc)
**text:** bool (**3D maps**)
text layers.
**texture:** JSON dict (**3D maps**)
Texture definition for elements. See :ref:`texturing`
below for details.
**title:** bool (**3D maps**)
The title string(s) will be used and displayed in the web site title.
**travel_ref_level:** str (**3D maps**)
depth level used to define the 3D ground level. It is used to adapt the
travel speed scale (for 3D controls) depending to the altitude of the
camera to this level. Up to now the level is assimilated to its average
altitude, but this could be refined to approximate a plane, if needed.
The default is the ``sup`` level (default level for all elements).
**travel_speed_base:** float (**3D maps**)
Travel speed factor for 3D controls at the altitude of the ``travel_level``
altitude, which is the minimum speed. The default is **0.03**.
**travel_speed_alt_factor:** float (**3D maps**)
Scale factor applied to the distance to the ``travel level`` to adapt the
travel speed: speed increases with this "elevation" according to this
factor. The default is: **0.003**.
**transform_3d:** str( **3D maps**)
3D transformation, used to rotate an object in space. The spec is similar
to SVG transformations specs, except that it has one more dimension, and
the "matrix()" spec becomes "matrix4()", with 12 parameters (columns of the
matrix).
**upper_level:** str (**3D maps**)
depth level for the top of elements. Only used for elements joining two
levels (wells, arrows)
**use_height_map:** str (**3D maps**)
When set on a corridor, block or wall layer, it replaces the constant
**item_height** when extruding the height with a height map whose level is
given by the value of this property.
**visibility:** str (**2D and 3D maps**)
may be either: ``private``: alternative to ``private: true``, or a list of
maps types (json-like), for which this layer is kept visible. If such a
list is specified, then maps types not listed here will drop the layer.
As ``visibility`` has now a broader meaning, it is better to specify
``private: true`` for private things, to avoid ambiguities.
**wall:** bool (**3D maps**)
wall elements have only side walls (no floor or ceiling).
**wall_texture:** JSON dict (**3D maps**)
Texture definition for the walls part of elements. See :ref:`texturing`
below for details.
**well:** bool (**3D maps**)
wells are replaced with custom elements, which type is the element label
(PE, PS, PSh, échelle, sans, P ossements, ...). Well elements (or groups,
or layers) shoud specify the level and upper_level.
**well_read_mode:** str (**3D maps**)
tells if well elements should be read in the XML file as a single "path"
element (a circle for instance), or a group (a grop containing a circle,
and additional lines). Thus allowed values are ``path`` or ``group``.
**z_scale:** float or "auto" (**3D maps)
Can be set **in the SVG metadtadata layer**. This scale factor applies to
depths and heights in order to match x, y scalings which may be arbitraty.
The default is 0.5 and is also arbitrary. If it is set to "auto", then if
**lambert93** coordinates are provied in the appropriate layer, the scale
factor will be processed according to these "true" coordinates to match x
and y scales.
**zoom_area_id:** str (**2D maps**)
Identifier for a zoomed area zone layer. Should be present in a layer
containing a polygon which defines a source region. The identifier is also
present in a target zoom region layer, see ``zoom_id``.
See also :ref:`zoomed regions` for details.
**zoom_hidden:** bool (**2D maps**)
Layers marked with this will be excluded from zoomed areas.
See also :ref:`zoomed regions` for details.
*zoom_id:** str (**2D maps**)
Identifier for a zoomed area zone layer. Should be present in a layer
containing a rectangle for the target zoomed region. The identifier is also
present in a source zoom region polygon layer, see ``zoom_area_id``.
See also :ref:`zoomed regions` for details.
.. _maps_types:
Maps types
----------
Most common types, such as "public", "private" are (for now at least) builtin
in the program, but the ``maps_def`` property dict in the metadata layer allows
to define new maps types associated with specific filters. Each map type is a
dictionary which defines the following:
Ex::
{
"igcportail": {
"name": "igcportail",
"filters": ["igc_private"],
"shadows": false,
"do_pdf": false,
"do_jpg": false,
},
}
name: str
name of the map type, usually the same as the map type key.
filters: list
list of filters used for this map type. Several filters are available in
the program. The filters list is available using the ``--list-filters``
option of the commandline.
shadows: bool
if true, shadows are applied in layers or objects which define the
``shadow`` property. If false, shadows are disabled.
do_pdf: bool
if false, PDF output is not processed.
do_jpg: bool
if false, JPEG or TIFF bitmap output is not processed.
.. _depth_maps:
Depth maps
----------
Depth maps are layers containing depth information, **and nothing else**.
Each depth information specifies the depth of a specifi point in the map. They can be specified in several ways:
* a *text element*: the position of the element (its center if the text is centered) will be used as the position. The text should specify a floating point number which is the depth at this position.
* a group containing a text element and a segment path: the text is a float number specifying the depth, and the segment (2 points, oriented from the text toward the application point) specifies where the depth applied: the end pont of the segment will be assigned the given depth.
* a rectangle: this was originally the way to specify depth, but it proved to be difficult to read and is thus obsolete. Prefer the two above methods. In 2 words, the min of the rectangle is the depth, and the position of the rectangle (its center) is the position. Well, better forget it.
.. _altitude_maps:
Altitude maps
-------------
Altitude maps can be built and used to shift all other depth maps, which are supposed to be relative to the surface ground altitude map (for now at least: we may change this later using an ``upper_level`` property in depth maps which could be any other map).
To work we require more information, because we need actual altitude maps, and a geolocalisation of the inkscape SVG file.
Geolocalisation of the SVG file
+++++++++++++++++++++++++++++++
The SVG file should contain a layer named ``lambert93`` which contains, as its name says it, world coordinates in the **Lambert 93 coordinates system**.
Coordinates can be spacified at any location of the map. At least 3 points are needed to estimate a transformation, but the more points are provided, the more precise the estimation will be. The coordinates transformation between the SVG positions and the Lambert93 coordinates will be estimated as a linear transformation fit to all SVG/Lambert 93 coortinates couples.
A Lambert93 point is specified in this ``lambert93`` layer as a group containing 2 objects:
* a text item with text being the 2 cooridinates separated by a coma, ex::
652470.89,6858871.84
* a segment line (2 points, oriented) going from the text toward the application point on the map.
Lambert 93 positions can be found using https://www.geoportail.gouv.fr/carte: on the map, select "tools" on the right, and enable "display coordinates". Then select for "reference system" the "Lambert 93" mode.
Altitude maps
+++++++++++++
Altitude maps can be retreived from BDAlti: https://geoservices.ign.fr/documentation/diffusion/telechargement-donnees-libres.html#bd-alti
Then maps have to be converted using tools in :mod:`catamap.altitude.bdalti`.
The ``altitude`` directory should be located in the same directory as the SVG map file.
Example
-------
See the map: :download:`example map <static/example_map.svg>`
The result in 2D is (left: original map, right: result):
.. image:: static/example_map.png
:height: 400
.. image:: static/example_map_imprimable.jpg
:height: 400
In 3D: `here <_static/example_3d/index.html>`_
See for instance how wells indications are enlarged in the "printable" 2D map, the inferior level is shifted to allow seing it when it is superimposed with the upper level, while they are still superposed in the 3D map.
.. _colorsets:
Colorsets
---------
Both 2D and 3D maps, while processed, may assign different colorsets to elements, if specified via the ``--color`` option. A colorset is a defined by a name. Each element, group, layer, or labelled element can be assigned a different color for each colorset. Colorsets are defined dynamically in the map SVG source file. XML elements having a ``alt_colors`` property can both define colorsets and assign colors.
A few "hard-coded" colorsets are expected to exist (even it it is not mandatory) and are used by default:
* 3D maps use by default the ``map_3d`` colorset.
* 2D maps using "igc" mode (with ICC maps overlayed) use the ``igc`` colorset.
The list of colorsets defined in a given SVG map file can be queried using the ``--list-colorsets`` option::
python -m catamap --list-colorsets plan_14_fdc_2023_07_31.svg
A map file may specify which colorset is used by each map type in the ``colorsets`` property in the ``metadata`` layer. See :ref:`colorsets property <colorsets_prop>`.
Colorsets inheritance
+++++++++++++++++++++
Some colorsets may be slight variants to other existing ones. To avoid redefining every element color in the SVG file, it is possible to specify some "colorset inheritances". This is done using the ``colorset_inheritance`` property in the ``metadata`` layer (a hidden layer made for SVG / inkscape). This property is a JSON dictionary, with a ``{'child': 'parent'}`` shape. The meaning is that a child inherits all its parent colors, and then can override some.
Colors specifications
+++++++++++++++++++++
An ``alt_colors`` property applies recursively to all its children. It is a JSON dictionary, whith the followin shape:
* primary keys are colorset names
* primary values are `color specs`
A `color spec` is normally also a dict, with the following shape:
* keys are like in SVG elements style, ``fg`` (foreground), ``bg`` (background), ``stroke-width``
* values are strings, either containing the RGBA color, or a float value for ``stroke-width``.
* colorsets used only in 3D may shunt the second level and specify directly a single RGBA color string as colorspec value.
Ex::
{"map_3d": "#c0c0c0ff",
"igc": {"bg": "#ff0000ff"},
"black": {"fg": "#ffffffff", "bg": "#c0606060ff", "stroke-width": "0.2"}}
A ``label_alt_colors`` property has an additional upper level whick key is the element label, and the primary value is like the ``alt_colors`` dict::
{"repetiteur": {"black": {"bg": "#c0c0c0ff"}},
"marches": {"black": {"fg": "#c0c0c0ff"}}}
.. _markers:
Markers
-------
**Markers** are small marker objects pointing to external links, such as photographs, or sounds. When the user clicks on a marker, or sufficiently close to it in a given radius, it triggers a link to an external resource (an web URL).
Markers are recorded in dedicated layers in the SVG file. Such layer must have a ``marker`` property, which value is the type of markers in the layer. Currently two types of markers are handled: ``photos`` (links to photographs URLS) or ``sounds``.
Sounds are handled specificly using a media player inside the 3D map, whereas photographs are just web links, and may actually point to any web resource or page (web page, photo file, video, or anything).
A marker layer can have ``markers_base_url`` or ``radius`` properties, which respectively specify the base URL for all markers, and the max distance to the marker for which a user click will trigger the marker.
The layer contents are similar to depth layers: they are text objects, or groups containing a text object and a line object to make an arrow from the text to the marker exact location. The layer may not have other kind of groups, it is not recursive.
Items in the marker layer are thus text at specified locations. The text is the URL of the resource (sound, photo, ...) and is appended to the base URL above. If no ``markers_base_url`` is specified, the marker value (type) is used as a relative directory as base URL (``sounds/...`` or ``photos/...``).
Each item may get a specific ``radius`` property, which may overload the layer radius. If no radius is specified, a default is used (10., which would correspond to 10 m radisu, depending on the map scale).
If an item text has no file extension, ``.jpg`` will be appended by the server in a photos markers layer. This way the map source, in Inkscape, may display a very short, readable, indication, such as a single number: for instance, ``123`` will be replaced with ``photos/123.jpg``.
On the web server side, photos and sounds directory have to be created and contain the referenced files.
Markers text syntax
+++++++++++++++++++
Markers text may be a filename (which will be prefixed with the contents of the ``markers_base_url`` property, and possibly suffixed with ``.jpg``), or a list of such filenames::
image1.jpg, video2.mp4, image3.jpg
or::
image1, video2.mp4, image3
In addition to the "direct URL" mechanism above, it is also possible to specify, for each markers layer, a "correspondance map" file (using the ``markers_map`` property on the layer). This file will be read as a CSV file (with ``tab`` separators). Then texts in markers text in the SGV file will go through this map to get a "final" file name for the marker element. A given marker text can be found several times in the file, and then several files will be associated with the marker element. For instance if you use numbers for markers texts, ``1``, ``2``, ``3`` in the SVG map, a ``markers_map`` can specify final filenames such as::
20220304_201758.jpg 1
20220304_201802.jpg 2
20220304_201804.jpg 1
20220304_201815.jpg 3
20220304_202652.jpg 2
20220304_202810.jpg 1
will assign to marker ``1`` the files: ``20220304_201758.jpg``, ``20220304_201804.jpg``, ``20220304_202810.jpg``, and so on. Each of the file names will undergo the prefix/suffix transformations using ``markers_base_url`` etc.
.. _zoomed regions:
Zoomed regions
--------------
Zoomed regions may be displayed to represent en enlarged duplicated portion of the original map. To do so we need 3 components, in 3 different layers:
* a source zoomed region definition. This is done in a layer containing a ``zoom_area_id`` property, this layer should contain a single closed and connected polygon, which traces the region to be zoomed. The `zoom_area_id`` identifier will be matched to a target zoom region.
* a target zoomed region area definition (where the zoomed copy will be drawn). This is done in a layer containing a ``zoom_id`` property (matching a source region definition identifier). This layer should contain a single rectangle, which marks the bounding box of where the zoomed copy of the map will be drawn.
Other layers can be marked with a ``zoom_hidden`` property, which indicates that these layers should not be part of zoomed regions.
Lights
------
Lights may be inserted in the 3D maps. They are actually handled as a special marker type. Thus they are specified in a dedicated layer, using the ``marker`` property with the value ``lights``.
Then elements are like markers: either a text element, or a group containing a text and a segment arrow to point at its exact position. The element can contain the property ``light_props`` to specify the light characteristics. This property is a JSON dict, with the following items:
type: string
light type: ``point``, ``directional`` or ``spot``.
color: list of float
color of the light, as a triplet (RGB) of float values in the range 0.-1.
intensity: float
intensity of the light source, default: 1.0
range: float
range of the light. 0 is infinity, otherwise this value drives the decay of the light with distance from the source.
direction: list of float
for directional lights (directional, spot), direction of the light.
Other properties may be specified (like the spot cone angle) as in https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_lights_punctual/README.md
.. _texturing:
Texturing
---------
In 3D mode, elements are transformed into meshes. By default meshes have a color but no texture (images mapped on the meshes). It is possible to specify texturing, by using images loaded in a hidden layer of the SVG file, and telling which texture image is associated with given elements, and how it is mapped.
Texturing involves 3 parts: texture images, mesh-image mapping, and texturing properties.
Texture images
++++++++++++++
They are images inserted in the Inkscape SVG file, and meant to be used for texturing. They are generally inserted into one or several separate layers, which are hidden in normal display modes.
Texture image elements may optionally contain texture mapping properties, in the ``gltf_properties`` property (see below, texturing properties).
Mesh-image mapping
++++++++++++++++++
Texture may be associated with an individual polygon element, or to a group or a layer, like most other properties. To be textured, an element shoud have either ``texture``, ``ceil_texture``, ``wall_texture`` or ``floor_texture`` properties defined, or several of them. ``ceil_texture``, ``wall_texture`` or ``floor_texture`` represent texturing properties for (respectively) the ceiling, walls, or floor part of an element. ``texture`` is the default texturing properties used for all of them (walls, ceil, floor) if other parts specifications are not given. Each of those texturing definition properties is a JSON dictionary, structured like the following:
id: string (mandatory)
SVG ID of the texture image to be mapped. The image element itself is generally in a different SVG layer, as explained above.
mapping_method: string (optional)
specifies how the image is mapped on the mesh. Available methods are, for now, ``geodesic_z`` and ``xy``. These are described below. The defautl value for ceilings and floors is ``xy`` and ``geodesic_z`` is the default for walls.
Texture mapping modes
+++++++++++++++++++++
xy:
used for horizontal (or semi-horizontal) meshes. Texture coordinates are the x,y projection (2D) of mesh coordinates, with appropriate scaling factors, in order to have the texture image exactly where it is in the SVG file.
geodesic_z:
used for vertical meshes. Walls are generally not all in the same plane, and can contain bifurcations. To be correctly mapped (as a wallpaper), texture images should follow a geodesic alignment, starting at the first mesh point, in order to follow curves and bifurcations.
Texturing properties
++++++++++++++++++++
They describe how a texture image is colored, and repeated on a mesh.
At the moment, only limited support for properties is implemented, and such properties are only associated with the texture image elements. In the future we could also specify it in the mesh-image mapping. Anyway this is given via the optional ``gltf_properties`` property on the texture image element.
We use ``gltf_properties`` because these properties are based on the GLTF format. See https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-sampler for details. This property is a JSON dictionary, which may contain the following:
sampler: JSON dict
specified how the image wraps around the mesh texture coordinates. The fll description is fund at: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-sampler, but mainly, the following sub-properties are used:
wrapS: int
1st texture coord mapping. 10497 means REPEAT; 33071 means CLAMP_TO_EDGE; 33648 means MIRRORED_REPEAT. (I did not invent these funny values...)
wrapT: int
2nd texture coord mapping. Same values as for ``wrapS``.
map_to_meshes module
====================
This module is an extension of the :mod:`~catamap.svg_to_mesh` module and ist main class, :class:`~catamap.svg_to_mesh.SvgToMesh` for more specific purposes. This means the former is dedicated to generically read Inkscape files and convert elements to meshes, while the specialized classes here are dedicated to building catacombs maps.
:class:`~catamap.map_to_meshes.CataSvgToMesh` wiil build 3D maps meshes and objects
:class:`~catamap.map_to_meshes.CataMapTo2DMap` will build "cleaner" 2D maps (with some symbols replaced and enlarged, regions zooms and more, and export PDF and JPG versions.
map_to_meshes module API
------------------------
'''
from . import svg_to_mesh
import numpy as np
from scipy.spatial import Delaunay
import copy
import xml.etree.cElementTree as ET
import datetime
import math
import json
import os
import os.path as osp
import hashlib
import collections
import sys
import subprocess
import distutils.spawn
import imp
import time # just for exec time stats
from argparse import ArgumentParser
import textwrap as _textwrap
import argparse
import csv
import pprint
import re
try:
import PIL.Image
except ImportError:
PIL = None
# import bdalti module
try:
from .altitude import bdalti
except ImportError:
bdalti = None
try:
from soma import aims
fake_aims = False
except ImportError:
# aims is not available, use the fake light one (with reduced
# functionalities)
aims = None
fake_aims = True
class xml_help(object):
'''
'''
pass
[docs]class ItemProperties(object):
'''
XML item properties structure, used by
:class:`~catamap.map_to_meshes.CataSvgToMesh`. They represent properties
read from the XML file elements.
Properties match those documented in `Inkscape SVG files`_, except the ``height`` property which is named ``item_height`` in XML file elements (because ``height`` is already used in SVG).
'''
properties = ('name', 'label', 'eid', 'main_group', 'level', 'upper_level',
'private', 'inaccessible', 'corridor', 'block', 'wall',
'symbol', 'arrow', 'text', 'well', 'catflap', 'hidden',
'depth_map', 'height', 'height_shift', 'border',
'alt_colors', 'label_alt_colors', 'category', 'layer',
'well_read_mode', 'grid_interval', 'marker',
'arrow_base_height_shift', 'visibility', 'non_visibility',
'relative_to', 'inverse', 'use_height_map', 'contrast_floor')
prop_types = None # will be initialized when used in get_typed_prop()
def __init__(self):
self.reset_properties()
def reset_properties(self):
for prop in self.properties:
setattr(self, prop, None)
self.private = False
self.inaccessible = False
self.corridor = False
self.block = False
self.wall = False
# self.wireframe = False
self.symbol = False
self.arrow = False
self.text = False
self.well = False
self.catflap = False
self.hidden = False
self.depth_map = False
self.height = None
self.height_shift = None
self.arrow_base_height_shift = None
self.border = False
self.marker = None
self.alt_colors = None
self.label_alt_colors = None
self.category = None
self.layer = False
self.well_read_mode = None
self.grid_interval = None
self.visibility = None
self.non_visibility = None
self.relative_to = None
self.inverse = None
self.use_height_map = None
self.contrast_floor = True
def __str__(self):
d = ['{']
for k in self.properties:
d.append("'%s': %s, " % (k, repr(getattr(self, k))))
d.append('}')
return ''.join(d)
def copy_from(self, other):
for prop in self.properties:
setattr(self, prop, getattr(other, prop))
@staticmethod
def get_typed_prop(prop, value):
if ItemProperties.prop_types is None:
ItemProperties.prop_types = {
'private': ItemProperties.is_true,
'inaccessible': ItemProperties.is_true,
'corridor': ItemProperties.is_true,
'block': ItemProperties.is_true,
'wall': ItemProperties.is_true,
# 'wireframe': ItemProperties.is_true,
'symbol': ItemProperties.is_true,
'arrow': ItemProperties.is_true,
'text': ItemProperties.is_true,
'well': ItemProperties.is_true,
'catflap': ItemProperties.is_true,
'hidden': ItemProperties.is_true,
'depth_map': ItemProperties.is_true,
'height': ItemProperties.float_value,
'height_shift': ItemProperties.float_value,
'arrow_base_height_shift': ItemProperties.float_value,
'border': ItemProperties.float_value,
'layer': ItemProperties.is_true,
'grid_interval': ItemProperties.float_value,
'inverse': ItemProperties.is_true,
}
type_f = ItemProperties.prop_types.get(prop, str)
if type_f is ItemProperties.is_true and value == prop:
# speclal case which we should handle a better way...
return True
return type_f(value)
@staticmethod
def is_true(value):
return value in ('1', 'True', 'true', 'TRUE', 1, True)
@staticmethod
def float_value(value):
if value in (None, 'None'):
return 0.
return float(value)
def fill_properties(self, element, parents=[], set_defaults=True,
style=None):
if parents:
self.copy_from(parents[-1])
else:
self.reset_properties()
self.layer = False # this one does not propagate to children
if element is not None:
eid, props = self.get_id(element, get_props=True, use_suffix=False)
if eid and props:
# properties as layer name suffixes
for prop, value in props.items():
value = ItemProperties.get_typed_prop(prop, value)
setattr(self, prop, value)
if eid:
self.eid = eid
if not self.name:
self.name = self.eid
# properties tags
for prop in ('level', 'upper_level', 'private', 'inaccessible',
'category', 'well_read_mode', 'grid_interval',
'marker', 'use_height_map'):
value = element.get(prop)
if value is not None:
setattr(self, prop,
ItemProperties.get_typed_prop(prop, value))
# alternative to "private: true", using "visibility: private"
visibility = element.get('visibility')
#if not visibility:
#visibility = []
if visibility is not None:
if visibility.strip().startswith('['):
visibility = json.loads(visibility)
else:
visibility = [visibility]
if 'private' in visibility:
self.private = True
self.visibility = visibility
non_visibility = element.get('non_visibility')
#if not non_visibility:
#non_visibility = []
if non_visibility is not None:
if non_visibility.strip().startswith('['):
non_visibility = json.loads(non_visibility)
else:
non_visibility = [non_visibility]
self.non_visibility = non_visibility
for kind in ('corridor', 'block', 'wall', # 'wireframe',
'well',
'catflap', 'hidden', 'depth_map', 'arrow', 'text'):
# + border ?
value = self.is_something(element, kind)
if value is not None:
setattr(self, kind, value)
height_map = element.get('height_map')
if height_map is not None:
height_map = ItemProperties.is_true(height_map)
if height_map:
self.depth_map = True
self.inverse = True
relative_to = element.get('relative_to')
if self.depth_map and element.get('depth_map') \
and relative_to is None:
# exception: if a depth is relative to "surf" by default
relative_to = DefaultItemProperties.ground_level
if relative_to is not None:
if relative_to in ('none', 'None'):
relative_to = None
self.relative_to = relative_to
inverse = element.get('inverse')
if inverse is not None:
self.inverse = ItemProperties.is_true(inverse)
# if style is not None and style.get('display') == 'none':
# self.hidden = True
contrast_floor = element.get('contrast_floor')
if contrast_floor is not None:
if contrast_floor in ('1', 'True', 'true', 'TRUE', 1, True):
contrast_floor = True
elif contrast_floor == 'if_no_tex':
contrast_floor = None
else:
contrast_floor = False
self.contrast_floor = contrast_floor
if element.tag == 'text' or element.tag.endswith('text'):
self.text = True
# label suffixes are an old, ambiguous, system. We must disable it
# in some cases
use_suffix = True
if self.depth_map:
use_suffix = False
label, props = self.get_label(element, get_props=True,
use_suffix=use_suffix)
if label and props:
# properties as layer name suffixes
for prop, value in props.items():
value = ItemProperties.get_typed_prop(prop, value)
if value is not None:
setattr(self, prop, value)
if label:
self.label = label
self.name = label
self.height = self.get_height(element)
self.height_shift = self.get_height_shift(element)
self.arrow_base_height_shift \
= self.get_arrow_base_height_shift(element)
if set_defaults:
for prop in ('level', 'upper_level', ):
if getattr(self, prop) is None:
setattr(self, prop,
getattr(DefaultItemProperties, prop))
# build main group (mesh object etc)
priv_str = 'private' if self.private else 'public'
access_str = ('inaccessible' if self.inaccessible
else 'accessible')
if self.label is None:
print('no label for item:', self)
self.label = 'undefined'
if self.level is None:
print('level None in', self)
self.level = 'undefined'
tags = [self.label, self.level, priv_str, access_str]
if self.category:
tags.append(self.category)
if self.text:
tags.append('text')
self.main_group = '_'.join(tags)
if element.get(
'{http://www.inkscape.org/namespaces/inkscape}'
'groupmode') == 'layer':
self.layer = True
label_alt_colors = element.get('label_alt_colors')
if label_alt_colors is not None:
try:
self.label_alt_colors = json.loads(label_alt_colors)
except:
print('error reading JSON in label_alt_colors of', eid,
label)
print('json code:', label_alt_colors)
raise
alt_colors = element.get('alt_colors')
if alt_colors is not None:
try:
self.alt_colors = json.loads(alt_colors)
except:
print('error reading JSON in alt_colors of', eid, label)
print('json code:', alt_colors)
raise
if self.well and self.well_read_mode is None:
well_read_mode = DefaultItemProperties.well_read_modes.get(
label)
self.well_read_mode = well_read_mode
if self.text and self.block:
self.block = False
[docs] @staticmethod
def remove_word(label, word):
''' remove word suffix to label (which may end with several
variants, _word_0 etc)
'''
if label == word:
return label
if label.endswith(' %s' % word) or label.endswith('_%s' % word):
return label[:-len(word) - 1]
for pattern in (' %s ', ' %s_', '_%s ', '_%s_'):
p = pattern % word
if p in label:
index = label.find(p)
sep = p[-1]
return label.replace(p, sep)
return label
@staticmethod
def remove_label_suffix(label, get_props, use_suffix=True):
props = {}
if label is None:
if get_props:
return None, None
return
if use_suffix:
for suffix, prop in DefaultItemProperties.layer_suffixes.items():
new_label = ItemProperties.remove_word(label, suffix)
if new_label != label and get_props or new_label == suffix:
props[prop] = DefaultItemProperties.layers_aliases.get(
suffix, suffix)
label = new_label
if get_props:
return label, props
return label
@staticmethod
def get_label(element, get_props=False, use_suffix=True):
label = element.get('label')
if label is None:
label = element.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
return ItemProperties.remove_label_suffix(label, get_props, use_suffix)
@staticmethod
def get_id(element, get_props=False, use_suffix=True):
label = element.get('id')
if label is None:
label = element.get(
'{http://www.inkscape.org/namespaces/inkscape}id')
return ItemProperties.remove_label_suffix(label, get_props, use_suffix)
@staticmethod
def is_corridor(element):
return ItemProperties.is_listed_element_type(
element, 'corridor', DefaultItemProperties.corridor_labels)
@staticmethod
def is_block(element):
return ItemProperties.is_listed_element_type(
element, 'block', DefaultItemProperties.block_labels)
@staticmethod
def is_wall(element):
return ItemProperties.is_listed_element_type(
element, 'wall', DefaultItemProperties.wall_labels)
@staticmethod
def is_hidden(element):
return ItemProperties.is_listed_element_type(
element, 'hidden', DefaultItemProperties.hidden_labels)
@staticmethod
def is_well(element):
return ItemProperties.is_listed_element_type(
element, 'well', DefaultItemProperties.wells_labels)
@staticmethod
def is_catflap(element):
return ItemProperties.is_listed_element_type(
element, 'catflap', DefaultItemProperties.catflap_labels)
@staticmethod
def is_depth_map(element):
return ItemProperties.is_listed_element_type(
element, 'depth_map', DefaultItemProperties.depth_map_labels)
@staticmethod
def is_border(element):
return ItemProperties.is_listed_element_type(
element, 'border', DefaultItemProperties.border_labels)
@staticmethod
def is_something(element, kind):
return ItemProperties.is_listed_element_type(
element, kind,
getattr(DefaultItemProperties, '%s_labels' % kind, ()))
@staticmethod
def is_listed_element_type(element, tag, layers_list):
value = element.get(tag)
if value is not None:
return ItemProperties.is_true(value)
label = ItemProperties.get_label(element)
if label in layers_list:
return True
return None # we don't know
def get_height(self, element):
height = element.get('item_height')
if height is not None:
return float(height)
if self.height is not None:
return self.height
for etype, h in DefaultItemProperties.types_heights.items():
is_type = getattr(self, etype, None)
if is_type:
height = h
h = DefaultItemProperties.heights.get(self.name)
if h is not None:
height = h
return height
def get_height_shift(self, element):
height_shift = element.get('height_shift')
if height_shift is not None:
return float(height_shift)
if self.height_shift is not None:
return self.height_shift
for etype, shift in DefaultItemProperties.types_height_shifts.items():
is_type = getattr(self, etype, None)
if is_type:
height_shift = shift
shift = DefaultItemProperties.height_shifts.get(self.name)
if shift is not None:
height_shift = shift
return height_shift
def get_arrow_base_height_shift(self, element):
height_shift = element.get('arrow_base_height_shift')
if height_shift is not None:
return float(height_shift)
if self.arrow_base_height_shift is not None:
return self.arrow_base_height_shift
return height_shift
[docs]class DefaultItemProperties(object):
''' Defaults and constant values.
This is mainly a remaining of the v1 of the software, where fewer things
were indicated in the SVG file, and the program was written originally for
a single map (GRS, Paris 14e) with hard-coded labels.
'''
ground_level = 'surf'
level = 'sup'
upper_level = ground_level
# layers containing these words are assigned corresponding properties
# automatically
layer_suffixes = {
'inf': 'level',
'tech': 'level',
'gtech': 'level',
'GTech': 'level',
'surf': 'level',
'metro': 'level',
'seine': 'level',
'private': 'private',
'inaccessibles': 'inaccessible',
'inaccessible': 'inaccessible',
'anciennes': 'inaccessible',
}
layers_aliases = {
'GTech': 'tech',
'gtech': 'tech',
'inaccessibles': 'inaccessible',
'anciennes': 'inaccessible',
}
# levels list
levels = [
'sup',
'inf',
'esc',
'tech',
'surf',
'pe', # ?
'metro',
]
# labels lists assigned with specific properties
corridor_labels = {
'galeries',
'galeries big sud',
'piliers a bras',
'inaccessible',
'galeries agrandissements',
'oss off',
'anciennes galeries big',
'aqueduc',
'remblai epais',
'remblai leger',
'ebauches',
'metro',
'esc',
'escaliers',
'escaliers anciennes galeries big',
'galeries techniques',
'galeries techniques despe',
}
block_labels = {
'calcaire 2010', 'calcaire ciel ouvert',
'calcaire masse2', 'calcaire masse', 'calcaire med',
'calcaire sup', 'calcaire vdg',
'piliers a bras',
'piliers',
'maconneries',
u'maçonneries',
u'maçonneries anciennes galeries big',
'hagues',
'hagues effondrees',
'cuves',
'mur',
'mur_ouvert',
'bassin',
'bassin_recouvert',
'rose',
'repetiteur',
'eau',
'grille',
'porte',
'porte_ouverte',
'passage', 'grille-porte',
}
wall_labels = {
'grilles',
}
street_labels = {'plaques rues', }
symbol_labels = {
'symboles',
'marches',
'stair_symbol',
}
depth_map_labels = {
'profondeurs esc',
'profondeurs galeries',
'profondeurs',
}
depth_map_names = (
'profondeurs esc_esc_public_accessible',
'profondeurs galeries_inf_public_accessible',
'profondeurs galeries_sup_public_accessible',
'profondeurs_metro_public_accessible',
# 'profondeurs_seine_public_accessible',
'profondeurs_tech_public_accessible',
# 'profondeurs surf',
# 'profondeurs pe',
)
wells_labels = {
u'échelle vers',
u'\xe9chelle vers', 'PSh', 'PSh vers',
'PE', 'PE anciennes galeries big',
'PS', 'PS anciennes galeries big',
'PSh', 'PSh anciennes galeries big',
'P ossements',
'P ossements anciennes galeries big',
'echelle', u'échelle', u'échelle anciennes galeries big',
u'\xe9chelle anciennes galeries big'
u'\xe9chelle'
'sans', 'PS sans',
'PSh sans',
'PS_sq',
}
catflap_labels = {
'chatieres v3',
'chatieres private',
'bas',
u'injecté',
}
hidden_labels = {
'indications_big_2010', 'a_verifier', 'bord', 'bord_sud',
u'légende_alt', u'découpage', 'raccords plan 2D',
'raccords 2D',
'masque vdg', u'masque cimetière', 'masque plage',
'agrandissement vdg', u'agrandissement cimetière',
'agrandissement plage', 'agrandissements fond',
'couleur_fond', 'couleur_fond sud',
'planches', 'planches fond', 'calcaire sup',
'calcaire limites', 'calcaire masse', 'calcaire masse2',
'lambert93',
#'légende',
}
hidden_labels.update(depth_map_names)
types_height_shifts = {
'corridor': 0.,
'steet_sign': 1.5,
'symbol': 2.5,
}
height_shifts = {
'aqueduc': 10.,
'fontis': 2.5,
'lys': 5.,
'grande_plaque': 5.,
'chatieres v3': 0.5,
'chatieres private': 0.5,
'chatieres v3_inf': 0.5,
'chatieres private_inf': 0.5,
u'injecté': 0.5,
'bas': 0.5,
'ossuaire': 5.,
'stair_symbol': 5.,
'etiage': 2.5,
'etiage_water_tri': 2.5,
'etiage_wall_tri': 2.5,
'etiage_line': 2.5,
'rose': 2.5,
'remblai leger': 0.8,
'remblai epais': 1.5,
'remblai leger_inf': 0.8,
'remblai epais_inf': 1.5,
'remblai leger inaccessibles': 0.8,
'remblai epais inaccessibles': 1.5,
'remblai leger inaccessibles_inf': 0.8,
'remblai epais inaccessibles_inf': 1.5,
'calcaire 2010': 0., # this one as walls
'calcaire vdg': 0., # this one as walls
'PE': 0.,
'PE anciennes galeries big': -9.,
}
types_heights = {
'corridor': 2.,
'stair': 1.,
'block': 1.,
'symbol': 0.3,
}
heights = {
'esc': 1.,
'cuves': 1.5,
'plaques rues': 0.5,
u'plaques rues volées': 0.5,
'repetiteur': 1.,
'bassin': 0.3,
'bassin_recouvert': 0.3,
'eau': 0.2,
'rose': 0.2,
'remblai leger': 1.2,
'remblai epais': 0.5,
'remblai leger_inf': 1.2,
'remblai epais_inf': 0.5,
'hagues effondrees': 1.2,
'sans': 1.5,
'PS sans': 1.5,
'echelle': 1.5,
u'échelle': 1.5,
u'échelle anciennes galeries big': 1.5,
'PSh sans': 1.5,
'PE': 10.5,
'PE anciennes galeries big': -10.,
'mur': 2.,
}
well_read_modes = {
'PS': 'path',
'PS galeries big': 'path',
'PE': 'path',
'PE galeries big': 'path',
'P ossements': 'path',
'P ossements galeries big': 'path',
'PSh': 'group',
'PSh galeries big': 'group',
'PSh vers': 'group',
'colim': 'group',
'échelle': 'group',
'échelle vers': 'group',
'échelle galeries big': 'group',
'PSh sans': 'group',
'sans': 'path',
}
[docs]class CataSvgToMesh(svg_to_mesh.SvgToMesh):
'''
Process XML tree to build 3D meshes
'''
proto_scale = np.array([[0.5, 0, 0],
[0, 0.5, 0],
[0, 0, 1]])
def __init__(self, concat_mesh='list_bygroup', skull_mesh=None,
headless=True):
super(CataSvgToMesh, self).__init__(concat_mesh)
self.props_stack = []
self.depth_maps = []
self.depth_meshes_def = {}
self.nrenders = 0
self.z_scale = 0.5
self.skull_mesh = skull_mesh
self.water_scale_model = None
self.stair_symbol_mesh = None
self.fontis_mesh = None
self.lily_mesh = None
self.large_sign_mesh = None
self.sounds_marker_model = None
self.photos_marker_model = None
self.level = ''
self.sounds = []
self.sounds_private = []
self.photos = []
self.photos_private = []
self.group_properties = {}
self.colorset = 'map_3d' # alt colors to translate to
self.colorset_inheritance = {}
self.map_name = 'map_3d'
self.lambert93_z_scaling = False
if headless:
try:
from anatomist import headless as ana
a = ana.HeadlessAnatomist()
except Exception:
headless = False
if not headless:
try:
from anatomist.direct import api as ana
a = ana.Anatomist()
except Exception:
raise RuntimeError('anatomist is not available. It is needed '
'for CataSvgToMesh to work.')
[docs] def filter_element(self, xml_element, style=None):
clean_return = []
is_group = False
if len(xml_element) != 0:
is_group = True
clean_return.append(self.exit_group)
if len(self.props_stack) == 1:
print(
'parse layer', xml_element.get('id'),
xml_element.get(
'{http://www.inkscape.org/namespaces/inkscape}label'))
if xml_element.tag.endswith('}metadata'):
z_scale = xml_element.get('z_scale')
if z_scale is not None:
if z_scale in ('auto', 'Auto', 'AUTO'):
self.lambert93_z_scaling = True
print('z_scale: based on Lambert93.')
if hasattr(self, 'lambert93_coords'):
self.z_scale \
= 2. / (abs(self.lambert_coords.x.slope)
+ abs(self.lambert_coords.y.slope))
print('z_scale:', self.z_scale)
else:
z_scale = float(z_scale)
self.z_scale = z_scale
colorset_inheritance = xml_element.get(
'colorset_inheritance')
if colorset_inheritance:
colorset_inheritance = json.loads(colorset_inheritance)
self.colorset_inheritance = colorset_inheritance
print('COLORSET INHERITANCE:', colorset_inheritance)
item_props = ItemProperties()
item_props.fill_properties(xml_element, self.props_stack, style=style)
if len(self.props_stack) == 1:
self.explicitly_show.append(item_props.label)
if is_group:
# maintain the stack of parent properties to allow inheritance
self.props_stack.append(item_props)
if len(self.depth_maps) != 0:
# while reading depth indications layer, process things differently
if xml_element.tag.endswith('g'):
return (self.read_depth_group,
[self.clear_depth_group] + clean_return, False)
elif xml_element.tag.endswith('path'):
return (self.read_depth_arrow, clean_return, False)
elif xml_element.tag.endswith('text'):
return (self.read_depth_text, clean_return, True)
elif xml_element.tag.endswith('rect'):
return (self.read_depth_rect, clean_return, True)
else:
print('unknown depth child:', xml_element)
return (self.noop, clean_return, True)
self.item_props = item_props
# keep this properties for the whole group (hope there are no
# inconistencies)
if item_props.main_group:
self.group_properties[item_props.main_group] = item_props
# print(item_props.main_group)
hidden = (self.item_props.hidden
or (self.item_props.visibility is not None
and self.map_name not in self.item_props.visibility)
or (self.item_props.non_visibility
and self.map_name in self.item_props.non_visibility))
self.level = self.item_props.level
self.main_group = self.item_props.main_group
if xml_element.get('title') in ('true', 'True', '1', 'TRUE'):
title = [x.text for x in xml_element]
self.title = getattr(self, 'title', []) + title
return (self.noop, clean_return, True)
label = item_props.label
if label is not None:
# depths are taken into account even when hidden (because they
# are normally hidden in the svg file)
if item_props.depth_map:
return (self.start_depth_rect,
[self.clean_depth] + clean_return, False)
if item_props.marker:
print('MARKER:', item_props.marker)
sname = item_props.marker
if item_props.private:
sname += '_private'
mmname = '%s_marker_model' % item_props.marker
model = getattr(self, mmname, None)
if model is None:
mmname = 'photos_marker_model'
model = self.photos_marker_model
if not hasattr(self, 'marker_types'):
self.marker_types = {}
self.marker_types[sname] = mmname
self.read_markers(xml_element, model, sname)
return (self.noop, clean_return, True)
if label == 'lambert93':
self.read_lambert93(xml_element)
return (self.noop, clean_return, True)
if hidden:
# hidden layers are not rendered
return (self.noop, clean_return, True)
if item_props.well:
if item_props.layer:
return (None, clean_return, False)
return (self.read_well, clean_return, False)
if label is not None:
if label == 'etiage':
return (self.read_water_scale, clean_return, True)
elif label in ('fontis', 'fontis_inf', 'fontis private',
'fontis private_inf'):
return (self.read_fontis, clean_return, True)
elif label in ('stair_symbol', ):
return (self.read_stair_symbol, clean_return, True)
elif label == 'arche':
return (self.read_arch, clean_return, True)
elif label.startswith('lys'):
return (self.read_lily, clean_return, True)
elif label.startswith('grande_plaque'):
return (self.read_large_sign, clean_return, True)
elif label == 'ossuaire':
return (self.read_bones, clean_return, True)
return (None, clean_return, False)
def noop(self, xml_path, trans, style=None):
return None
def exit_group(self):
# print('leave group',
# self.props_stack[-1].main_group)
# print('with properties:', self.item_props)
self.props_stack.pop(-1)
[docs] def read_path(self, xml_path, trans, style=None):
if self.main_group is None:
print('path with no group:', xml_path, list(xml_path.items()))
mesh = super(CataSvgToMesh, self).read_path(xml_path, trans, style)
props = self.group_properties.get(self.main_group)
if props and props.arrow:
# print('arrow')
vert = mesh.vertex()
nv = len(vert)
for i in range(nv):
vert[i][2] = i / (nv - 1)
## create mesh if it doesn't exist, and assign it arrow indices
#gmesh = self.mesh_dict.setdefault(self.main_group,
#aims.AimsTimeSurface(2))
#if not hasattr(gmesh, 'arrow_indices'):
#gmesh.arrow_indices = []
## keep track of each beginning of arrow object in a concatenated
## mesh in order to postprocess arrow locations later
#gmesh.arrow_indices.append(len(gmesh.vertex()))
return mesh
[docs] def read_paths(self, xml):
# print('### read_paths')
self.symbol_scale = 1.
meta = self.get_metadata(xml)
symbol_scale = meta.get('symbol_scale')
if symbol_scale is not None:
self.symbol_scale = float(symbol_scale)
if self.skull_mesh is None:
self.skull_mesh = self.make_skull_model(xml)
if self.water_scale_model is None:
self.water_scale_model \
= self.make_water_scale_model((0., 0., 0.), 1.)
if self.fontis_mesh is None:
self.fontis_mesh = self.make_fontis_model(xml)
if self.stair_symbol_mesh is None:
self.stair_symbol_mesh = self.make_stair_symbol_model()
if self.lily_mesh is None:
self.lily_mesh = self.make_lily_model(xml)
if self.large_sign_mesh is None:
self.large_sign_mesh = self.make_large_sign_model(xml)
if self.sounds_marker_model is None:
self.sounds_marker_model = self.make_sounds_marker_model()
if self.photos_marker_model is None:
self.photos_marker_model = self.make_photos_marker_model()
res = super(CataSvgToMesh, self).read_paths(xml)
#print('======= read_path done =======')
#print('groups properties:', len(self.group_properties))
#for k, v in self.group_properties.items():
#print(k, ':', v)
return res
def text_description(self, xml_item, trans=None, style=None, text=''):
# add level information in text objects
desc = super(CataSvgToMesh, self).text_description(
xml_item, trans=trans, style=style, text=text)
if self.level:
props = desc.get('properties', {})
props['level'] = self.level
desc['properties'] = props
return desc
def read_well(self, well_xml, trans, style=None):
props = self.item_props
# print('read_well', props)
if props.well_read_mode == 'group':
return self.read_well_group(well_xml, trans, style=style)
allowed_items = set(['path', 'rect', 'circle', 'ellipse'])
p = well_xml.tag.rfind('}')
if p >= 0:
tag = well_xml.tag[p+1:]
else:
tag = well_xml.tag
if tag not in allowed_items:
return
# print('read_well path')
mesh = self.read_path(well_xml, trans, style)
bmin = list(mesh.vertex()[0])
bmax = list(bmin)
for v in mesh.vertex():
if v[0] < bmin[0]:
bmin[0] = v[0]
if v[0] > bmax[0]:
bmax[0] = v[0]
if v[1] < bmin[1]:
bmin[1] = v[1]
if v[1] > bmax[1]:
bmax[1] = v[1]
center = ((bmin[0] + bmax[0]) / 2, (bmin[1] + bmax[1]) / 2)
radius = (bmax[0] - bmin[0]) / 2
z = bmin[2] * self.z_scale
height = 20. * self.z_scale
wells_spec = self.mesh_dict.setdefault(props.main_group, [])
wells_spec.append((center, radius, z, height))
# print('well_type:', well_type, len(wells_spec))
def read_well_group(self, stair_xml, trans, style=None):
props = self.group_properties[self.main_group]
props.well = True
props.corridor = False
props.block = False
props.wall = False
# we are looking for a group with a path child
if len(stair_xml[:]) == 0:
return
# trans = self.get_transform(stair_xml, trans)
child = stair_xml[0]
if child is None:
return
if child.tag.endswith('}path') or child.tag == 'path':
trans = self.get_transform(child, trans)
mesh = self.read_path(child, trans, style)
bmin = list(mesh.vertex()[0])
bmax = list(bmin)
for v in mesh.vertex():
if v[0] < bmin[0]:
bmin[0] = v[0]
if v[0] > bmax[0]:
bmax[0] = v[0]
if v[1] < bmin[1]:
bmin[1] = v[1]
if v[1] > bmax[1]:
bmax[1] = v[1]
center = ((bmin[0] + bmax[0]) / 2, (bmin[1] + bmax[1]) / 2)
radius = (bmax[0] - bmin[0]) / 2
z = bmin[2] * self.z_scale
height = 20. * self.z_scale
wells_spec = self.mesh_dict.setdefault(self.main_group, [])
wells_spec.append((center, radius, z, height))
def read_bones(self, bones_xml, trans, style=None):
bbox = self.boundingbox(bones_xml[0], trans)
mesh = self.mesh_dict.setdefault(self.main_group,
aims.AimsTimeSurface(3))
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
0.]
tr = aims.AffineTransformation3d()
tr.setTranslation(center)
skull_mesh = aims.AimsTimeSurface(self.skull_mesh)
aims.SurfaceManip.meshTransform(skull_mesh, tr)
aims.SurfaceManip.meshMerge(mesh, skull_mesh)
if 'material' not in mesh.header():
mesh.header().update(skull_mesh.header())
if 'material' in mesh.header():
mat = mesh.header()['material']
else:
mat = {}
mat['face_culling'] = 0
mesh.header()['material'] = mat
def read_fontis(self, fontis_xml, trans, style=None):
bbox = self.boundingbox(fontis_xml[0], trans)
mesh = self.mesh_dict.setdefault(self.main_group,
aims.AimsTimeSurface(3))
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
0.]
tr = aims.AffineTransformation3d()
tr.setTranslation(center)
fontis_mesh = aims.AimsTimeSurface(self.fontis_mesh)
aims.SurfaceManip.meshTransform(fontis_mesh, tr)
aims.SurfaceManip.meshMerge(mesh, fontis_mesh)
if 'material' not in mesh.header():
mesh.header().update(fontis_mesh.header())
if 'material' in mesh.header():
mat = mesh.header()['material']
else:
mat = {}
mat['face_culling'] = 0
mesh.header()['material'] = mat
def read_lily(self, lily_xml, trans, style=None):
bbox = self.boundingbox(lily_xml[0], trans)
mesh = self.mesh_dict.setdefault(self.main_group,
aims.AimsTimeSurface(3))
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
0.]
tr = aims.AffineTransformation3d()
tr.setTranslation(center)
lily_mesh = aims.AimsTimeSurface(self.lily_mesh)
aims.SurfaceManip.meshTransform(lily_mesh, tr)
aims.SurfaceManip.meshMerge(mesh, lily_mesh)
if 'material' not in mesh.header():
mesh.header().update(lily_mesh.header())
if 'material' in mesh.header():
mat = mesh.header()['material']
else:
mat = {}
mat['face_culling'] = 0
mesh.header()['material'] = mat
def read_large_sign(self, lily_xml, trans, style=None):
bbox = self.boundingbox(lily_xml[0], trans)
mesh = self.mesh_dict.setdefault(self.main_group,
aims.AimsTimeSurface(3))
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
0.]
tr = aims.AffineTransformation3d()
tr.setTranslation(center)
try:
lily_mesh = aims.AimsTimeSurface(self.large_sign_mesh)
except Exception:
print('Mesh error:', type(self.large_sign_mesh), 'in',
self.item_props)
return
aims.SurfaceManip.meshTransform(lily_mesh, tr)
aims.SurfaceManip.meshMerge(mesh, lily_mesh)
if 'material' not in mesh.header():
mesh.header().update(lily_mesh.header())
if 'material' in mesh.header():
mat = mesh.header()['material']
else:
mat = {}
mat['face_culling'] = 0
mesh.header()['material'] = mat
def read_arch(self, arch_xml, trans, style=None):
## don't apply transform, we will do it later on the mesh
props = self.group_properties[self.main_group]
props.symbol = True
bbox = None
for child in arch_xml:
bboxc = self.boundingbox(child, trans)
if bbox is None:
bbox = bboxc
else:
bbox[0] = [min(bbox[0][i], bboxc[0][i]) for i in range(2)]
bbox[1] = [max(bbox[1][i], bboxc[1][i]) for i in range(2)]
if bbox is None:
print('no bbox for arch:', arch_xml, arch_xml.items(),
len(arch_xml))
return
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
0.]
radius = max(np.array(bbox[1]) - bbox[0]) / 2
arch_spec = self.mesh_dict.setdefault(self.main_group, [])
arch_spec.append((center, (radius, trans), 0., 3.))
def read_water_scale(self, ws_xml, trans, style=None):
props = self.group_properties[self.main_group]
props.symbol = True
bbox = self.boundingbox(ws_xml[0], trans)
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
4.]
tr = aims.AffineTransformation3d()
tr.setTranslation(center)
for mesh_id, ws_model in self.water_scale_model.items():
ws_mesh = aims.AimsTimeSurface(ws_model)
aims.SurfaceManip.meshTransform(ws_mesh, tr)
mesh = self.mesh_dict.setdefault(mesh_id, type(ws_mesh)())
if len(mesh.header()) == 0:
mesh.header().update(ws_model.header())
self.group_properties[mesh_id] = props
aims.SurfaceManip.meshMerge(mesh, ws_mesh)
def read_stair_symbol(self, st_xml, trans, style=None):
bbox = self.boundingbox(st_xml[0], trans)
mesh = self.mesh_dict.setdefault(self.main_group,
aims.AimsTimeSurface(3))
center = [(bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2,
0.]
tr = aims.AffineTransformation3d()
tr.setTranslation(center)
stair_mesh = aims.AimsTimeSurface(self.stair_symbol_mesh)
aims.SurfaceManip.meshTransform(stair_mesh, tr)
aims.SurfaceManip.meshMerge(mesh, stair_mesh)
if 'material' not in mesh.header():
mesh.header().update(stair_mesh.header())
if 'material' in mesh.header():
mat = mesh.header()['material']
else:
mat = {}
mat['face_culling'] = 0
mesh.header()['material'] = mat
def make_psh_sq_well(self, center, radius, z, height, props, faces=8):
# square well
return self.make_psh_well(center, radius, z, height, props, faces=4,
smooth=False, rotate=math.pi / 4)
def make_psh_well(self, center, radius, z, height, props, faces=8,
smooth=True, rotate=0.):
p1 = aims.Point3df(center[0], center[1], z)
p2 = aims.Point3df(center[0], center[1], z + height)
r0 = radius * 0.7
well = aims.SurfaceGenerator.cylinder({
'point1': p1, 'point2': p2, 'radius': r0, 'facets': faces,
'smooth_tube': smooth, 'closed': False})
if rotate != 0.:
tr = aims.AffineTransformation3d()
tr.setTranslation(p1)
rot = aims.Quaternion()
rot.fromAxis((0, 0, 1), rotate)
mat = tr * aims.AffineTransformation3d(rot) * tr.inverse()
aims.SurfaceManip.meshTransform(well, mat)
vert = well.vertex()
poly = well.polygon()
norm = well.normal()
a = np.pi / 6.
hl = radius - r0
nv0 = len(vert)
nv = nv0
stair_step = 0.3 * self.z_scale
phb0 = p1 + r0 * aims.Point3df(-np.sin(a), np.cos(a), 0.)
phb1 = p1 + r0 * aims.Point3df(np.sin(a), np.cos(a), 0.)
phb2 = p1 + r0 * aims.Point3df(np.cos(a), np.sin(a), 0.)
phb3 = p1 + r0 * aims.Point3df(np.cos(a), -np.sin(a), 0.)
phb5 = p1 + r0 * aims.Point3df(-np.sin(a), -np.cos(a), 0.)
phb4 = p1 + r0 * aims.Point3df(np.sin(a), -np.cos(a), 0.)
phb7 = p1 + r0 * aims.Point3df(-np.cos(a), np.sin(a), 0.)
phb6 = p1 + r0 * aims.Point3df(-np.cos(a), -np.sin(a), 0.)
for zbari in range(1, int(height / stair_step)):
zbar = z + zbari * stair_step
ph0 = aims.Point3df(phb0[0], phb0[1], zbar)
ph1 = aims.Point3df(phb1[0], phb1[1], zbar)
ph2 = aims.Point3df(phb2[0], phb2[1], zbar)
ph3 = aims.Point3df(phb3[0], phb3[1], zbar)
ph4 = aims.Point3df(phb4[0], phb4[1], zbar)
ph5 = aims.Point3df(phb5[0], phb5[1], zbar)
ph6 = aims.Point3df(phb6[0], phb6[1], zbar)
ph7 = aims.Point3df(phb7[0], phb7[1], zbar)
vert += [ph0, ph1, ph0 + (0., hl, 0.), ph1 + (0., hl, 0.),
ph2, ph3, ph2 + (hl, 0., 0.), ph3 + (hl, 0., 0.),
ph4, ph5, ph4 + (0., -hl, 0.), ph5 + (0., -hl, 0.),
ph6, ph7, ph6 + (-hl, 0., 0.), ph7 + (-hl, 0., 0.)]
poly += [(nv, nv + 1, nv + 3), (nv, nv + 3, nv + 2),
(nv + 4, nv + 5, nv + 7), (nv + 4, nv + 7, nv + 6),
(nv + 8, nv + 9, nv + 11), (nv + 8, nv + 11, nv + 10),
(nv + 12, nv + 13, nv + 15), (nv + 12, nv + 15, nv + 14)]
nv += 16
norm += [(0., 0., 1.)] * (len(vert) - nv0)
color = self.get_alt_color(props)
if not color:
color = [0.5, 0.7, .6, 1.]
well.header()['material'] = {'diffuse': color,
'face_culling': 0}
return well
def make_ladder(self, center, radius, z, height, props):
p1 = aims.Point3df(center[0], center[1], z)
p2 = aims.Point3df(center[0], center[1], z + height)
pole1 = p1 + aims.Point3df(radius, 0., 0.)
pole2 = p1 - aims.Point3df(radius, 0., 0.)
r0 = 0.05
r1 = 0.025
ladder = aims.SurfaceGenerator.cylinder({
'point1': pole1,
'point2': p2 + aims.Point3df(radius, 0., 0.), 'radius': r0,
'facets': 4, 'smooth': True, 'closed': False})
ladder2 = aims.SurfaceGenerator.cylinder({
'point1': pole2,
'point2': p2 - aims.Point3df(radius, 0., 0.), 'radius': r0,
'facets': 4, 'smooth': True, 'closed': False})
aims.SurfaceManip.meshMerge(ladder, ladder2)
stair_step = 0.3 * self.z_scale
for zbari in range(1, int(height / stair_step)):
zbar = z + zbari * stair_step
b1 = aims.Point3df(pole1[0], pole1[1], zbar)
b2 = aims.Point3df(pole2[0], pole2[1], zbar)
bar = aims.SurfaceGenerator.cylinder({
'point1': b1, 'point2': b2, 'radius': r1, 'facets': 4,
'smooth': True, 'closed': False})
aims.SurfaceManip.meshMerge(ladder, bar)
color = self.get_alt_color(props)
if not color:
color = [1., 0., .6, 1.]
ladder.header()['material'] = {'diffuse': color}
return ladder
def make_spiral_stair(self, center, radius, z, height, props=None):
p1 = aims.Point3df(center[0], center[1], z)
p2 = aims.Point3df(center[0], center[1], z + height)
r0 = radius * 0.25
well = aims.SurfaceGenerator.cylinder({
'point1': p1, 'point2': p2, 'radius': r0, 'facets': 8,
'smooth': True, 'closed': False})
vert = well.vertex()
poly = well.polygon()
a = np.pi / 6.
nv0 = len(vert)
nv = nv0
stair_step = 0.2 * self.z_scale
angle = 0.
for zbari in range(1, int(height / stair_step)):
zbar = z + zbari * stair_step
ph1 = aims.Point3df(p1)
ph1[2] = zbar
ph0 = ph1 - (0., 0., stair_step)
ph2 = ph0 + radius * aims.Point3df(np.cos(angle), np.sin(angle),
0.)
ph3 = ph2 + aims.Point3df(0., 0., stair_step)
angle += a
ph4 = ph1 + radius * aims.Point3df(np.cos(angle), np.sin(angle),
0.)
vert += [ph0, ph1, ph2, ph3, ph1, ph3, ph4]
poly += [(nv, nv + 2, nv + 1), (nv + 1, nv + 2, nv + 3),
(nv + 4, nv + 5, nv + 6)]
nv += 7
well.updateNormals()
color = self.get_alt_color(props)
if not color:
color = [1., .6, 0., 1.]
well.header()['material'] = {'diffuse': color,
'face_culling': 0}
return well
def make_ps_sq_well(self, center, radius, z, height, well_type, props):
return self.make_ps_well(center, radius, z, height, well_type, props,
faces=4, smooth=False, rotate=math.pi / 4)
[docs] def make_ps_well(self, center, radius, z, height, well_type, props,
faces=8, smooth=True, rotate=0.):
'''PS or PE (blue), P ossements (yellow)
'''
p1 = aims.Point3df(center[0], center[1], z)
p2 = aims.Point3df(center[0], center[1], z + height)
well = aims.SurfaceGenerator.cylinder({
'point1': p1, 'point2': p2, 'radius': radius, 'facets': faces,
'smooth_tube': smooth, 'closed': False})
if rotate != 0.:
tr = aims.AffineTransformation3d()
tr.setTranslation(p1)
rot = aims.Quaternion()
rot.fromAxis((0, 0, 1), rotate)
mat = tr * aims.AffineTransformation3d(rot) * tr.inverse()
aims.SurfaceManip.meshTransform(well, mat)
color = self.get_alt_color(props)
if not color:
color = [0., 1., .6, 1.]
if well_type.startswith('PE'):
color = [0., 0.7, 1., 1.]
elif well_type == 'PS' or well_type.startswith('PS_') \
or well_type.startswith('PS '):
color = [1., 1., 1., 1.]
elif well_type.startswith('P ossements'):
color = [1., 1., 0., 1.]
well.header()['material'] = {'diffuse': color, 'face_culling': 0}
return well
def make_well(self, center, radius, z, height, well_type=None, props=None):
if well_type is None and props is not None:
well_type = props.label
if well_type == 'PS':
return self.make_ps_well(center, radius, z, height, well_type,
props)
elif well_type == 'PS_sq':
return self.make_ps_sq_well(center, radius, z, height, well_type,
props)
if well_type in ('PSh', 'sans', 'PSh sans', 'PSh sans_sq'):
return self.make_psh_well(center, radius, z, height, props)
elif well_type in ('PSh_sq', 'sans_sq'):
return self.make_psh_sq_well(center, radius, z, height, props)
elif well_type in ('echelle', u'échelle'):
return self.make_ladder(center, radius, z, height, props)
elif well_type.startswith('colim'):
return self.make_spiral_stair(center, radius, z, height, props)
if well_type.endswith('_sq'):
return self.make_ps_sq_well(center, radius, z, height, well_type,
props)
return self.make_ps_well(center, radius, z, height, well_type, props)
def make_arche(self, center, radius_and_trans, z, height, well_type=None,
props=None):
# translate method name
return self.make_arch(center, radius_and_trans, z, height,
well_type=well_type, props=props)
def make_arch(self, center, radius_and_trans, z, height, well_type=None,
props=None):
radius, trans = radius_and_trans
# get transform parent to children
tmat = np.eye(4)
tmat[:2, :2] = trans[:2, :2]
tmat[:2, 3:] = trans[:2, 2:3]
# mesh will be created in source space, apply its source space center
tr = aims.AffineTransformation3d(tmat)
tr = tr.inverse()
center0 = tr.transform(0, 0, 0)
# source scale
scl0 = (tr.transform(1, 0, 0) - center0).norm()
scl1 = (tr.transform(0, 1, 0) - center0).norm()
scl = (scl0 + scl1) / 2
#print('arch:', center, ', scl:', scl, scl0, scl1, ', radius:', radius)
radius = radius * scl * 0.5
nf = 6
wp = radius * 2. # radius of the vault curve
r0 = radius * 0.2 # radius of pillar stones
amax = np.pi * .35 # max angle of the vault
zscl = 1. # Z scaling of the vault ellipse
wheight = 1.5 # straight wall height
a0 = amax * 0.98
c0 = center0 + (-wp * np.cos(a0), 0, wheight)
arch = aims.AimsTimeSurface_3()
cyl = aims.SurfaceGenerator.cylinder(c0 + (wp, 0, -wheight),
c0 + (wp, 0, 0.07),
r0, r0, nf, False, False)
aims.SurfaceManip.meshMerge(arch, cyl)
for i in range(4):
alpha = i * amax / 4
alpha2 = (i + 1.07) * amax / 4
c1 = c0 + (wp * np.cos(alpha), 0, wp * np.sin(alpha) * zscl)
c2 = c0 + (wp * np.cos(alpha2), 0, wp * np.sin(alpha2) * zscl)
cyl = aims.SurfaceGenerator.cylinder(c1, c2, r0, r0, nf, False,
False)
aims.SurfaceManip.meshMerge(arch, cyl)
c0 = center0 + (wp * np.cos(a0), 0, wheight)
cyl = aims.SurfaceGenerator.cylinder(c0 + (-wp, 0, -wheight),
c0 + (-wp, 0, 0.07),
r0, r0, nf, False, False)
aims.SurfaceManip.meshMerge(arch, cyl)
for i in range(4):
alpha = i * amax / 4
alpha2 = (i + 1.07) * amax / 4
c1 = c0 + (-wp * np.cos(alpha), 0, wp * np.sin(alpha) * zscl)
c2 = c0 + (-wp * np.cos(alpha2), 0, wp * np.sin(alpha2) * zscl)
cyl = aims.SurfaceGenerator.cylinder(c1, c2, r0, r0, nf, False,
False)
aims.SurfaceManip.meshMerge(arch, cyl)
tmat = np.eye(4)
tmat[:2, :2] = trans[:2, :2]
tmat[:2, 3:] = trans[:2, 2:3]
tmat[2, 3] = z
tr = aims.AffineTransformation3d(tmat)
aims.SurfaceManip.meshTransform(arch, tr)
tmat2 = np.eye(4)
tmat2[:3, 3] += center
tr = aims.AffineTransformation3d(tmat2)
aims.SurfaceManip.meshTransform(arch, tr)
arch.header()['material'] = {'diffuse': [1., 1., 0.5, 1.]}
return arch
def start_depth_rect(self, child_xml, trans, style=None):
level = self.item_props.level
mesh_def = self.depth_meshes_def.get(level)
if mesh_def is not None:
print('Warning: several depth meshes for level',
level, ', existing:\n', mesh_def[1])
print('current:')
print(self.item_props)
depth_mesh = mesh_def[0]
else:
depth_mesh = self.mesh_dict.setdefault(self.main_group,
aims.AimsTimeSurface_3())
depth_mesh.header()['material'] = {'face_culling': 0,
'diffuse': [0., 0.6, 0., 1.]}
self.depth_maps.append(True)
self.depth_meshes_def[level] = (depth_mesh, self.item_props)
# print('start_depth_rect', self.main_group, '///', level)
# print('level:', level)
# print(self.item_props)
def clean_depth(self):
self.depth_maps.pop()
def read_depth_group(self, child_xml, trans, style=None):
if hasattr(self, 'depth_group'):
raise RuntimeError(
'Nested depth group in main_group %s, element: %s, items: %s'
% (self.main_group, repr(child_xml),
repr(list(child_xml.items()))))
self.depth_group = {}
def clear_depth_group(self):
if not hasattr(self, 'depth_group'):
raise RuntimeError(
'End of depth group outside of depth group. Current '
'main_group: %s' % str(self.main_group))
depth_mesh = self.mesh_dict[self.main_group]
pos = self.depth_group.get('position')
z = self.depth_group.get('depth')
if pos is not None and z is not None:
for p in pos:
depth_mesh.vertex().append((p[0], p[1], -z * self.z_scale))
del self.depth_group
def read_depth_arrow(self, child_xml, trans, style=None):
mesh = super(CataSvgToMesh, self).read_path(child_xml, trans, style)
if len(mesh.vertex()) != 0:
try:
position = self.depth_group.setdefault('position', [])
position.append(mesh.vertex()[-1][:2])
except Exception as e:
print('failed depth arrow, main_group:', self.main_group,
', vertices:')
print(np.array(mesh.vertex()))
print(e)
def read_depth_text(self, child_xml, trans, style=None):
text_span = [x for x in child_xml if x.tag.endswith('tspan')]
if len(text_span) != 0:
text = ''.join([t.text for t in text_span if t.text is not None])
try:
text = re.findall('[0-9.\\-]+', text)[0]
depth = float(text)
except ValueError:
print('error in SVG, in depth text: found non-float value:',
repr(text), 'in element:', child_xml.get('id'),
child_xml.get('x'), child_xml.get('y'))
raise
if hasattr(self, 'depth_group'):
self.depth_group['depth'] = depth
else:
# text outside a group: use its position
try:
x = float(child_xml.get('x'))
y = float(child_xml.get('y'))
except TypeError:
try:
# x, y are sometimes on the tspan
x = float(text_span[0].get('x'))
y = float(text_span[0].get('y'))
except TypeError:
print('error in depth text position:', child_xml,
child_xml.get('x'), child_xml.get('y'),
', in element:', child_xml.get('id'))
raise
if trans is not None:
x, y = trans.dot([[x], [y], [1]])[:2]
x = x[0, 0]
y = y[0, 0]
depth_mesh = self.mesh_dict[self.main_group]
depth_mesh.vertex().append((x, y, -depth * self.z_scale))
def read_depth_rect(self, rect_xml, trans, style=None):
props = self.item_props # self.group_properties.get(self.main_group)
if not props.depth_map:
# skip non-depth rects
return
depth_mesh = self.mesh_dict[self.main_group]
x = float(rect_xml.get('x'))
y = float(rect_xml.get('y'))
if trans is not None:
x, y = trans.dot([[x], [y], [1]])[:2]
x = x[0, 0]
y = y[0, 0]
z = -np.max((float(rect_xml.get('width')),
float(rect_xml.get('height')))) * 10.
depth_mesh.vertex().append((x, y, z * self.z_scale))
def read_markers_map(self, filename):
import csv
#def _parse(item):
#item = item.strip()
#if '"' not in item and "'" not in item:
#return [s.strip() for s in item.split(' ')]
#q = item.find('"')
#if q >= 0:
#items = item.split('"')
#else:
#items = item.split("'")
#parsed = []
#i = 0
#for item in items:
#if i % 2 == 0:
#parsed += _parse(item)
#else:
#parsed.append(item)
#i += 1
#return parsed
try:
mmap = {}
with open(filename) as f:
#for line in f.readlines():
#items = line.strip().split('\t')
#row = []
#for item in items:
#row += _parse(item)
dialect = csv.Sniffer().sniff(f.read(1024))
f.seek(0)
#reader = csv.reader(f, delimiter='\t')
reader = csv.reader(f, dialect=dialect)
for row in reader:
if row:
if len(row) == 1: # no \t separtator
row = [x.strip() for x in row[0].split()]
if len(row) == 1: # still no split
print('marker without identifier:', row[0])
continue
if len(row) == 0:
continue # empty line
mmap.setdefault(row[1], []).append(row[0])
except Exception as e:
print('error reading markers map file:', filename, file=sys.stderr)
raise
return mmap
def read_markers(self, xml, marker_model, mtype, trans=None, style=None):
print('READ', mtype.upper())
if not hasattr(self, mtype):
setattr(self, mtype, [])
markers = getattr(self, mtype)
if trans is None:
trans = self.get_transform(xml)
if trans is None:
trans = np.matrix(np.eye(3))
# print('trans:', trans)
base_url = xml.get('markers_base_url')
markers_map = {}
markers_map_file = xml.get('markers_map')
if markers_map_file:
markers_map = self.read_markers_map(markers_map_file)
if base_url is None:
base_url = mtype + '/'
if not base_url.endswith('/'):
base_url += '/'
layer_radius = xml.get('radius')
if layer_radius is not None:
layer_radius = float(layer_radius)
else:
layer_radius = 10.
layer_hshift = xml.get('height_shift')
if layer_hshift is not None:
layer_hshift = float(layer_hshift) * self.z_scale
else:
layer_hshift = 0.
used_texts = {}
missing = []
missing_files = []
for xml_element in xml:
level = xml_element.get('level')
if level is None:
level = self.item_props.level
text = None
pos = None
trans2 = xml_element.get('transform')
trans_el = trans
if trans2 is not None:
transm = self.get_transform(trans2)
trans_el = trans * transm
radius = xml_element.get('radius')
if radius is not None:
radius = float(radius)
else:
radius = layer_radius
hshift = xml_element.get('height_shift')
if hshift is not None:
hshift = float(hshift) * self.z_scale
else:
hshift = layer_hshift
elements = xml_element
if xml_element.tag.split('}')[-1] != 'g':
elements = [xml_element]
for sub_el in elements:
tag = sub_el.tag.split('}')[-1]
if tag == 'text':
if pos is None:
try:
pos = [float(sub_el.get('x')),
float(sub_el.get('y'))]
except Exception:
if len(sub_el[:]) != 0 and sub_el[0].get('x') \
and sub_el[0].get('y'):
try:
pos = [float(sub_el[0].get('x')),
float(sub_el[0].get('y'))]
except Exception:
print(
'error while reading marker',
sub_el.get('id'))
raise
else:
print(
'error while reading marker',
sub_el.get('id'))
raise
x, y = trans_el.dot([[pos[0]], [pos[1]], [1]])[:2]
pos = [x[0, 0], y[0, 0]]
if sub_el[0].text is None:
print('marker with no text:', sub_el.get('id'), sub_el)
text = sub_el[0].text.strip()
elif tag == 'path':
trans3 = sub_el.get('transform')
trans_el2 = trans_el
if trans3 is not None:
transm = self.get_transform(trans3)
trans_el2 = trans_el * transm
mesh = super(CataSvgToMesh, self).read_path(sub_el,
trans_el2,
style)
if len(mesh.vertex()) != 0:
try:
pos = mesh.vertex()[-1][:2]
except Exception:
print('failed marker arrow in', mtype,
', vertices:')
print(np.array(mesh.vertex()))
if text and pos:
if text.startswith('['): # list
texts = [t.strip() for t in text[1:-1].split(',')]
else:
texts = [text]
images = []
if mtype == 'lights': # light markers are not files
lprops = xml_element.get('light_props')
if lprops is None:
lprops = {'type': 'directional'}
else:
try:
lprops = json.loads(lprops)
except Exception:
print('json decode error in',
xml_element.get('id'))
print('in light_props:', lprops)
raise
markers.append([pos + [hshift, level, lprops], texts])
else:
for text in texts:
imlist = markers_map.get(text)
if imlist is None:
if markers_map:
missing.append((text, pos))
imlist = [text]
images += imlist
# test existence of files
# warning: only works relative to current dir
for image in imlist:
url = base_url + image
if not os.path.exists(url):
missing_files.append((text, pos, url))
used_texts.setdefault(text, []).append(pos)
markers.append([pos + [hshift, level, radius],
[base_url + image for image in images]])
# print('%s:' % mtype, markers[-1])
print('read', len(markers), mtype)
if mtype != 'lights':
# check and print duplicates
dup = False
for text, pos in used_texts.items():
if len(pos) != 1:
if not dup:
print('** Duplicate markers texts in layer', mtype,
': **')
dup = True
print('marker:', text, 'at positions:')
for p in pos:
print(' -', p)
if missing:
print('** Missing correspondance for marker texts: **')
for m in missing:
print(m[0], 'at position:', m[1])
if missing_files:
print('** Missing marker files: **')
for m in missing_files:
print(m[0], 'at position:', m[1], ':', m[2])
def read_lambert93(self, xml, trans=None):
print('READ LAMBERT93')
if trans is None:
trans = np.matrix(np.eye(3))
lambert_map = []
for xml_element in xml:
if xml_element.tag.split('}')[-1] != 'g':
raise ValueError('lambert93 element is not a group:',
xml_element)
text = None
pos = None
trans2 = xml_element.get('transform')
trans_el = np.matrix(np.eye(3))
if trans2 is not None:
transm = self.get_transform(trans2)
if trans is None:
trans_el = transm
else:
trans_el = trans * transm
for sub_el in xml_element:
tag = sub_el.tag.split('}')[-1]
if tag == 'text':
if pos is None:
pos = [float(sub_el.get('x')),
float(sub_el.get('y'))]
text = sub_el[0].text
elif tag == 'path':
trans3 = sub_el.get('transform')
trans_el2 = trans_el
if trans3 is not None:
transm = self.get_transform(trans3)
trans_el2 = trans_el * transm
mesh = super(CataSvgToMesh, self).read_path(sub_el,
trans_el2,
style=None)
if len(mesh.vertex()) != 0:
try:
pos = mesh.vertex()[-1][:2]
except Exception:
print('failed lambert93 arrow, vertices:')
print(np.array(mesh.vertex()))
if text and pos:
lamb = text.strip().split(',')
if len(lamb) != 2:
print('incorrect lambert93 coordinates:', text)
continue
lambert_map.append((pos, [float(x) for x in lamb]))
# print('lambert93:', lambert_map[-1])
self.lambert93_coords = lambert_map
# regress
from scipy import stats
x = [l[0][0] for l in lambert_map]
y = [l[1][0] for l in lambert_map]
lamb_x = stats.linregress(x, y)
x = [l[0][1] for l in lambert_map]
y = [l[1][1] for l in lambert_map]
lamb_y = stats.linregress(x, y)
class xy(object):
pass
self.lambert_coords = xy()
self.lambert_coords.x = lamb_x
self.lambert_coords.y = lamb_y
# print('Lambert93 slope:', lamb_x.slope, lamb_y.slope)
if self.lambert93_z_scaling:
self.z_scale \
= 2. / (abs(self.lambert_coords.x.slope)
+ abs(self.lambert_coords.y.slope))
# print('z_scale:', self.z_scale)
def change_level(self, mesh, dz):
vert = mesh.vertex()
for v in vert:
v[2] += dz
@staticmethod
def delaunay(mesh):
points = np.array([[p[0], p[1]] for p in mesh.vertex()])
tri = Delaunay(points)
mesh.polygon().assign(tri.simplices)
#mesh.header()['material']['face_culling'] = 0
@staticmethod
def build_depth_win(depth_mesh, size=(1000, 1000), object_win_size=(8, 8)):
headless = False
if headless:
import anatomist.headless as ana
a = ana.HeadlessAnatomist()
else:
import anatomist.api as ana
a = ana.Anatomist()
from soma.qt_gui.qt_backend import Qt
import time
win = a.createWindow('Axial') #, options={'hidden': 1})
Qt.qApp.processEvents()
time.sleep(0.5)
win.windowConfig(view_size=size, cursor_visibility=0)
Qt.qApp.processEvents()
time.sleep(0.5)
Qt.qApp.processEvents()
admesh = a.toAObject(depth_mesh)
a.releaseObject(admesh)
for i in range(10):
time.sleep(0.2)
Qt.qApp.processEvents()
win.addObjects(admesh)
for i in range(10):
time.sleep(0.2)
Qt.qApp.processEvents()
view = win.view()
bbmin = view.boundingMin()
bbmax = view.boundingMax()
# close up on center
tbbmin = (bbmin + bbmax) / 2 \
- aims.Point3df(object_win_size[0], object_win_size[1], 0)
tbbmax = (bbmin + bbmax) / 2 \
+ aims.Point3df(object_win_size[0], object_win_size[1], 0)
tbbmin[2] = bbmin[2]
tbbmax[2] = bbmax[2]
view.setExtrema(tbbmin, tbbmax)
#view.qglWidget().updateGL()
view.paintScene()
#view.updateGL()
return win, admesh
def load_ground_altitude(self, filename):
self.ground_img = None
self.alt_bounds = None
try:
print('load ground altitude image:', filename)
ground_img = aims.read(filename)
alt_extr_filename = os.path.join(os.path.dirname(filename),
'global.json')
print('realding altidude extrema:', alt_extr_filename)
alt_extr = json.load(open(alt_extr_filename))
print('extrema:', alt_extr)
scl_min = alt_extr['scale_min']
scl_max = alt_extr['scale_max']
except Exception:
print('no ground image', filename)
return
conv = aims.Converter(intype=ground_img, outtype=aims.Volume_FLOAT)
self.ground_img \
= conv(ground_img) * (scl_max - scl_min) / 255. + scl_min
xml = self.svg.getroot()
layers = [l for l in xml
if l.get('{http://www.inkscape.org/namespaces/inkscape}label')
== 'altitude']
if len(layers) == 0:
return
layer = layers[0]
grp = layer[0]
self.main_group = 'altitude'
self.alt_bounds = self.boundingbox(grp, None)
[docs] def load_ground_altitude_bdalti(self, filename):
'''
filename:
json map, to be used with the API module bdalti.py
'''
if bdalti is None:
print('warning, bdalti module is not present')
return
if os.path.exists(filename):
with open(filename) as f:
self.bdalti_map = json.load(f)
self.bdalti_base = os.path.dirname(filename)
else:
print('warning, BDAlti meta-map is not available, file %s '
'does not exist' % filename)
def ground_altitude(self, pos, use_scale=True):
if hasattr(self, 'bdalti_map') and hasattr(self, 'lambert_coords'):
alt = self.ground_altitude_bdalti(pos)
else:
alt = self.ground_altitude_topomap(pos)
if use_scale:
# scale ground altitude the same way as depths in the map
alt *= self.z_scale
return alt
def ground_altitude_topomap(self, pos):
if not hasattr(self, 'alt_bounds') or self.alt_bounds is None \
or self.ground_img is None:
return 0.
x = int((pos[0] - self.alt_bounds[0][0])
/ (self.alt_bounds[1][0] - self.alt_bounds[0][0])
* (self.ground_img.getSizeX() - 0.001))
y = int((pos[1] - self.alt_bounds[0][1])
/ (self.alt_bounds[1][1] - self.alt_bounds[0][1])
* (self.ground_img.getSizeY() - 0.001))
if x < 0 or y < 0 or x >= self.ground_img.getSizeX() \
or y >= self.ground_img.getSizeY():
return 50. # arbitrary
gray = self.ground_img.at(x, y)
return gray
def ground_altitude_bdalti(self, pos):
x = pos[0] * self.lambert_coords.x.slope \
+ self.lambert_coords.x.intercept
y = pos[1] * self.lambert_coords.y.slope \
+ self.lambert_coords.y.intercept
z = bdalti.get_z(x, y, self.bdalti_map, self.bdalti_base,
background_z=50.)
return z
def add_ground_alt(self, mesh, verbose=False):
# print('add_ground_alt on:', mesh)
for v in mesh.vertex():
if verbose:
print(v[2], end=' ')
v[2] += self.ground_altitude(v[:2])
if verbose:
print('->', v[2])
def build_depth_wins(self, size=(1000, 1000),
object_win_size=(8, 8)):
self.depth_meshes = {}
self.depth_wins = {}
todo = list(self.depth_meshes_def.items())
while todo:
level, mesh_def = todo.pop(0)
print('building depth map', level)
depth_mesh, props = mesh_def
if props.relative_to:
print(' rel:', props.relative_to)
if props.relative_to is not None \
and props.relative_to != self.ground_level:
if props.relative_to in self.depth_wins:
# apply dependent map
if props.inverse:
print(' height map')
depth_mesh.vertex().np[:, 2] *= -1
print(' apply relative depth:', props.relative_to)
dwin = self.depth_wins[props.relative_to]
view = dwin.view()
for vertex in depth_mesh.vertex():
vertex[2] += self.get_depth(vertex, view)
else:
# depends on another map which has not been done
# WARNING: infinite loops are possible. TODO: Detect them
todo.append((level, (depth_mesh, props)))
print(' delay depth map', level, 'which depends on',
props.relative_to)
continue
win, amesh = self.build_depth_win(depth_mesh, size,
object_win_size)
self.depth_meshes[level] = amesh
self.depth_wins[level] = win
def release_depth_wins(self):
del self.depth_meshes, self.depth_wins
def get_depth(self, pos, view, object_win_size=(8., 8.), verbose=False):
if view is None:
# surface map
return self.ground_altitude(pos)
pt = aims.Point3df()
ok = view.cursorFromPosition(pos, pt)
if verbose: print('cursorFromPosition', list(pos), ':', ok, pt.np)
if ok and (pt[0] < 0 or pt[0] >= view.width()
or pt[1] < 0 or pt[1] >= view.height()):
if verbose: print('changing camera')
ok = False
if not ok:
tbbmin = pos \
- aims.Point3df(object_win_size[0], object_win_size[1], 0)
tbbmax = pos \
+ aims.Point3df(object_win_size[0], object_win_size[1], 0)
bbmin = view.boundingMin()
bbmax = view.boundingMax()
tbbmin[2] = bbmin[2]
tbbmax[2] = bbmax[2]
view.setExtrema(tbbmin, tbbmax)
#view.qglWidget().updateGL()
view.paintScene()
##view.updateGL()
self.nrenders += 1
ok = view.cursorFromPosition(pos, pt)
if verbose: print('ok:', ok, pt.np)
if ok:
ok = view.positionFromCursor(int(pt[0]), int(pt[1]), pt)
if verbose: print('depth:', ok, pt.np)
if ok:
return pt[2]
# print('get_depth: point not found:', pos, pt)
return None
def get_alt_color(self, props, colorset=None, conv=True, get_bg=True):
if props is None:
return None
if colorset is None:
colorset = self.colorset
col = None
while col is None and colorset:
col = self.get_alt_color_s(props, colorset, conv)
if col is None:
colorset = self.colorset_inheritance.get(colorset)
if isinstance(col, dict) and get_bg:
bg = col.get('bg')
if not bg:
bg = col.get('fg')
if bg is not None:
col = bg
return col
[docs] def get_alt_colors(self, props, colorset=None, conv=True):
''' Get backgroud, foreground colors (fill/border)
'''
colors = self.get_alt_color(props, colorset, conv, get_bg=False)
if colors is None:
return None, None
if not isinstance(colors, dict):
return colors, colors
bg = colors.get('bg')
fg = colors.get('fg')
if bg is None and fg is not None:
bg = fg
elif bg is not None and fg is None:
fg = bg
return bg, fg
@staticmethod
def get_alt_color_s(props, colorset='map_3d', conv=True):
if not props:
return None
color = None
if props.label_alt_colors:
color = props.label_alt_colors.get(props.label, {}).get(colorset)
if not color:
if not props.alt_colors:
return None
color = props.alt_colors.get(colorset)
def convert_color(color):
if isinstance(color, str) and color.startswith('#'):
c1 = color[1::2]
c2 = color[2::2]
col = [float(int('%s%s' % (x, y), base=16)) / 255. for x, y in
zip(c1, c2)]
# print('alt color:', props.main_group, col)
return col
try:
return float(color)
except Exception:
return color
if color:
if not conv:
return color
if isinstance(color, str):
return convert_color(color)
elif isinstance(color, dict):
return {k: convert_color(v) for k, v in color.items()}
def apply_depths(self, meshes):
self.nrenders = 0
object_win_size = (2., 2.)
self.build_depth_wins((250, 250))
for main_group, mesh_l in meshes.items():
props = self.group_properties.get(main_group)
if not props:
print('skip group', main_group, 'with no properties')
continue
if props.text or props.depth_map or props.arrow:
# text and arrows will be done just after.
# depth maps will not need it
continue
level = props.level
win = self.depth_wins.get(level)
debug = False
if win is not None:
view = win.view()
else:
view = None
if mesh_l is not None:
print('processing corridor depth:', main_group, '(level:',
level, ')')
alt_colors = self.get_alt_colors(props)
hshift = (props.height_shift
if props.height_shift else 0.) * self.z_scale
failed = 0
done = 0
if not isinstance(mesh_l, list):
mesh_l = [mesh_l]
for mesh in mesh_l:
if not hasattr(mesh, 'vertex'):
# not a mesh: skip it
continue
material = mesh.header().get('material', {})
if alt_colors[0] is not None:
material['diffuse'] = alt_colors[0]
if alt_colors[1] is not None:
material['border_color'] = alt_colors[1]
for v in mesh.vertex():
if np.isnan(v[0]):
print('NAN in mesh:', mesh.vertex().np)
z = self.get_depth(v, view, object_win_size)
if z is not None:
v[2] += z # + hshift # done via transform3d
else:
failed += 1
if debug:
print('missed Z:', v)
done += len(mesh.vertex())
if failed != 0:
print('failed:', failed, '/', done)
if float(failed) / done >= 0.2:
print('abnormal failure rate - '
'malfunction in 3D renderings ?')
debug = True
# apply texts depths
text_zshift = 5.
for main_group, mesh_items in meshes.items():
props = self.group_properties.get(main_group)
if not props or not props.text:
continue
for text_item in mesh_items['objects']:
position = text_item.get('properties', {}).get('position')
if position is not None:
level = text_item.get('properties', {}).get('level',
'')
# print('text:', text_item)
# print('text depth:', text_item['objects'][0]['properties']['text'], position, ', level:', level)
win = self.depth_wins.get(level)
if win is not None:
view = win.view()
else:
view = None
hshift = (props.height_shift
if props.height_shift else text_zshift) \
* self.z_scale
# print('hshift:', hshift)
z = self.get_depth(position, view, object_win_size)
# print('z:', z, '->', z + hshift)
if z is not None: # and z + hshift > position[2]:
position[2] = z + hshift
print('built depths in', self.nrenders, 'renderings')
def apply_arrow_depth(self, mesh, props):
alt_color = self.get_alt_color(props)
if alt_color:
mesh.header()['material']['diffuse'] = alt_color
level = props.level
tz_level = props.upper_level
hshift = props.height_shift
if hshift is None:
hshift = 0.
hshift *= self.z_scale
text_hshift = props.arrow_base_height_shift
if text_hshift is None:
text_hshift = 4.
text_hshift *= self.z_scale
text_win = self.depth_wins.get(tz_level)
object_win_size = (8, 8)
win = self.depth_wins.get(level)
view = None
text_view = None
if win is not None:
view = win.view()
if text_win is not None:
text_view = text_win.view()
# print('ARROW DEPTH for', props)
for v in mesh.vertex():
z = self.get_depth(v, view, object_win_size)
if z is not None:
z += hshift
old_z = v[2] # old_z is a weight between text and z
text_z = self.get_depth(v, text_view, object_win_size)
if text_z is None:
text_z = 0.
text_z += text_hshift
new_z = z * old_z + text_z * (1. - old_z)
# print('w:', old_z, ', t: ', text_z, ', a:', z, ':', new_z)
v[2] = new_z
else:
pass # warn ?
def build_wells_with_depths(self, meshes):
views = {level: win.view() for level, win in self.depth_wins.items()}
for main_group in list(meshes.keys()):
specs = meshes[main_group]
props = self.group_properties.get(main_group)
if not props or not specs:
continue
method = None
stype = props.label
if hasattr(self, 'make_%s' % stype):
method = getattr(self, 'make_%s' % stype)
elif props.well:
method = self.make_well
if method:
level = props.level
next_level = props.upper_level
view = views.get(level)
view_up = views.get(next_level)
wells = None
for ws in specs:
try:
center, radius, z, height = ws
except TypeError:
import traceback
traceback.print_exc()
print('= while processing', main_group)
print('ws:', ws)
print('method:', method)
print('specs:', specs)
raise
c3 = (center[0], center[1], 0.)
z0 = self.get_depth(c3, view)
if z0 is not None:
z = z0
z0 = self.get_depth(c3, view_up)
if z0 is not None:
height = z0 - z
shift = props.height_shift
if shift is None:
shift = 0.
pheight = props.height
if pheight is None:
pheight = 0.
well = method(center, radius,
z + shift * self.z_scale,
height + pheight * self.z_scale,
well_type=None,
props=props)
if wells is None:
wells = well
else:
aims.SurfaceManip.meshMerge(wells, well)
meshes[main_group + '_tri'] = wells
self.group_properties[main_group + '_tri'] = props
def tesselate(self, mesh, flat=False):
import anatomist.api as ana
a = ana.Anatomist()
win = getattr(self, '_tesselate_win', None)
if win is None:
# we might need to have an existing window in order to initialize
# OpenGL things.
win = a.createWindow('3D')
self._tesselate_win = True
a.setUserLevel(5) # needed to use tesselation fusion
#if 'transformation' in mesh.header():
## if the mesh has a 3D transformation, don't do that
#flat = False ## FIXME
if flat:
projv = aims.Point3df(0., 0., 1.) # project vertically
if 'transformation' in mesh.header():
tr = aims.AffineTransformation3d(
mesh.header()['transformation'])
projv = tr.transform(projv) \
- tr.transform(aims.Point3df(0, 0, 0))
orig_pos = np.array(mesh.vertex(), copy=True)
# tessalate a projected, flmattened mesh, restore after tesselation
v = mesh.vertex().np
v -= np.expand_dims(projv.np, 0) * np.expand_dims(v.dot(projv), 1)
amesh = a.toAObject(mesh)
a.releaseObject(amesh)
all_obj = a.getObjects()
atess = a.fusionObjects([amesh], method='TesselationMethod')
if not atess:
print('cannot make tesselation object for:', amesh.name)
return None
atess_m = [o for o in a.getObjects()
if o not in all_obj and o != atess][0]
# force tesselating
atess.render([], ana.cpp.ViewState())
tess = a.toAimsObject(atess_m)
if flat:
if len(tess.vertex()) != 0:
mvert = mesh.vertex().np
for tv in tess.vertex():
i = np.argmin(np.sum((mvert - tv) ** 2, axis=1))
tv[:] = orig_pos[i]
#print('tesselate flat. orig vert:', len(orig_pos), len(tess.vertex()))
for v, ov in zip(mesh.vertex(), orig_pos):
v[:] = ov
# find nearest vertex for tesselated
if len(tess.vertex()) == 0:
return None # no tesselation
del atess
del atess_m
del amesh
tess.header().update(
{k: copy.deepcopy(v) for k, v in mesh.header().items()})
# restore shared texture images (avoid duplications)
if 'textures' in mesh.header():
for tt, tv in mesh.header()['textures'].items():
tim = tv.get('image')
if tim is not None:
tess.header()['textures'][tt]['image'] = tim
if 'polygon_dimension' in tess.header():
del tess.header()['polygon_dimension']
if 'material' in tess.header():
mat = tess.header()['material']
else:
mat = {}
mat['face_culling'] = 0
return tess
def make_cat_flap(self, mesh, color=[1., 0., 0., 0.8]):
def connected_meshes(mesh):
vert_mesh = {}
for link in mesh.polygon():
mesh1 = vert_mesh.get(link[0])
mesh2 = vert_mesh.get(link[1])
if mesh1 is None and mesh2 is None:
# segment in new mesh
mesh1 = [link[0], link[1]]
vert_mesh[link[0]] = mesh1
vert_mesh[link[1]] = mesh1
elif mesh1 is None:
# prepend to mesh2
mesh2.insert(0, link[0])
vert_mesh[link[0]] = mesh2
elif mesh2 is None:
# append to mesh1
mesh1.append(link[1])
vert_mesh[link[1]] = mesh1
else:
# connect 2 meshes
if mesh1 is mesh2:
# loop
# print('mesh1 is mesh2')
if link[0] == mesh1[-1]:
mesh1.append(link[1])
continue
mesh1 += mesh2
for v in mesh2:
vert_mesh[v] = mesh1
# check duplicates
meshes = []
for m in vert_mesh.values():
if len([n for n in meshes if n is m]) == 0:
meshes.append(m)
return meshes
def slice_segment(mesh, v0, v, alt, offset, slen, xradius,
zradius, connected=False, prev_v=None, next_v=None):
def shift_point(p, shift_s, sh_fac):
return p \
+ ((p.dot(shift_s[0]) + shift_s[1]) * shift_s[2]
* sh_fac[0]
+ (p.dot(shift_s[3]) + shift_s[4]) * shift_s[5]
* sh_fac[1]) * shift_s[6]
def section_vertices(v0, xdir, zdir, xradius, zradius, shift_s,
sh_fac):
pts = [v0 - xdir * xradius - zdir * zradius / 2,
v0 - xdir * xradius + zdir * zradius / 2,
v0 - xdir * xradius / 2 + zdir * zradius,
v0 + xdir * xradius / 2 + zdir * zradius,
v0 + xdir * xradius + zdir * zradius / 2,
v0 + xdir * xradius - zdir * zradius / 2,
v0 + xdir * xradius / 2 - zdir * zradius,
v0 - xdir * xradius / 2 - zdir * zradius]
return [shift_point(p, shift_s, sh_fac) for p in pts]
def add_prev_section_vertices(mesh, alt, connected, v0, xdir,
zdir, xradius, zradius, shift_s,
sh_fac):
vert = mesh[alt].vertex()
if not connected or len(mesh[1-alt].vertex()) == 0:
vert += section_vertices(v0, xdir, zdir, xradius, zradius,
shift_s, sh_fac)
else:
vert += mesh[1-alt].vertex()[-8:]
def section_polygons(n):
poly = [((n+i, n+i+8, n+8+(i+1)%8),
(n+i, n+8+(i+1)%8, n+(i+1)%8))
for i in range(8)]
#poly = [((n+i+8, n+i, n+8+(i+1)%8),
#(n+8+(i+1)%8, n+i, n+(i+1)%8))
#for i in range(8)]
return [p for dp in poly for p in dp]
def build_shift(v0, prev_v, direc):
if prev_v is None:
return (aims.Point3df(0, 0, 0), 0., 0.)
prev_direc = (v0 - prev_v).normalize()
diff_axis = prev_direc.crossed(direc) # rotation axis
if diff_axis.norm2() != 0:
diff_plane = direc.crossed(diff_axis).normalize()
diff_offset = -diff_plane.dot(v0)
n = diff_axis.norm()
if diff_axis.norm() > 0.95:
n = 0.95
diff_angle = math.asin(n) / 2
diff_depth = math.tan(diff_angle)
else:
return (aims.Point3df(0, 0, 0), 0., 0.)
shift_s = (diff_plane, diff_offset, -diff_depth)
return shift_s
def build_shift2(next_v, v, direc):
if next_v is None:
return (aims.Point3df(0, 0, 0), 0., 0.)
next_direc = (next_v - v).normalize()
diff_axis = direc.crossed(next_direc) # rotation axis
if diff_axis.norm2() != 0:
diff_plane = direc.crossed(diff_axis).normalize()
diff_offset = -diff_plane.dot(v)
n = diff_axis.norm()
if diff_axis.norm() > 0.95:
n = 0.95
diff_angle = math.asin(n) / 2
diff_depth = math.tan(diff_angle)
else:
return (aims.Point3df(0, 0, 0), 0., 0.)
shift_s = (diff_plane, diff_offset, diff_depth)
return shift_s
direc = (v - v0).normalize()
mlen = (v - v0).norm()
if mlen == 0:
# zero length segment: nothing to do.
return alt, offset
# section plane
xdir = direc.crossed((0., 0., 1.))
if xdir.norm2() == 0:
xdir = aims.Point3df(1, 0, 0)
zdir = aims.Point3df(0, 1, 0)
else:
zdir = xdir.crossed(direc)
prev_shift_s = build_shift(v0, prev_v, direc)
# next_v = None
next_shift_s = build_shift2(next_v, v, direc)
shift_s = prev_shift_s + next_shift_s + (direc, )
x = 0.
vert = [m.vertex() for m in mesh]
poly = [m.polygon() for m in mesh]
## DEBUG
#n = vert[0].size()
#vert[0] += [v, v + shift_s[3]* 2, v + direc]
#poly[0] += [(n, n+1, n+2)]
#n = vert[1].size()
#if next_v:
#prev_direc = (next_v - v).normalize()
#vert[1] += [v, v+shift_s[3]* 1.5, v0 + prev_direc*0.6]
#poly[1] += [(n, n+1, n+2)]
if offset > 0:
x = min((slen - offset, mlen))
add_prev_section_vertices(mesh, alt, False, v0, xdir,
zdir, xradius, zradius, shift_s,
(1., 0))
sfac = x / mlen
vert[alt] += section_vertices(v0 + direc * x, xdir, zdir,
xradius, zradius, shift_s,
(1.-sfac, sfac))
poly[alt] += section_polygons(len(vert[alt]) - 16)
connected = True
if x <= mlen:
alt = 1 - alt
x2 = x
for x in np.arange(x, mlen - slen, slen):
x2 = min(x + slen, mlen)
sfac = x / mlen
add_prev_section_vertices(mesh, alt, connected,
v0 + direc * x, xdir, zdir, xradius,
zradius, shift_s, (1.-sfac, sfac))
sfac = x2 / mlen
vert[alt] += section_vertices(v0 + direc * x2, xdir, zdir,
xradius, zradius, shift_s,
(1.-sfac, sfac))
poly[alt] += section_polygons(len(vert[alt]) - 16)
connected = True
if x2 <= mlen:
alt = 1 - alt
if x2 < mlen:
sfac = x2 / mlen
add_prev_section_vertices(mesh, alt, connected,
v0 + direc * x2, xdir, zdir,
xradius, zradius, shift_s,
(1.-sfac, sfac))
vert[alt] += section_vertices(v0 + direc * mlen, xdir, zdir,
xradius, zradius, shift_s,
(0., 1.))
poly[alt] += section_polygons(len(vert[alt]) - 16)
connected = True
offset = (offset + mlen) - int((offset + mlen) / slen) * slen
return alt, offset
slen = 1. # length of zebra item
cols = [color, [1., 1., 1., 0.8]] # alterning colors
xradius = 0.3
zradius = 0.2
meshes = connected_meshes(mesh)
vert = mesh.vertex()
smesh = [aims.AimsSurfaceTriangle(), aims.AimsSurfaceTriangle()]
smesh[0].header()['material'] = {'diffuse': cols[0]}
smesh[1].header()['material'] = {'diffuse': cols[1]}
alt = 0
for sub_mesh in meshes:
offset = 0.
connected = False
prev_v = None
# smesh = [aims.AimsSurfaceTriangle(), aims.AimsSurfaceTriangle()]
v0 = vert[sub_mesh[0]]
n = len(sub_mesh)
for i, v in enumerate(sub_mesh[1:]):
v1 = vert[v]
if i >= n - 2:
next_v = None
else:
next_v = vert[sub_mesh[i + 2]]
alt, offset = slice_segment(smesh, v0, v1, alt, offset,
slen, xradius, zradius, connected,
prev_v, next_v)
connected = True
prev_v = v0
v0 = v1
smesh[0].updateNormals()
smesh[1].updateNormals()
return smesh
def recolor_text_specs(self, text_specs, diffuse):
for tospec in text_specs['objects']:
for tspec in tospec['objects']:
props = tspec.setdefault('properties', {})
material = props.setdefault('material', {})
material['diffuse'] = diffuse
def postprocess(self, meshes):
self.ground_level = DefaultItemProperties.ground_level
# lighen texts for black background
tspec = meshes.get('annotations_text')
if tspec is not None:
self.recolor_text_specs(tspec, [.8, .8, .8, 1.])
tspec = meshes.get('rues sans plaques_text')
if tspec is not None:
self.recolor_text_specs(tspec, [.6, .6, .6, 1.])
# move arrows in order to follow text in 3D
self.attach_arrows_to_text(meshes, with_squares=False)
# get ground altitude map
#self.load_ground_altitude(
#os.path.join(os.path.dirname(self.svg_filename),
#'altitude', 'real', 'alt_image.png'))
bdalti_map = os.path.join(
os.path.dirname(self.svg_filename),
'altitude', 'BDALTIV2_2-0_75M_ASC_LAMB93-IGN69_FRANCE_2020-04-28',
'BDALTIV2', 'map.json')
self.load_ground_altitude_bdalti(bdalti_map)
# make depth maps
for level, mesh_def in self.depth_meshes_def.items():
mesh, props = mesh_def
if mesh is not None:
if props.relative_to == self.ground_level:
print('add ground alt on:', level)
self.add_ground_alt(mesh)
self.delaunay(mesh)
meshes['grille surface'] = self.build_ground_grid()
props = ItemProperties()
props.level = self.ground_level
props.category = 'Surface'
self.group_properties['grille surface'] = props
# apply depths to corridors
self.apply_depths(meshes)
# build wells with inf/sup depths
self.build_wells_with_depths(meshes)
# apply real depths to arrows
for main_group in list(meshes.keys()):
props = self.group_properties.get(main_group)
if not props or not props.arrow:
continue
mesh_l = meshes.get(main_group)
if not mesh_l:
continue
if not isinstance(mesh_l, list):
mesh_l = [mesh_l]
for mesh in mesh_l:
self.apply_arrow_depth(mesh, props)
if 'material' not in mesh.header():
mesh.header()['material'] \
= {'diffuse': [1., 0.5, 0., 1.]}
mesh.header()['material']['line_width'] = 2.
# travel speed factor
self.setup_travel_speed()
# extrude corridors walls
for main_group in list(meshes.keys()):
# here we iterate through list(keys) instead of using items()
# because some processings will insert new meshes in meshes,
# and this would make the iteration fail
props = self.group_properties.get(main_group)
if not props:
continue
# print(props)
mesh_l = meshes[main_group]
height_map = props.use_height_map
if height_map in ('none', 'None', 'false'):
height_map = None
if mesh_l and props.corridor or props.block or props.wall:
print('extrude:', main_group, props.corridor, props.block,
height_map)
if not isinstance(mesh_l, list):
mesh_l = [mesh_l]
for mesh in mesh_l:
if not hasattr(mesh, 'vertex'):
# not a mesh
continue
height = props.height * self.z_scale
ceil, wall = self.extrude(mesh, height, height_map)
if 'material' not in ceil.header():
ceil.header()['material'] \
= {'diffuse': [0.3, 0.3, 0.3, 1.]}
elif 'diffuse' not in ceil.header()['material']:
ceil.header()['material']['diffuse'] = [0.3, 0.3,
0.3, 1.]
elif props.contrast_floor or (
props.contrast_floor is None
and not self.enable_texturing):
color = list(ceil.header()['material']['diffuse'])
intensity \
= np.sqrt(np.sum(np.array(color[:3])**2) / 3)
if intensity <= 0.75:
for i in range(3):
c = color[i] + 0.4
# if c > 1.:
# c = 1.
color[i] = c
m = max(color[:3])
if m > 1.:
for i in range(3):
color[i] /= m
else:
for i in range(3):
c = color[i] - 0.4
if c < 0.:
c = 0.
color[i] = c
ceil.header()['material']['diffuse'] = color
meshes.setdefault(main_group + '_wall', []).append(wall)
meshes.setdefault(main_group + '_ceil', []).append(ceil)
self.group_properties[main_group + '_wall'] = props
self.group_properties[main_group + '_ceil'] = props
self.group_properties[main_group + '_floor'] = props
# build floor or ceiling meshes using tesselated objects
# (anatomist)
if props.block:
# "blocks" have a closed ceiling
tess_mesh = self.tesselate(ceil, flat=True)
if tess_mesh is not None:
meshes.setdefault(main_group + '_ceil_tri',
[]).append(tess_mesh)
self.group_properties[main_group + '_ceil_tri'] \
= props
if props.corridor:
# corridor have a closed floor
# print('tesselate corridor:', props.main_group)
tess_mesh = self.tesselate(mesh, flat=True)
if tess_mesh is not None:
meshes.setdefault(main_group + '_floor_tri',
[]).append(tess_mesh)
self.group_properties[main_group + '_floor_tri'] \
= props
# set border color to filar meshes
if mesh_l:
if not isinstance(mesh_l, list):
mesh_l = [mesh_l]
for mesh in mesh_l:
if isinstance(mesh, aims.AimsTimeSurface_2):
mat = mesh.header().get('material')
if mat is not None and 'border_color' in mat:
mat['diffuse'] = mat['border_color']
# merge meshes in each group
self.merge_meshes_by_group(meshes)
# fix normals of limestone parts
for corridor, mesh in meshes.items():
if corridor.startswith('calcaire') \
and corridor.endswith('_ceil_tri'):
mesh.normal().assign(
np.array([0., 0., 1.]) * np.ones((len(mesh.vertex()), 1)))
# make cat flap mesh
catflap_col = {'bas': [0.85, 0.56, 0.16, 0.8],
u'injecté': [0.66, 0.61, 0.63, 0.8]}
for main_group in list(meshes.keys()):
props = self.group_properties.get(main_group)
if not props or not props.catflap:
continue
mesh = meshes[main_group]
if mesh is not None:
color = catflap_col.get(props.label)
if color is None:
color = mesh.header().get('material', {}).get('diffuse')
if not color:
color = [1., 0., 0., 0.8]
cat_flap = self.make_cat_flap(mesh, color)
meshes['%s_0' % main_group] = cat_flap[0]
meshes['%s_1' % main_group] = cat_flap[1]
self.group_properties['%s_0' % main_group] = props
self.group_properties['%s_1' % main_group] = props
del meshes[main_group]
# sounds and photos depth + markers meshes
object_win_size = [8, 8]
protos = getattr(self, 'marker_types', {})
for mtype, proto in protos.items():
mesh = None
if mtype == 'lights':
mesh_proto = None
for mpos in getattr(self, mtype):
pos = mpos[0][:4]
props = mpos[0][4]
hshift = pos[2]
level = pos[3]
win = self.depth_wins[level]
z = self.get_depth(pos[:2] + [0.], win.view(),
object_win_size)
if z is None:
print('failed to get depth for:', mtype, pos, level)
z = 0.
mpos[0][2] = z + hshift
else:
mesh_proto = getattr(self, proto)
print(mtype, 'proto:', len(mesh_proto.vertex()))
for mpos in getattr(self, mtype):
pos = mpos[0][:4]
hshift = pos[2]
# radius = mpos[0][4]
level = pos[3]
win = self.depth_wins[level]
z = self.get_depth(pos[:2] + [0.], win.view(),
object_win_size)
if z is None:
print('failed to get depth for:', mtype, pos, level)
z = 0.
z += hshift
mpos[0][2] = z
pos[2] = z
# print('marker:', level, pos)
mmesh = type(mesh_proto)(mesh_proto) # copy
trans = aims.AffineTransformation3d()
trans.setTranslation(pos[:3])
aims.SurfaceManip.meshTransform(mmesh, trans)
if mesh is None:
mesh = mmesh
else:
aims.SurfaceManip.meshMerge(mesh, mmesh)
# print('marker:', pos, len(mesh.vertex()))
if mesh is not None:
main_group = '%s_mesh' % mtype
self.mesh_dict[main_group] = mesh
meshes[main_group] = mesh
props = ItemProperties()
props.category = 'markers'
self.group_properties[main_group] = props
print('--- marker mesh:', main_group, len(mesh.vertex()), props)
else:
print('NO MARKER MESH:', mtype)
[docs] def setup_travel_speed(self):
'''
'''
meta = self.get_metadata(self.svg)
travel_level = meta.get('travel_ref_level', 'sup')
travel_speed_base = meta.get('travel_speed_base', 0.03)
travel_speed = meta.get('travel_speed_alt_factor', 0.003)
level_mesh = self.depth_meshes_def.get(travel_level)
travel_mat = np.array([0., 0., 0.003, 0., 0.03])
travel_mat[2] = travel_speed
travel_mat[4] = travel_speed_base
if level_mesh is not None:
zavg = np.average(level_mesh[0].vertex().np[:, 2])
travel_mat[3] = -zavg * travel_mat[2]
self.travel_speed_projection = travel_mat
return travel_mat
[docs] def extrude(self, mesh, distance, height_map=None):
'''
This is an overloaded version of the static SvgToMesh.extrude(mesh,
distance)
When height_map is given, use this level height map to shift Z
positions
'''
if height_map is None:
return super().extrude(mesh, distance)
# print('MAP extrude:', height_map)
win = self.depth_wins.get(height_map)
view = win.view()
up = aims.AimsTimeSurface(mesh)
object_win_size = (2., 2.)
for v in up.vertex():
z = self.get_depth(v, view, object_win_size)
if z is None:
v[2] += distance # fallback to default
else:
# we inverse (again) because depth maps are already inverted
v[2] -= z
walls = aims.AimsTimeSurface(3)
walls.header().update(
dict([(k, copy.deepcopy(v)) for k, v in mesh.header().items()]))
material = {}
if 'material' in walls.header():
material = walls.header()['material']
material['face_culling'] = 0
walls.header()['material'] = material
vert0 = mesh.vertex()
poly0 = mesh.polygon()
vert = walls.vertex()
poly = walls.polygon()
vert.assign(vert0 + up.vertex())
nv = len(vert0)
for line in poly0:
poly.append((line[0], line[1], nv + line[0]))
poly.append((line[1], nv + line[1], nv + line[0]))
walls.updateNormals()
return up, walls
def attach_arrows_to_text(self, meshes, with_squares=False):
# find text attached to each arrow
for arrow, mesh_l in meshes.items():
props = self.group_properties.get(arrow)
if props and props.arrow and mesh_l:
if not isinstance(mesh_l, list):
mesh_l = [mesh_l]
for mesh in mesh_l:
if not hasattr(mesh, 'vertex'):
print('*** WARNING: ***')
print('A non-mesh object lies in an arrows '
'layer/group')
print(props)
print('faulty mesh:', mesh)
continue
text_o = self.find_text_for_arrow(meshes, mesh)
# print('arrow/text:', mesh, text_o)
if text_o:
props = text_o['properties']
pos = props['position']
vert = mesh.vertex()
size = props['size']
# print('text pos:', pos)
# vert2 = aims.vector_POINT3DF(vert)
decal = aims.Point3df(pos[0], pos[1], vert[0][2]) \
- vert[0]
# print('decal text', text_o['objects'][0]['properties']['text'], list(decal), 'to:', pos, ', size', size)
n = len(vert)
for i, v in enumerate(vert):
# print('vert', i, ':', v.np, '->', (v + decal * (1. - v[2])).np)
# v += decal * float(n - i) / n
v += decal * (1. - v[2])
if with_squares:
# debug: display rectangle around text location
size = props['size']
x0 = pos[0] - size[0] / 2
x1 = pos[0] + size[0] / 2
y0 = pos[1] - size[1] / 2
y1 = pos[1] + size[1] / 2
vert = mesh.vertex()
z = vert[0][2]
n = len(vert)
vert += [(x0, y0, z), (x1, y0, z), (x1, y1, z),
(x0, y1, z)]
poly = mesh.polygon()
poly += [(n, n+1), (n+1, n+2), (n+2, n+3),
(n+3, n)]
def find_text_for_arrow(self, meshes, mesh):
dmin = -1
text_min = None
point = mesh.vertex()[0][:2]
# print('find_text_for_arrow', mesh, point)
for mtype, mesh_items in meshes.items():
if mtype.endswith('_text'):
# print(mtype, ' text:', mesh_items)
for text in mesh_items['objects']:
# print('text:', text)
props = text['properties']
# print('props:', props)
pos = props.get('position')
size = props['size']
# print('pos:', pos, ', size:', size)
# distances to each segment
x0 = pos[0] - size[0] / 2
x1 = pos[0] + size[0] / 2
y0 = pos[1] - size[1] / 2
y1 = pos[1] + size[1] / 2
if point[0] < x0:
d0 = x0 - point[0]
elif point[0] > x1:
d0 = point[0] - x1
else:
d0 = 0
if point[1] < y0:
d1 = y0 - point[1]
elif point[1] > y1:
d1 = point[1] - y1
else:
d1 = 0
d = d0 * d0 + d1 * d1
if dmin < 0 or d < dmin:
dmin = d
text_min = text
if d == 0:
# found a good match, skip other tests
break
return text_min
def build_ground_grid(self):
for border in ('bord complet', 'bord_general', 'bord_sud'):
layer = [l for l in self.svg.getroot()
if l.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
== border]
if len(layer) != 0:
break
if not layer:
print('No border layer found. Not building ground grid.')
return aims.AimsTimeSurface_2() # no layer
layer = layer[0]
label = layer.get('{http://www.inkscape.org/namespaces/inkscape}label')
print('ground - border layer:', label)
self.main_group = label
bounds = self.boundingbox(layer)
# print('ground grid bounds:', bounds)
if bounds[0] is None:
print('No border bounding box. Not building ground grid.')
return aims.AimsTimeSurface_2()
props = [y for x, y in self.group_properties.items()
if y.label == label]
interval = 5.
if props:
props = props[0]
if props.grid_interval:
interval = props.grid_interval
grid = np.mgrid[bounds[0][0]:bounds[1][0]:interval,
bounds[0][1]:bounds[1][1]:interval].T
grid_v = grid.reshape((grid.shape[0] * grid.shape[1], 2))
grid_v = np.hstack((grid_v, np.zeros((grid_v.shape[0], 1))))
grid_s = [(i + j*grid.shape[1], i+1 + j*grid.shape[1])
for j in range(grid.shape[0])
for i in range(grid.shape[1] - 1)] \
+ [(i + j*grid.shape[1], i + (j + 1) * grid.shape[1])
for j in range(grid.shape[0] - 1)
for i in range(grid.shape[1])]
mesh = aims.AimsTimeSurface_2()
mesh.vertex().assign(grid_v)
mesh.polygon().assign(grid_s)
mesh.header()['material'] = {'diffuse': [0.9, 0.9, 0.9, 1.]}
self.ground_grid = mesh
# print('ground grid:', mesh.vertex().size(), 'vertices')
return mesh
def make_skull_model(self, xml):
cm = CataMapTo2DMap()
protos = cm.find_protos(xml)
if not protos:
return
skproto = protos.get('label')
if not skproto:
return
skproto = skproto.get('ossuaire')
if skproto is None:
return
self.main_group = 'ossuaire_model'
skmesh_l = aims.AimsTimeSurface_2()
for child in skproto['element']:
aims.SurfaceManip.meshMerge(
skmesh_l, self.read_path(child,
self.proto_scale * skproto['trans']))
skmesh_up_l, skmesh_w = self.extrude(skmesh_l, 0.7)
skmesh_bk = self.tesselate(skmesh_l)
skmesh_up = self.tesselate(skmesh_up_l)
aims.SurfaceManip.invertSurfacePolygons(skmesh_w)
skmesh_w.updateNormals()
aims.SurfaceManip.invertSurfacePolygons(skmesh_bk)
skmesh_bk.updateNormals()
aims.SurfaceManip.meshMerge(skmesh_w, skmesh_bk)
aims.SurfaceManip.meshMerge(skmesh_w, skmesh_up)
vert = np.asarray(skmesh_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
bbmax = aims.Point3df(np.max(vert, axis=0))
center = (bbmin + bbmax) / 2
vert -= center
skmesh_w.vertex().assign(vert)
q = aims.Quaternion()
q.fromAxis([1., 0., 0.], -np.pi / 2)
tr = aims.AffineTransformation3d(q)
aims.SurfaceManip.meshTransform(skmesh_w, tr)
vert = np.asarray(skmesh_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
vert += [0., 0., 1.5 - bbmin[2]]
skmesh_w.vertex().assign(vert)
return skmesh_w
def make_fontis_model(self, xml):
cm = CataMapTo2DMap()
protos = cm.find_protos(xml)
if not protos:
return
fproto = protos['label'].get('fontis')
if fproto is None:
return
self.main_group = 'fontis'
fmesh_l = aims.AimsTimeSurface_2()
fmesh_l.header()['material'] = {'diffuse': [0.74, 0.33, 0., 1.]}
for child in fproto['element'][1:]:
aims.SurfaceManip.meshMerge(
fmesh_l, self.read_path(child,
self.proto_scale * fproto['trans']))
fmesh_up_l, fmesh_w = self.extrude(fmesh_l, 0.3)
fmesh_bk = self.tesselate(fmesh_l)
fmesh_up = self.tesselate(fmesh_up_l)
aims.SurfaceManip.invertSurfacePolygons(fmesh_w)
fmesh_w.updateNormals()
aims.SurfaceManip.invertSurfacePolygons(fmesh_bk)
fmesh_bk.updateNormals()
aims.SurfaceManip.meshMerge(fmesh_w, fmesh_bk)
aims.SurfaceManip.meshMerge(fmesh_w, fmesh_up)
vert = np.asarray(fmesh_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
bbmax = aims.Point3df(np.max(vert, axis=0))
center = (bbmin + bbmax) / 2
vert -= center
fmesh_w.vertex().assign(vert)
vert = np.asarray(fmesh_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
vert += [0., 0., - bbmin[2]]
fmesh_w.vertex().assign(vert)
return fmesh_w
def make_water_scale_model(self, pos, size):
s1 = size * 0.1
s2 = size * 0.2
s9 = size * 0.9
s95 = size * 0.95
p = aims.Point3df(pos)
para = aims.SurfaceGenerator.parallelepiped_wireframe(
p - (size, size, s1), p + (size, size, s1))
para2 = aims.SurfaceGenerator.parallelepiped_wireframe(
p - (s9, s9, s1), p + (s9, s9, s1))
para3 = aims.SurfaceGenerator.parallelepiped_wireframe(
p + (-s2, s95, -size), p + (s2, size, size))
aims.SurfaceManip.meshMerge(para, para2)
aims.SurfaceManip.meshMerge(para, para3)
para.header()['material'] = {'diffuse': [1., 0.9, 0.64, 1.]}
mesh = aims.AimsTimeSurface_3()
cube = aims.SurfaceGenerator.cube((0., 0., 0.), size)
vert = np.asarray(cube.vertex()) * (1., 1., 0.1)
vert2 = np.asarray(cube.vertex()) * (.9, .9, 0.1)
mesh.vertex().assign(np.vstack((vert, vert2)) + pos)
poly = np.asarray(cube.polygon()[2:-2])
poly2 = poly + len(cube.vertex())
c = np.array(poly2[:, 2])
poly2[:, 2] = poly2[:, 1]
poly2[:, 1] = c
poly = np.vstack((poly, poly2))
poly2 = np.array([(0, 24, 3), (3, 24, 27), (3, 27, 6), (6, 27, 30),
(6, 30, 9), (9, 30, 33), (9, 33, 0), (0, 33, 24)])
poly3 = np.vstack((poly2[:, 0], poly2[:, 2], poly2[:, 1])).T + 12
poly = np.vstack((poly, poly2, poly3))
mesh.polygon().assign(poly)
vert3 = np.asarray(cube.vertex()) * (0.2, 0.025, 1.) \
+ (0, size * 0.975, 0) + pos
cube.vertex().assign(vert3)
aims.SurfaceManip.meshMerge(mesh, cube)
mesh.header()['material'] = {'diffuse': [.96, 0.88, 0.64, 1.]}
water = aims.AimsTimeSurface_3()
vert3 = np.hstack((vert2[(0, 3, 6, 9), :2], np.zeros((4, 1)))) + pos
water.vertex().assign(vert3)
water.polygon().assign([(0, 2, 1), (0, 3, 2)])
water.header()['material'] = {'diffuse': [0.5, 0.6, 1., 0.7],
'face_culling': 0}
return {'etiage_line': para,
'etiage_wall_tri': mesh,
'etiage_water_tri': water}
def make_lily_model(self, xml):
cm = CataMapTo2DMap()
protos = cm.find_protos(xml)
if not protos:
return
lproto = protos['label'].get('lys')
if lproto is None:
print('No proto for lys')
return
self.main_group = 'lys'
lily_l = aims.AimsTimeSurface_2()
for child in lproto['element']:
aims.SurfaceManip.meshMerge(
lily_l, self.read_path(child,
self.proto_scale * lproto['trans']))
lily_up_l, lily_w = self.extrude(lily_l, 1.)
lily_bk = self.tesselate(lily_l)
lily_up = self.tesselate(lily_up_l)
aims.SurfaceManip.invertSurfacePolygons(lily_w)
lily_w.updateNormals()
aims.SurfaceManip.invertSurfacePolygons(lily_bk)
lily_bk.updateNormals()
aims.SurfaceManip.meshMerge(lily_w, lily_bk)
aims.SurfaceManip.meshMerge(lily_w, lily_up)
vert = np.asarray(lily_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
bbmax = aims.Point3df(np.max(vert, axis=0))
center = (bbmin + bbmax) / 2
vert -= center
lily_w.vertex().assign(vert)
q = aims.Quaternion()
q.fromAxis([1., 0., 0.], -np.pi / 2)
tr = aims.AffineTransformation3d(q)
aims.SurfaceManip.meshTransform(lily_w, tr)
vert = np.asarray(lily_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
vert += [0., 0., 1.5 - bbmin[2]]
lily_w.vertex().assign(vert)
return lily_w
def make_large_sign_model(self, xml):
cm = CataMapTo2DMap()
lproto = cm.find_element(xml,
filters={'layer': 'légende',
'label': 'grande_plaque'})
if lproto is None:
print('No proto for grande_plaque')
return
self.main_group = 'grande_plaque'
lily_l = aims.AimsTimeSurface_2()
todo = [lproto[0]]
trans = lproto[1]
while todo:
child = todo.pop(0)
if child.tag.endswith('}g'):
todo += list(child)
continue
if child.tag.endswith('}text'):
continue
aims.SurfaceManip.meshMerge(
lily_l, self.read_path(child,
self.proto_scale * trans))
lily_up_l, lily_w = self.extrude(lily_l, 0.4)
lily_bk = self.tesselate(lily_l)
lily_up = self.tesselate(lily_up_l)
aims.SurfaceManip.invertSurfacePolygons(lily_w)
lily_w.updateNormals()
aims.SurfaceManip.invertSurfacePolygons(lily_bk)
lily_bk.updateNormals()
aims.SurfaceManip.meshMerge(lily_w, lily_bk)
aims.SurfaceManip.meshMerge(lily_w, lily_up)
vert = np.asarray(lily_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
bbmax = aims.Point3df(np.max(vert, axis=0))
center = (bbmin + bbmax) / 2
vert -= center
lily_w.vertex().assign(vert)
q = aims.Quaternion()
q.fromAxis([1., 0., 0.], -np.pi / 2)
tr = aims.AffineTransformation3d(q)
aims.SurfaceManip.meshTransform(lily_w, tr)
vert = np.asarray(lily_w.vertex())
bbmin = aims.Point3df(np.min(vert, axis=0))
vert += [0., 0., 1.5 - bbmin[2]]
lily_w.vertex().assign(vert)
return lily_w
def make_stair_symbol_model(self):
return self.make_spiral_stair([0, 0], 1., 0., 1.5)
def make_sounds_marker_model(self):
scale = self.symbol_scale
mesh = aims.SurfaceGenerator.icosphere((0, 0, 2.5 * scale),
0.5 * scale, 80)
cone = aims.SurfaceGenerator.cone((0, 0, 2.5 * scale),
(1.3 * scale, 0., 2.7 * scale),
0.5 * scale, 12,
False, True)
aims.SurfaceManip.meshMerge(mesh, cone)
mesh.header()['material'] = {'diffuse': [.8, 0.6, 0., 1.]}
return mesh
def make_photos_marker_model(self):
scale = self.symbol_scale
mesh = aims.SurfaceGenerator.icosphere((0, 0, 2.5 * scale),
0.5 * scale, 80)
cone = aims.SurfaceGenerator.cone((0, 0, scale),
(0, 0, 2.5 * scale), 0.15 * scale, 6,
False, True)
aims.SurfaceManip.meshMerge(mesh, cone)
mesh.header()['material'] = {'diffuse': [1., 0., 0., 1.]}
return mesh
[docs] def save_mesh_dict(self, meshes, dirname, mesh_format='.glb',
mesh_wf_format='.glb', json_filename=None,
map2d_filename=None):
# filter out wells definitions
def mod_key(key, item):
if isinstance(item, aims.AimsTimeSurface_3):
if not key.endswith('_tri'):
key += '_tri'
if isinstance(item, aims.AimsTimeSurface_2):
if not key.endswith('_line'):
key += '_line'
return key
mdict = dict([(mod_key(k, v), v) for k, v in meshes.items()
if not k.endswith('_wells')])
format = mesh_format
wf_format = mesh_wf_format
use_gltf = False
if mesh_format in ('.gltf', '.glb'):
# don't build the GLTF dict because we need to split it in
# categories
format = None
use_gltf = True
if mesh_wf_format in ('.gltf', '.glb'):
wf_format = None
use_gltf = True
summary = super().save_mesh_dict(
mdict, dirname, mesh_format=format, mesh_wf_format=wf_format)
if use_gltf:
from soma.aims import gltf_io
gltf_dicts = {} # public
gltf_p_dicts = {} # private
gltf_lights = None
gltf_p_lights = None
lights = getattr(self, 'lights', None)
if lights:
gltf_lights = super().save_mesh_dict(
{}, dirname, mesh_format=mesh_format, mesh_wf_format=None,
lights=lights)
# print('gltf_lights:', gltf_lights)
lights_p = getattr(self, 'lights_private', None)
if lights_p:
gltf_p_lights = super().save_mesh_dict(
{}, dirname, mesh_format='gltf', mesh_wf_format=None,
lights=lights_p)
# output JSON dict
json_obj = collections.OrderedDict()
# date / version
p = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, p)
try:
import build_version
imp.reload(build_version)
# increment build version
build_version.build_version += 1
# save modified build version
ver_file = build_version.__file__
if ver_file.endswith('.pyc'):
ver_file = ver_file[:-1] # get a .py
try:
print('rewrite version file:', ver_file)
open(ver_file, 'w').write(
'build_version = %d\n' % build_version.build_version)
except IOError as e:
print(e)
pass # oh OK I can't write there.
finally:
del sys.path[0]
json_obj['version'] = build_version.build_version
title = getattr(self, 'title', None)
if title:
json_obj['title'] = title
d = datetime.date.today()
json_obj['date'] = '%04d-%02d-%02d' % d.timetuple()[:3]
if map2d_filename:
json_obj['map'] = map2d_filename
# build 3D layers:
# 0: main_corridors
# 1: unreachable
# 2: text
# 3: parcels
# 4: oss_off
# 5: tech
# 6: limestone
# 7: legend
categories = [
"Couloirs",
"Inaccessible",
"Textes",
"Parcelles",
"Ossuaire officiel",
"Galeries Tech.",
"Remblai",
"Calcaire",
"Légende",
"Surface",
]
json_obj['categories'] = categories
def_categories = [
]
json_obj['default_categories'] = def_categories
used_layers = set()
# print('summary:', summary, '\n')
if 'meshes' in summary:
jmeshes = []
pmeshes = []
for filename, mesh in summary['meshes'].items():
filename = os.path.basename(filename)
group = mesh
# print('mesh:', mesh)
if group.endswith('_tri'):
group = group[:-4]
elif group.endswith('_line'):
group = group[:-5]
props = self.group_properties.get(group)
if not props:
print('no props for mesh:', group, filename)
if '.' in filename: # remove extension
filename = '.'.join(filename.split('.')[:-1])
layer = 0
if 'bord' in filename:
continue # skip
if props and props.depth_map:
print('DEPTH MAP MESH:', filename)
layer = -1 # not displayed
elif props and props.category:
if props.category not in categories:
categories.append(props.category)
layer = categories.index(props.category)
else:
# old way, specific
if (props and props.level == 'tech') \
or '_tech_' in filename \
or 'techniques' in filename \
or 'gtech' in filename or 'ebauches' in filename \
or 'metro' in filename:
layer = 5
elif (props and props.arrow) \
or filename.startswith('plaques ') \
or u' flèches' in filename \
or filename.startswith('etiage_'):
layer = 2
elif 'parcelles' in filename:
layer = 3
elif 'oss off' in filename:
layer = 4
elif (props and props.inaccessible) \
or filename.startswith('anciennes ') \
or 'anciennes galeries' in filename \
or filename.startswith('aqueduc') \
or filename.startswith('ex-') \
or ' inaccessibles' in filename:
layer = 1
elif filename.startswith('remblai'):
layer = 6
elif filename.startswith('calcaire'):
layer = 7
elif u'légende' in filename \
or 'grandes plaques' in filename:
layer = 8
elif 'grille surface' in filename:
layer = 9
if use_gltf:
if 'private' in filename or (props and props.private):
gltf = gltf_p_dicts.setdefault(layer, {})
else:
gltf = gltf_dicts.setdefault(layer, {})
if len(gltf) == 0:
matrix = [-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0,
0, 0, 0, 1]
gltf_io.default_gltf_scene(matrix=matrix, gltf=gltf)
if len(mdict[mesh].vertex()) == 0:
print('mesh has 0 size:', mesh)
else:
self.store_gltf_texmesh(mdict[mesh], mesh, gltf=gltf)
else:
# print('mesh:', layer, ':', filename, props)
size = os.stat(os.path.join(dirname,
filename + '.obj')).st_size
# hash
md5 = hashlib.md5(
open(os.path.join(dirname, filename + '.obj'),
'rb').read()).hexdigest()
if 'private' in filename or (props and props.private):
pmeshes.append([layer, filename, size, md5])
else:
jmeshes.append([layer, filename, size, md5])
used_layers.add(layer)
if use_gltf:
for light_l, l_summary, private, gltf_d in (
(lights, gltf_lights, '', gltf_dicts),
(lights_p, gltf_p_lights, '_private', gltf_p_dicts)):
if light_l:
for light in light_l:
glight = l_summary['gltf_scene']
category = 'Lumières'
categories.append(category)
layer = categories.index(category)
used_layers.add(layer)
gltf_d[layer] = glight
# print('Light dict, layer', layer, ':', glight)
try:
from pygltflib import GLTF2, BufferFormat, ImageFormat
except ImportError:
GLTF2 = None # no GLTF/GLB conversion support
for gltf_d, private, mmeshes in (
(gltf_dicts, '', jmeshes),
(gltf_p_dicts, '_private', pmeshes)):
# regroup GLTFs
for layer, gltf in gltf_d.items():
print('layer:', layer)
if layer < 0:
category = 'hidden'
else:
category = categories[layer]
mformat = mesh_format
if category == 'Lumières':
use_draco = False
# mformat = '.gltf'
else:
use_draco = True
filename = osp.join(
dirname, category + private + mformat)
print('GLTF layer:', layer, ':', filename)
filename = gltf_io.save_gltf(gltf, filename,
use_draco=use_draco)
# print('GLTF mesh:', layer, ':', filename, props)
size = os.stat(filename).st_size
if layer >= 0: # layer -1 is hidden
# hash
md5 = hashlib.md5(
open(filename, 'rb').read()).hexdigest()
mmeshes.append([layer, osp.basename(filename), size,
md5])
json_obj['meshes'] = sorted(jmeshes)
json_obj['meshes_private'] = sorted(pmeshes)
new_nums = {l: i for i, l in enumerate(used_layers)}
categories = [c for i, c in enumerate(categories) if i in used_layers]
# re-number all items
for item in jmeshes:
item[0] = new_nums[item[0]]
for item in pmeshes:
item[0] = new_nums[item[0]]
json_obj['categories'] = categories
if 'Couloirs' in categories:
def_categories.append('Couloirs')
if 'Ossuaire officiel' in categories:
def_categories.append('Ossuaire officiel')
# texts
# TODO: separate them in different layers (hidden...)
if 'text_fnames' in summary:
json_obj['text_fnames'] \
= sorted([os.path.basename(f)
for f in summary['text_fnames'].keys()
if 'private' not in f])
json_obj['text_fnames_private'] \
= sorted([os.path.basename(f)
for f in summary['text_fnames'].keys()
if 'private' in f])
texts = []
json_obj['texts'] = texts
for fname in json_obj['text_fnames']:
size = os.stat(os.path.join(dirname, fname)).st_size
# hash
md5 = hashlib.md5(open(os.path.join(dirname, fname),
'rb').read()).hexdigest()
texts.append([0, fname, size, md5])
texts = []
json_obj['texts_private'] = texts
for fname in json_obj['text_fnames_private']:
size = os.stat(os.path.join(dirname, fname)).st_size
# hash
md5 = hashlib.md5(open(os.path.join(dirname, fname),
'rb').read()).hexdigest()
texts.append([0, fname, size, md5])
# sounds
if self.sounds:
json_obj['sounds'] = self.sounds
if self.sounds_private:
json_obj['sounds_private'] = self.sounds_private
# photos
if self.photos:
json_obj['photos'] = self.photos
if self.photos_private:
json_obj['photos_private'] = self.photos_private
metadata = self.get_metadata(self.svg)
cam_light = metadata.get('camera_light')
if cam_light is not None:
json_obj['camera_light'] = cam_light
def_cat = metadata.get('default_categories')
if def_cat is not None:
def_cat = json.loads(def_cat)
json_obj['default_categories'] = def_cat
travel_speed = getattr(self, 'travel_speed_projection', None)
if travel_speed is not None:
json_obj['travel_speed_projection'] = list(travel_speed)
if json_filename is not None:
with open(json_filename, 'w') as f:
json.dump(json_obj, f, indent=4, sort_keys=False,
ensure_ascii=False)
return json_obj
[docs]class CataMapTo2DMap(svg_to_mesh.SvgToMesh):
'''
Process XML tree to build modified 2D maps
'''
proto_scale = np.array([[0.5, 0, 0],
[0, 0.5, 0],
[0, 0, 1]])
def __init__(self, concat_mesh='bygroup'):
super(CataMapTo2DMap, self).__init__(concat_mesh)
def find_protos(self, xml):
root = xml.getroot()
trans = np.matrix(np.eye(3))
trans2 = root.get('transform')
if trans2 is not None:
transm = self.get_transform(trans2)
if trans is None:
trans = transm
else:
trans = trans * transm
symbols = [x for x in root
if x.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
== u'légende'
or x.get('legend') in ('1', 'True', 'true', 'TRUE')]
if symbols:
symbols = symbols[0]
else:
return
trans2 = symbols.get('transform')
if trans2 is not None:
transm = self.get_transform(trans2)
if trans is None:
trans = transm
else:
trans = trans * transm
trans_org = trans
trans = self.proto_scale * trans_org
labels = {}
ids = {}
#rep_child = ['PSh', 'sans', 'PS sans', 'PSh sans', 'PS', 'P ossements',
#u'échelle', u'\xc3\xa9chelle', 'colim', 'PE', ]
rep_child = []
repl_map = {'id': ids, 'label': labels}
for child in symbols:
eid = child.get('id')
ptype = None
if eid and eid.endswith('_proto'):
ptype = eid[:-6]
plabel = 'id'
pmap = ids
# exception case: if label is the same without _proto
if child.get('label') == ptype:
plabel = 'label'
pmap = labels
else:
label = child.get('label')
if label and label.endswith('_proto'):
ptype = label[:-6]
plabel = 'label'
pmap = labels
if ptype:
element = copy.deepcopy(child)
element.set(plabel, ptype)
item = {'element': element}
etrans = trans
pscale = element.get('proto_scale')
if pscale:
pscale = float(pscale)
pscalem = np.matrix(np.eye(3))
pscalem[0, 0] = pscale
pscalem[1, 1] = pscale
etrans = pscalem * trans_org
bbox = self.boundingbox(child, etrans)
item['boundingbox'] = bbox
item['center'] = ((bbox[0][0] + bbox[1][0]) / 2,
(bbox[0][1] + bbox[1][1]) / 2)
item['trans'] = etrans
replace_children = child.get('replace_children')
if replace_children:
if replace_children in ('1', 'True', 'true', 'TRUE'):
replace_children = True
else:
replace_children = False
elif ptype in rep_child:
replace_children = True
if replace_children:
item['children'] = True
pmap[ptype] = item
return repl_map
def transform_inf_level(self, xml):
todo = [xml.getroot()]
while todo:
element = todo.pop(0)
map_trans = element.get('map_transform')
if map_trans is not None:
trans = element.get('transform')
if trans is None:
trans = map_trans
else:
trans = map_trans + ' ' + trans
element.set('transform', trans)
added = element[:]
todo = added + todo
def shadow1(self, filter_id):
f = ET.Element('{http://www.w3.org/2000/svg}filter')
f.set('{http://www.inkscape.org/namespaces/inkscape}label',
'Shadow')
f.set('style', 'color-interpolation-filters:sRGB;')
f.set('id', filter_id)
f = ET.Element('{http://www.w3.org/2000/svg}filter')
f.set('{http://www.inkscape.org/namespaces/inkscape}label',
'Drop Shadow')
f.set('style', 'color-interpolation-filters:sRGB;')
f.set('id', 'filter14930')
c = ET.Element('{http://www.w3.org/2000/svg}feFlood')
c.set('result', 'flood')
c.set('id', 'feFlood14920')
c.set('flood-opacity', '0.498039')
c.set('flood-color', 'rgb(0,0,0)')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'out')
c.set('id', 'feComposite14922')
c.set('result', 'composite1')
c.set('in2', 'SourceGraphic')
c.set('in', 'flood')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feGaussianBlur')
c.set('id', 'feGaussianBlur14924')
c.set('stdDeviation', '0.3')
c.set('result', 'blur')
c.set('in', 'composite1')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feOffset')
c.set('id', 'feOffset14926')
c.set('result', 'offset')
c.set('dx', '0.3')
c.set('dy', '-0.3')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'atop')
c.set('id', 'feComposite14928')
c.set('result', 'fbSourceGraphic')
c.set('in2', 'SourceGraphic')
c.set('in', 'offset')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feColorMatrix')
c.set('id', 'feColorMatrix14932')
c.set('values', '0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0')
c.set('result', 'fbSourceGraphicAlpha')
c.set('in', 'fbSourceGraphic')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feFlood')
c.set('result', 'flood')
c.set('flood-color', 'rgb(0,0,0)')
c.set('in', 'fbSourceGraphic')
c.set('id', 'feFlood14934')
c.set('flood-opacity', '0.498039')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'in')
c.set('result', 'composite1')
c.set('id', 'feComposite14936')
c.set('in2', 'fbSourceGraphic')
c.set('in', 'flood')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feGaussianBlur')
c.set('result', 'blur')
c.set('stdDeviation', '0.4')
c.set('id', 'feGaussianBlur14938')
c.set('in', 'composite1')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feOffset')
c.set('result', 'offset')
c.set('id', 'feOffset14940')
c.set('dx', '-0.4')
c.set('dy', '0.4')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'over')
c.set('result', 'composite2')
c.set('id', 'feComposite14942')
c.set('in2', 'offset')
c.set('in', 'fbSourceGraphic')
f.append(c)
return f
def shadow2(self, filter_id):
f = ET.Element('{http://www.w3.org/2000/svg}filter')
f.set('{http://www.inkscape.org/namespaces/inkscape}label',
'Shadow')
f.set('style', 'color-interpolation-filters:sRGB;')
f.set('id', filter_id)
c = ET.Element('{http://www.w3.org/2000/svg}feFlood')
c.set('result', 'flood')
c.set('id', 'feFlood14920')
c.set('flood-opacity', '0.7')
c.set('flood-color', 'rgb(128,128,128)')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'in')
c.set('id', 'feComposite14922')
c.set('result', 'composite1')
c.set('in2', 'SourceGraphic')
c.set('in', 'flood')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feGaussianBlur')
c.set('id', 'feGaussianBlur14924')
c.set('stdDeviation', '0.3')
c.set('result', 'blur')
c.set('in', 'composite1')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feOffset')
c.set('id', 'feOffset14926')
c.set('result', 'offset')
c.set('dx', '0.3')
c.set('dy', '-0.3')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'over')
c.set('id', 'feComposite14928')
c.set('result', 'fbSourceGraphic')
c.set('in2', 'SourceGraphic')
c.set('in', 'offset')
f.append(c)
return f
def halo1(self, filter_id, scale):
f = ET.Element('{http://www.w3.org/2000/svg}filter')
f.set('{http://www.inkscape.org/namespaces/inkscape}label',
'Shadow')
f.set('style', 'color-interpolation-filters:sRGB;')
f.set('id', filter_id)
c = ET.Element('{http://www.w3.org/2000/svg}feMorphology')
c.set('result', 'dilate1')
c.set('id', 'feMorphology44406')
c.set('radius', '%f' % (0.25 / scale))
c.set('operator', 'dilate')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feFlood')
c.set('result', 'flood')
c.set('id', 'feFlood14920')
c.set('flood-opacity', '0.6')
c.set('flood-color', 'rgb(135,128,128)')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'in')
c.set('id', 'feComposite14922')
c.set('result', 'composite1')
c.set('in2', 'dilate1')
c.set('in', 'flood')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feGaussianBlur')
c.set('id', 'feGaussianBlur14924')
c.set('stdDeviation', '%f' % (0.2 / scale))
c.set('result', 'blur')
c.set('in', 'composite1')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feFlood')
c.set('result', 'flood2')
c.set('id', 'feFlood14921')
c.set('flood-opacity', '1.')
c.set('flood-color', 'rgb(0,0,0)')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'in')
c.set('id', 'feComposite14929')
c.set('result', 'composite2')
c.set('in', 'flood2')
c.set('in2', 'SourceGraphic')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'over')
c.set('id', 'feComposite14930')
c.set('result', 'composite3')
c.set('in2', 'blur')
c.set('in', 'composite2')
f.append(c)
c = ET.Element('{http://www.w3.org/2000/svg}feComposite')
c.set('operator', 'over')
c.set('id', 'feComposite14928')
c.set('result', 'fbSourceGraphic')
c.set('in', 'SourceGraphic')
c.set('in2', 'compisite3')
f.append(c)
return f
def make_shadow_filter(self, xml, scale=1.):
if not hasattr(self, 'shadow_filters'):
self.shadow_filters = {}
scalei = round(scale * 100)
f = self.shadow_filters.get(scalei)
if f:
return f
f = self.halo1('filter14930_%d' % scalei, scale)
defs = [l for l in xml.getroot()
if l.tag == '{http://www.w3.org/2000/svg}defs'][0]
defs.append(f)
self.shadow_filters[scalei] = f
return f
def add_shadow(self, layer, filter):
child = layer
if True:
#for child in layer:
style = child.get('style')
if style is None:
style = ' '
else:
style += '; '
style += 'filter:url(#%s)' % filter
child.set('style', style)
def add_shadows(self, xml):
if inkscape_version()[:2] == [1, 1]:
# inkscape 1.1 has a problem with filters, it is extemely slow
# and consumes memory. Rendering with inkscape 1.0 is 3 minutes for
# a 360dpi map, and 185 minutes with inkscape 1.1
return
meta = self.get_metadata(xml)
if meta is not None:
lscale = meta.get('shadow_scale')
if lscale is not None:
lscale = float(lscale)
print('LSCALE:', lscale)
else:
lscale = None
print('add_shadows, scale:', lscale)
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
# print('label:', label)
if not (label in self.removed_labels
or label.startswith('profondeurs')
or label.startswith('background_bitm')):
style = self.get_style(layer)
style['display'] = 'inline'
self.set_style(layer, style)
shadow = layer.get('shadow')
# print('shadow:', shadow)
if shadow is not None and shadow not in ('0', 'false', 'False',
'FALSE'):
shadow = True
else:
shadow = False
# print('shadow :', shadow)
if shadow or label in (
'galeries inaccessibles inf',
'anciennes galeries inf',
'galeries inf',
'galeries inf private',
'anciennes galeries big',
'galeries inaccessibles', 'galeries big PARIS',
'galeries',
'galeries private',
'galeries big 2',
'galeries big sud',
'galeries techniques',
'galeries inf private'):
trans = self.get_transform(layer.get('transform'))
scale = (trans[0, 0] + trans[1, 1]) / 2.
if lscale:
scale /= lscale
shadow = self.make_shadow_filter(xml, scale).get('id')
self.add_shadow(layer, shadow)
def remove_shadows(self, xml):
defs = [l for l in xml.getroot()
if l.tag == '{http://www.w3.org/2000/svg}defs'][0]
if defs[-1].tag == '{http://www.w3.org/2000/svg}filter':
del defs[-1]
@staticmethod
def roman(number):
onum = ''
r = number % 1000
c = int(r / 100)
syms = (('M', '?'), ('C', 'D'), ('X', 'L'), ('I', 'V'))
div = 1000
r = number
for i in range(len(syms)):
c = int(r / div)
if c == 9:
onum += syms[i][0] + syms[i - 1][0]
elif c >= 5:
onum += syms[i][1] + syms[i][0] * (c - 5)
elif c == 4:
onum += syms[i][0] + syms[i][1]
else:
onum += syms[i][0] * c
r = r % div
div = int(div / 10)
return onum
@staticmethod
def formatted_date(date):
months = (u'', u'Janvier', u'Février', u'Mars', u'Avril', u'Mai',
u'Juin', u'Juillet', u'Août', u'Septembre', u'Octobre',
u'Novembre', u'Décembre')
return '%d %s %s' % (date.day, months[date.month],
CataMapTo2DMap.roman(date.year))
@staticmethod
def in_region(pt, region, bbox, verbose=False):
x = pt[0]
y = pt[1]
# bbox is used first to quickly discard points
if bbox is not None and (x < bbox[0][0] or x > bbox[1][0]
or y < bbox[0][1] or y > bbox[1][1]):
return False
if region is None:
return True
# then check clip region polygon more thoroughfully
if verbose:
print('in_region check polygon:', pt, bbox)
lines = region.polygon()
vert = region.vertex()
left_pts = 0
for l in lines:
# intersects with horizontal line on pt
intersect = ((vert[l[0]][1] - y) * (vert[l[1]][1] - y) <= 0)
if not intersect:
continue
# intersect abscissa
v = vert[l[1]] - vert[l[0]]
h = (y - vert[l[0]][1]) / v[1]
xi = vert[l[0]][0] + h * v[0]
if xi == x:
# just on border: in
if verbose:
print('__in__')
return True
elif xi < x:
left_pts += 1
# odd nb of intersections on the left (and right): in
# even: out
if verbose:
print('__', (left_pts & 1 == 1), '__', left_pts)
return (left_pts & 1 == 1)
[docs] @staticmethod
def box_in_region(box, region, bbox, verbose=False):
''' check if a box is totally inside a region, or totally outside, or
intersecting
Warning: if 4 corners are inside, the test says inside, whereas it is not always true for a non-convex clip polygon
Returns
-------
1: inside
0: intersecting
-1: outside
'''
pts = [(box[0][0], box[0][1]),
(box[0][0], box[1][1]),
(box[1][0], box[0][1]),
(box[1][0], box[1][1])]
nin = sum([int(CataMapTo2DMap.in_region(pt, region, bbox))
for pt in pts])
if verbose:
print('box_in_region:', box, bbox, nin)
if nin == 0:
return -1
elif nin == 4:
return 1
else:
return 0
def clip_and_scale(self, layer, target_layer, trans, region, region_bbox,
src_trans=None, with_copy=False, verbose=False):
# verbose = verbose or ((layer.get('id') == 'layer8') and target_layer.get('id') == 'layer81')
if verbose:
print('clip_and_scale:', layer.tag, layer.get('id'), 'into:',
target_layer.get('id'))
if not with_copy:
target_layer = layer
trans2 = layer.get('transform')
if trans2 is not None:
trans2 = self.get_transform(trans2)
if src_trans is None:
src_trans = trans2
else:
src_trans = src_trans * trans2
to_remove = []
# to_add = []
copied = []
for index, element in enumerate(layer):
if with_copy:
element = copy.deepcopy(element)
target_layer.append(element)
copied.append(element)
bbox = self.boundingbox(element, src_trans)
# print('bbox:', bbox)
if bbox != [None, None]:
in_out = self.box_in_region(bbox, region, region_bbox,
verbose=verbose)
if in_out <= 0:
if verbose:
print('out:', element.tag, element.get('id'))
#to_remove.append(element)
# elif in_out == 0:
# intesect
if verbose:
print('intersect:', element.tag, element.get('id'))
if element.tag.endswith('}g'):
# group: look inside
self.clip_and_scale(element, element, trans, region,
region_bbox, src_trans,
verbose=verbose)
else:
# real object intersect
remove = True
if element.tag.endswith('}path'):
trans2 = element.get('transform')
if trans2 is not None:
trans2 = self.get_transform(trans2)
if src_trans is not None:
trans2 = src_trans * trans2
else:
if src_trans is None:
trans2 = np.matrix(np.eye(3))
else:
trans2 = src_trans
intersect = self.clip_path(element, trans2,
region)
if verbose:
print('clip:', intersect,
len(intersect.get('d')))
if intersect is not None \
and len(intersect.get('d')) != 0:
if verbose:
print('keep intersection')
# to_add.append((index, intersect))
element.set('d', intersect.get('d'))
remove = False
if remove:
# reject
to_remove.append(element)
elif verbose:
print('** in **:', element.tag, element.get('id'))
else:
# text ? or other object with x, y attributes
x = element.get('x')
y = element.get('y')
if x is None or y is None:
# no: something else: skip
to_remove.append(element)
continue
x = float(x)
y = float(y)
trans2 = element.get('transform')
if trans2 is None:
trans2 = src_trans
else:
trans2 = self.get_transform(trans2)
if src_trans is not None:
trans2 = src_trans * trans2
if trans2 is not None:
p = trans2 * np.expand_dims([x, y, 1.], 1)
x, y = p[0, 0], p[1, 0]
# print('tag:', element.tag, x, y)
if not self.in_region((x, y), region, region_bbox,
verbose=verbose):
# print('remove from:', target_layer, target_layer.get('id'))
to_remove.append(element)
elif verbose:
print('** in **:', element.tag, element.get('id'))
# for index, element in to_add:
# target_layer.insert(index, element)
for element in to_remove:
target_layer.remove(element)
if trans is not None:
init_tr = layer.get('transform')
if init_tr is not None:
init_tr = self.get_transform(init_tr)
trans = trans * init_tr
for element in copied:
trans2 = element.get('transform')
if trans2 is not None:
trans2 = trans * self.get_transform(trans2)
else:
trans2 = trans
element.set('transform', self.to_transform(trans2))
def enlarge_region(self, src_xml, xml, region, keep_private=True):
mask_layer = [
x for x in src_xml.getroot()
if x.get('zoom_area_id') == region]
if mask_layer:
mask_layer = mask_layer[0]
else:
return # this region doesn't exist in the map
target_layer = [
x for x in xml.getroot()
if x.get('zoom_id') == region][0]
target_trans = self.get_transform(target_layer.get('transform'))
target_rect = target_layer[0]
rect = self.boundingbox(target_rect, target_trans)
# print('target rect:', rect)
# calculate transform
trans = self.get_transform(mask_layer.get('transform'))
in_rect = self.boundingbox(mask_layer[0], trans)
# print('in_rect:', in_rect)
enl_tr = np.matrix(np.eye(3))
scl1 = (rect[1][0] - rect[0][0]) / (in_rect[1][0] - in_rect[0][0])
scl2 = (rect[1][1] - rect[0][1]) / (in_rect[1][1] - in_rect[0][1])
# print('scales:', scl1, scl2)
scl = min((scl1, scl2))
enl_tr[0, 0] = scl
enl_tr[1, 1] = scl
enl_tr[:2, 2] \
= (np.expand_dims([rect[0][0], rect[1][1], 1.], 1)
- enl_tr * np.expand_dims([in_rect[0][0],
in_rect[1][1], 1.], 1))[:2]
# get back into target layer coords
enl_tr = np.linalg.inv(target_trans) * enl_tr
# print('enlarge_region:', region)
# print('rect:', rect)
# print('in_rect:', in_rect)
# print('enl_tr:', enl_tr)
# print('target_trans:', target_trans)
# replace rect by actual data
target_layer.remove(target_rect)
clip_region = self.read_path(mask_layer[0], trans)
for layer in xml.getroot():
if not layer.tag.endswith('}g'):
continue
style = layer.get('style')
if style is not None and 'display:none' in style:
continue
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
hidden = ItemProperties.is_true(layer.get('zoom_hidden')) \
or ItemProperties.is_true(layer.get('zoom_id')) \
or ItemProperties.is_true(layer.get('zoom_area_id'))
if hidden or label is None:
continue
self.clip_and_scale(layer, target_layer, enl_tr, clip_region,
in_rect, None, with_copy=True)
# print('in_rect:', in_rect)
# print(np.asarray(clip_region.vertex()))
[docs] def replace_filter_element(self, xml):
# if not self.keep_private and xml.get('level') in ('tech', ):
if not self.keep_private:
if xml.get('level') in ('tech', ):
return None
return xml
def do_remove_layers(self, xml):
to_remove = []
labels = []
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label in self.removed_labels \
or layer.get('hidden') in ('true', '1', 1, 'True'):
to_remove.append(layer)
labels.append(label)
print('removing layers:', labels)
for layer in to_remove:
xml.getroot().remove(layer)
def remove_selected_layers(self, xml):
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
non_visibility = layer.get('non_visibility')
if non_visibility:
if non_visibility.strip().startswith('['):
non_visibility = json.loads(non_visibility)
else:
non_visibility = [non_visibility]
if self.map_name in non_visibility:
self.removed_labels.add(label)
continue
visibility = layer.get('visibility')
if visibility is None:
continue
elif visibility.strip().startswith('['):
visibility = json.loads(visibility)
else:
if visibility == 'private': # old style
# exception, here 'private' is not a map type name but a
# variant (used in private, igc_private maps etc). It is
# filtered by remove_private(), and it is better to specify
# the tag private: true.
continue
visibility = [visibility]
if self.map_name not in visibility:
self.removed_labels.add(label)
def remove_wip(self, xml):
self.removed_labels.update(
('a_verifier', 'indications_big_2010', 'planches',
'calcaire 2010',
'work done calc', 'galeries techniques despe',
u'à vérifier', ))
self.do_remove_layers(xml)
def remove_south(self, xml):
self.removed_labels.update(
('galeries_big_sud',))
self.do_remove_layers(xml)
def remove_background(self, xml):
self.removed_labels.update(('couleur_fond', 'couleur_fond sud',))
self.do_remove_layers(xml)
def remove_private(self, xml):
priv_labels = ['inscriptions', 'inscriptions conso',
'inscriptions inaccessibles',
u'inscriptions flèches',
u'inscriptions flèches inaccessibles',
u'inscriptions conso flèches',
u'maçonneries private', 'private', 'calcaire 2010',
'work done calc']
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if ItemProperties.is_true(layer.get('private')) \
or layer.get('visibility') == 'private' \
or label in priv_labels \
or label.endswith(' private') \
or 'tech' in label:
self.removed_labels.add(label)
self.keep_private = False
self.remove_wip(xml)
for layer in xml.getroot():
print('filter private in', layer.get('{http://www.inkscape.org/namespaces/inkscape}label'))
todo = [(layer, element) for element in layer]
while todo:
parent, element = todo.pop(0)
if element.get('visibility') == 'private' \
or ItemProperties.is_true(element.get('private')):
print('remove:', element)
parent.remove(element)
else:
todo += [(element, item) for item in element]
def remove_gtech(self, xml):
self.removed_labels.update(('ebauches', 'galeries techniques',
'PS gtech', 'PSh gtech', 'PSh vers gtech',
'symboles gtech', 'eau gtech',
'raccords gtech 2D',
'metro',
u'curiosités flèches GTech',
'curiosités GTech',
u'échelle gtech',
u'plaques de puits GTech',
u'échelle vers gtech',
u'plaques de puits GTech',
'annotations metro',
))
self.do_remove_layers(xml)
def remove_calcaire(self, xml):
self.removed_labels.update(('calcaire 2010', 'calcaire ciel ouvert',
'calcaire masse2', 'calcaire masse', 'calcaire med',
'calcaire sup', 'calcaire inf', 'calcaire vdg',
))
self.do_remove_layers(xml)
def remove_igc(self, xml):
self.removed_labels.update(('planches', 'planches fond',
'planches IGC',
))
self.do_remove_layers(xml)
def remove_non_aqueduc(self, xml):
self.removed_labels.update(('légende', 'grandes plaques',
))
self.do_remove_layers(xml)
def remove_non_printable1(self, xml):
self.removed_labels.update(
['masque bg', 'masques v1', u'd\xe9coupage',
'chatieres old', 'photos',
# 'bord_sud', 'galeries big sud',
u'légende_alt', 'sons', 'altitude', 'lambert93',
'bord'])
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label.startswith('profondeurs') \
or label.startswith('background_bitm'):
self.removed_labels.add(label)
self.do_remove_layers(xml)
def remove_non_printable1_main(self, xml):
self.removed_labels.update(
['masque bg', 'masques v1', u'd\xe9coupage',
'chatieres old', 'photos',
'bord_sud', 'galeries big sud',
u'légende_alt', 'sons', 'altitude', 'lambert93',])
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label.startswith('profondeurs') \
or label.startswith('background_bitm'):
self.removed_labels.add(label)
self.do_remove_layers(xml)
def remove_non_printable1_pub(self, xml):
self.removed_labels.update(
['masque bg', 'masques v1', u'd\xe9coupage',
'chatieres old', 'photos',
'bord_sud', 'bord', # 'galeries big sud',
u'légende_alt', 'sons', 'altitude', 'lambert93', ])
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label.startswith('profondeurs') \
or label.startswith('background_bitm'):
self.removed_labels.add(label)
self.do_remove_layers(xml)
def remove_non_printable2(self, xml):
self.removed_labels.update(
['galeries agrandissements',
'masque vdg', u'masque cimetière',
'masque plage'])
self.do_remove_layers(xml)
def remove_non_printable_igc_private(self, xml):
self.removed_labels.update(
['masque bg', 'masques v1', u'd\xe9coupage',
'chatieres old', 'photos',
'bord',
# 'bord_sud', 'galeries big sud',
u'légende_alt', 'sons', 'altitude', 'lambert93'])
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label.startswith('profondeurs') \
or label.startswith('background_bitm'):
self.removed_labels.add(label)
self.do_remove_layers(xml)
def remove_limestone(self, xml):
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label.startswith('calcaire') or 'masses' in label:
self.removed_labels.add(label)
self.do_remove_layers(xml)
def remove_zooms(self, xml):
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label is None:
continue
if label.startswith('agrandissement'):
self.removed_labels.add(label)
self.do_remove_layers(xml)
def resize_poster(self, xml, page_width=1050):
target_width = page_width / .254 # mm -> inch
if self.clip_rect:
rect = self.clip_rect
else:
rect = xml.getroot() # main document
width = float(rect.get('width'))
height = float(rect.get('height'))
if height < width:
# adapt for landscape orientation
target_height = target_width
scale = target_height / height
else:
scale = target_width / width
root = xml.getroot()
root.set('width', str(width * scale))
root.set('height', str(height * scale))
root.set('transform', 'scale(%f)' % scale)
def remove_other(self, xml, *labels):
self.removed_labels.update(labels)
self.do_remove_layers(xml)
def replace_symbols(self, xml):
protos = self.find_protos(self.xml)
self.replace_elements(xml, protos)
def zoom_areas(self, xml):
zoom_regions = [x.get('zoom_area_id') for x in xml.getroot()
if x.get('zoom_area_id') is not None]
print('zoom regions:', zoom_regions)
for region in zoom_regions:
self.enlarge_region(self.xml, xml, region,
keep_private=self.keep_private)
def set_date(self, xml):
for layer in xml.getroot():
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if (label and label.startswith(u'légende')) \
or layer.get('legend') in ('1', 'true', 'True', 'TRUE'):
for child in layer:
if child.get('date') is not None:
# set date in appropriate field
d = datetime.date.today()
child[0].text = u'\xe9dition du %s' \
% self.formatted_date(d)
def show_all(self, xml):
for layer in xml.getroot():
layer.set('style', 'display:inline')
def shadow_text(self, xml):
xml2 = copy.deepcopy(xml)
todel = []
shift = [-0.075, 0.075]
meta = [layer for layer in xml.getroot()
if layer.tag.endswith('}metadata')]
if len(meta) != 0:
meta = meta[0]
shift_txt = meta.get('text_shadow_shift')
if shift_txt:
shift = json.loads(shift_txt)
for layer in xml2.getroot():
trans = None
trans = self.get_transform(layer, trans)
# shift (-0.075, 0.075)
trans[0, 2] += shift[0]
trans[1, 2] += shift[1]
self.set_transform(layer, trans)
todo = [(xml2.getroot(), layer) for layer in xml2.getroot()[:]
if layer.tag == '{http://www.w3.org/2000/svg}g']
while todo:
parent, elem = todo.pop(0)
tag = elem.tag
#if tag not in ('{http://www.w3.org/2000/svg}text',
#'{http://www.w3.org/2000/svg}flowRoot',
#'{http://www.w3.org/2000/svg}g'):
#parent.remove(elem)
#continue
todo += [(elem, sub_elem) for sub_elem in elem]
if tag == '{http://www.w3.org/2000/svg}g':
if len(elem) == 0:
parent.remove(elem)
continue
# else text
style = self.get_style(elem)
if style is None:
style = {}
if style.get('fill') not in (None, 'none') or tag in (
'{http://www.w3.org/2000/svg}text',
'{http://www.w3.org/2000/svg}flowRoot',
'{http://www.w3.org/2000/svg}tspan',
'{http://www.w3.org/2000/svg}flowRegion'):
style['fill'] = '#ffffff'
style['fill-opacity'] = 0.5
if style.get('stroke') not in (None, 'none'):
style['stroke'] = '#ffffff'
style['stroke-opacity'] = 0.5
self.set_style(elem, style)
xml2.write('/tmp/shadow_text.svg') # FIXME debug
# insert shadowed layers
insert_pos = 0
for layer in xml.getroot():
if layer.tag == '{http://www.w3.org/2000/svg}g':
break
insert_pos += 1
for layer in xml2.getroot():
if layer.tag == '{http://www.w3.org/2000/svg}g':
xml.getroot().insert(insert_pos, layer)
insert_pos += 1
def get_alt_color(self, props, colorset, conv=True):
col = None
while col is None and colorset:
col = CataSvgToMesh.get_alt_color_s(props, colorset, conv)
if col is None:
colorset = self.colorset_inheritance.get(colorset)
return col
def recolor(self, xml, colorset='igc'):
colorsets = {}
colors = colorsets.setdefault(colorset, {})
legend_layer = None
# get colorset_inheritance dict in metadata
if not hasattr(self, 'colorset_inheritance'):
self.colorset_inheritance = {}
meta = [layer for layer in xml.getroot()
if layer.tag.endswith('}metadata')]
if len(meta) != 0:
meta = meta[0]
colorset_inheritance = meta.get('colorset_inheritance')
if colorset_inheritance:
colorset_inheritance = json.loads(colorset_inheritance)
self.colorset_inheritance = colorset_inheritance
for layer in xml.getroot():
props = ItemProperties()
props.fill_properties(layer)
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
print('recolor', label)
todo = [(x, [props]) for x in layer]
while todo:
item, parents = todo.pop()
props = ItemProperties()
props.fill_properties(item, parents)
if len(item) != 0:
todo += [(x, parents + [props]) for x in item]
corridor_colors = self.get_alt_color(
props, colorset, conv=False)
if not corridor_colors:
corridor_colors = colors.get(label)
if not corridor_colors:
# print('skip recolor', label)
continue
if label == u'légende':
legend_layer = layer
# allow legend to match labels found in map
# however all items with this label will be affected...
#if label not in colors:
#colors[label] = corridor_colors
if isinstance(corridor_colors, str):
corridor_colors = {'bg': corridor_colors}
bg = corridor_colors.get('bg')
op = 1.
fill_op = 1.
if bg and len(bg) >= 7:
fill_op = float(eval('0x%s' % bg[-2:])) / 255.
bg = bg[:-2]
fg = corridor_colors.get('fg')
if fg and len(fg) >= 7:
op = float(eval('0x%s' % fg[-2:])) / 255.
fg = fg[:-2]
style = self.get_style(item)
if style:
if bg and 'fill' in style:
style['fill'] = bg
style['fill-opacity'] = str(fill_op)
if fg and 'stroke' in style:
style['stroke'] = fg
style['stroke-opacity'] = str(op)
self.set_style(item, style)
# allow to replace other style elements
for k, style_item in corridor_colors.items():
if k not in ('fg', 'bg'):
style[k] = style_item
# recolor legend items
if legend_layer:
todo = legend_layer[:]
while todo:
item = todo.pop(0)
if len(item) != 0:
todo += item[:]
label = item.get('label')
if not label:
continue
corridor_colors = colors.get(label)
if not corridor_colors:
continue
style = self.get_style(item)
if style:
bg = corridor_colors.get('bg')
fg = corridor_colors.get('fg')
op = 1.
fill_op = 1.
if bg and len(bg) >= 7:
fill_op = float(eval('0x%s' % bg[-2:])) / 255.
bg = bg[:-2]
if fg and len(fg) >= 7:
op = float(eval('0x%s' % fg[-2:])) / 255.
fg = fg[:-2]
if bg:
style['fill'] = bg
style['fill-opacity'] = str(fill_op)
if fg:
style['stroke'] = fg
style['stroke-opacity'] = str(op)
self.set_style(item, style)
def list_colorsets(self, xml):
colorsets = set()
todo = [xml.getroot()]
while todo:
item = todo.pop()
alt_col = item.get('alt_colors')
if alt_col:
try:
alt_col = json.loads(alt_col)
except Exception:
raise
colorsets.update(alt_col.keys())
label_alt_col = item.get('label_alt_colors')
if label_alt_col:
try:
label_alt_col = json.loads(label_alt_col)
except Exception:
raise
for c in label_alt_col.values():
colorsets.update(c.keys())
todo += item[:]
print('available colorsets:')
for col in sorted(colorsets):
print(' %s' % col)
print()
return colorsets
def list_maps(self, xml_et, maps_def, details=False):
maps = set(maps_def.keys())
print('available maps:')
for map_t in sorted(maps):
print(' %s' % map_t)
if details:
print(' ', end='')
pprint.pprint(maps_def[map_t], indent=8)
print()
return maps
def read_maps_def(self, xml_et, colorset, do_recolor):
col_filter = []
if do_recolor:
col_filter = ['recolor="%s"' % colorset]
maps_def = {
'private': {
'name': 'private',
'filters': col_filter + ['remove_wip', 'printable_map'],
'shadows': True,
'do_pdf': True,
},
'poster': {
'name': 'poster',
'filters': col_filter + ['remove_private', 'poster_map'],
'shadows': True,
'do_pdf': True,
},
'poster_private': {
'name': 'poster_private',
'filters': col_filter + ['remove_wip', 'poster_map'],
'shadows': True,
'do_pdf': True,
},
'wip': {
'name': 'private_wip',
'filters': col_filter + ['printable_map'],
'shadows': True,
'do_pdf': False,
},
'public': {
'name': 'public',
'filters': col_filter + ['remove_private', 'remove_gtech',
'printable_map_public'],
'shadows': True,
'do_pdf': True,
},
'igc': {
'name': 'igc',
'filters': ['igc'],
'shadows': True,
'do_pdf': False,
},
'igc_private': {
'name': 'igc_private',
'filters': ['igc_private'],
'shadows': True,
'do_pdf': False,
},
'igcportail': {
'name': 'igcportail',
'filters': ['igc_private'],
'shadows': False,
'do_pdf': False,
'do_jpg': False,
},
'igcportail_txt': {
'name': 'igcportail_txt',
'filters': ['igc_private', 'shadow_text'],
'shadows': False,
'do_pdf': False,
'do_jpg': False,
},
'igcportail_legend': {
'name': 'igcportail_legend',
'filters': ['igc_private', 'shadow_text'],
'shadows': False,
'do_pdf': False,
'do_jpg': False,
},
'igcportail_tech': {
'name': 'igcportail_tech',
'filters': ['igc_private'],
'shadows': False,
'do_pdf': False,
'do_jpg': False,
},
'aqueduc': {
'name': 'aqueduc',
'filters': col_filter + ['aqueduc'],
'shadows': True,
'do_pdf': True,
},
'No_GTech': {
'name': 'No_GTech',
'filters': col_filter + ['remove_wip',
'printable_map', 'remove_gtech'],
'shadows': True,
'do_pdf': True,
},
}
meta = self.get_metadata(xml_et)
user_map_defs = meta.get('maps_def')
if user_map_defs is not None:
replacements = {'col_filter': col_filter,
'colorset': colorset}
user_map_defs % replacements
user_map_defs = json.loads(user_map_defs)
maps_def.update(user_map_defs)
return maps_def
def list_filters(self):
all_filters = self.get_filters()
print('available filters:')
pprint.pprint(all_filters)
def split_layers(self, xml, style='default'):
layer_setups = {
'default': [
['a_verifier',
'bord_sud',
'indications_big_2010',
'salles vdg',
'salles vdg private',
'salles v1',
'salles inf',
'private',
'zones',
u'curiosités vdg private',
u'curiosités vdg',
u'curiosités private',
u'curiosités',
u'curiosités inf',
'historiques',
'rues vdg',
'rues email',
'rues v1 private',
'rues v1',
'rues inf petit',
'rues inf',
'rues sans plaques',
'annotations vdg',
'annotations',
'plaques de puits',
u'curiosités flèches dessus',
u'rues flèches dessus',
u'salles vdg flèches',
u'salles v1 flèches',
u'salles flèches private',
u'curiosités flèches private',
u'curiosités flèches',
u'historiques flèches',
u'rues v1 flèches private',
u'rues v1 flèches',
],
['inscriptions vdg',
'inscriptions conso',
'inscriptions',
u'plaques rues volées',
'symboles gtech',
'PSh gtech',
'PSh vers gtech',
u'échelle vers gtech',
u'échelle vers gtech private',
'eau gtech',
'ebauches',
'raccords gtech 2D',
'galeries techniques',
'galeries techniques despe',
'metro',
u'inscriptions flèches',
u'inscriptions conso flèches',
],
['symboles private',
'symboles',
'plaques rues',
'raccords plan 2D',
'sol-ciel',
'PSh',
'PS',
'PE',
'P ossements',
'escaliers',
'escaliers private',
'escaliers inaccessibles',
'escaliers anciennes galeries big',
'PS sans',
'PSh sans',
u'échelle',
'eau',
u'maçonneries',
u'maçonneries private',
'cuves',
'galeries private',
'galeries',
'galeries big sud',
'galeries inaccessibles',
'remblai epais',
'remblai leger',
'remblai epais inaccessibles',
'remblai leger inaccessibles',
'remblai epais inaccessibles inf',
'remblai leger inaccessibles inf',
'hagues',
'hagues effondrees',
'anciennes galeries big',
'cuves inf',
'symboles inf',
'galeries inf',
'anciennes galeries inf',
'galeries inaccessibles inf',
'chatieres private',
'chatieres v3',
],
[
u'légende_alt',
u'légende',
u'découpage',
'profondeurs gtech',
'profondeurs galeries',
'profondeurs seine',
'profondeurs esc',
'profondeurs galeries_inf',
'profondeurs metro',
'masque vdg',
u'masque cimetière',
'masque plage',
'agrandissement vdg',
'agrandissement cimetière',
'agrandissement plage',
'agrandissement fond',
'calcaire 2010',
'calcaire limites',
'calcaire masse',
'calcaire masse2',
'calcaire ciel ouvert',
'calcaire sup',
'calcaire med',
'calcaire inf',
'calcaire vdg',
'parcelles',
'altitude',
'couleur_fond1',
'couleur_fond',
'couleur_fond sud',
'planches',
'planches fond',
'planches IGC',
],
]
}
common = set(['bord'])
layers = layer_setups[style]
maps = []
root = xml.getroot()
for i in range(len(layers)):
mr = ET.Element(root.tag)
m = ET.ElementTree(mr)
for k, v in root.items():
mr.set(k, v)
maps.append(m)
layer_num = 0
for layer in root:
to_all = False
if layer.tag != '{http://www.w3.org/2000/svg}g':
# common to all
to_all = True
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
layer.set('layer_num', str(layer_num))
layer_num += 1
if label in common:
to_all = True
if to_all:
for m in maps:
m.getroot().append(copy.deepcopy(layer))
continue
for i, lnames in enumerate(layers):
if label in lnames:
maps[i].getroot().append(copy.deepcopy(layer))
break
else:
maps[-1].getroot().append(copy.deepcopy(layer))
return maps
def join_layers(self, xml_dummy, filename):
pattern = filename.replace('.svg', '_%d.svg')
## clear xml
#to_remove = []
#labels = []
#for layer in xml.getroot():
#if layer.tag == '{http://www.w3.org/2000/svg}g':
#to_remove.append(layer)
#for layer in to_remove:
#xml.getroot().remove(layer)
i = 0
while os.path.exists(pattern % i):
filename = pattern % i
print('adding', filename)
mapi = self.read_xml(filename)
i += 1
if i == 1:
xml = mapi
self.xml = xml
continue
for layer in mapi.getroot():
if layer.tag != '{http://www.w3.org/2000/svg}g':
continue
layer_num = int(layer.get('layer_num'))
# look where to insert it
for j, xlayer in enumerate(xml.getroot()):
if xlayer.tag != '{http://www.w3.org/2000/svg}g':
continue
xlayer_num = int(xlayer.get('layer_num'))
if xlayer_num == layer_num:
# already here, do nothing
j = -1
break
elif xlayer_num > layer_num:
break
if j >= 0:
xml.getroot().insert(j, layer)
# now remove layer_num
for layer in xml.getroot():
if layer.get('layer_num'):
del layer.attrib['layer_num']
def layer_opacity(self, xml, label, opacity):
print('set layer opacity:', label, opacity)
for layer in xml.getroot():
if layer.tag != '{http://www.w3.org/2000/svg}g':
continue
if layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label') \
== label:
print('found it.')
style = self.get_style(layer)
style['opacity'] = opacity
self.set_style(layer, style)
print('style:', layer.get('style'))
break
def map_layers_opacity(self, xml):
print('map layers opacity')
for layer in xml.getroot():
if layer.tag != '{http://www.w3.org/2000/svg}g':
continue
lay_op = layer.get('map_opacity')
if lay_op:
lay_op = json.loads(lay_op.strip())
if self.map_name in lay_op:
opacity = lay_op[self.map_name]
style = self.get_style(layer)
if style is None:
style = {}
style['opacity'] = opacity
self.set_style(layer, style)
def find_clip_rect(self, xml, rect_def):
for layer in xml.getroot():
if layer.tag != '{http://www.w3.org/2000/svg}g':
continue
label = layer.get(
'{http://www.inkscape.org/namespaces/inkscape}label')
if label == rect_def:
return layer[0]
for child in layer:
if child.get('id') == rect_def \
or child.get('label') == rect_def:
return child
[docs] def ensure_clip_rect(self, out_xml, rect_id, in_xml):
''' if rect_id is not in out_xml, then take it from in_xml and copy it
in a new layer un out_xml.
'''
if self.find_clip_rect(out_xml, rect_id):
return # OK
elem = self.find_element(in_xml, rect_id)
if not elem:
raise ValueError('element not found: %s' % rect_id)
rect, trans = elem
layer = ET.Element('{http://www.w3.org/2000/svg}g')
out_xml.getroot().insert(0, layer)
layer.set('{http://www.inkscape.org/namespaces/inkscape}label',
'clip_border')
layer.set('{http://www.inkscape.org/namespaces/inkscape}groupmode',
'layer')
layer.set('style', 'display:none')
layer.set('id', 'clip_border')
layer.append(copy.deepcopy(rect))
layer[0].set('id', rect.get('id')) # copy same id
if trans is not None:
self.set_transform(layer, trans)
def get_filters(self):
all_filters = {
'remove_private': self.remove_private,
'remove_non_printable1': self.remove_non_printable1,
'remove_non_printable2': self.remove_non_printable2,
'remove_non_printable1_pub': self.remove_non_printable1_pub,
'remove_non_printable1_main': self.remove_non_printable1_main,
'remove_non_printable_igc_private':
self.remove_non_printable_igc_private,
'remove_non_printable': ['remove_non_printable1',
'remove_non_printable2'],
'remove_wip': self.remove_wip,
'remove_south': self.remove_south,
'remove_background': self.remove_background,
'remove_limestone': self.remove_limestone,
'remove_gtech': self.remove_gtech,
'remove_non_aqueduc': self.remove_non_aqueduc,
'remove_calcaire': self.remove_calcaire,
'remove_igc': self.remove_igc,
'remove_other': self.remove_other,
'add_shadow': self.add_shadows,
'shift_inf_level': self.transform_inf_level,
'replace_symbols': self.replace_symbols,
'recolor': self.recolor,
'zooms': self.zoom_areas,
'date': self.set_date,
'show_all': self.show_all,
'split_layers': self.split_layers,
'join_layers': self.join_layers,
'layer_opacity': self.layer_opacity,
'remove_zooms': self.remove_zooms,
'resize_poster': self.resize_poster,
'shadow_text': self.shadow_text,
'map_layers_opacity': self.map_layers_opacity,
'printable_map': ['remove_non_printable1', 'show_all',
'shift_inf_level', 'replace_symbols', 'date',
'zooms', 'remove_non_printable2', 'remove_igc',
'layer_opacity=["XIII masses", "0.31"]',
'map_layers_opacity'],
'poster_map': ['remove_non_printable1_main', 'show_all',
'shift_inf_level', 'replace_symbols', 'date',
'zooms', 'remove_non_printable2', 'remove_igc',
'layer_opacity=["XIII masses", "0.31"]',
'map_layers_opacity'],
'printable_map_public': [
'remove_non_printable1_pub', 'show_all',
'shift_inf_level', 'replace_symbols', 'date',
'zooms', 'remove_non_printable2', 'remove_igc',
'layer_opacity=["XIII masses", "0.31"]',
'map_layers_opacity'],
'igc': ['remove_private', 'remove_non_printable1_pub',
'remove_non_printable2',
'remove_background', 'remove_limestone', 'remove_zooms',
'remove_other=["raccords plan 2D", "parcelles", '
'"raccords gtech 2D"]',
'show_all', 'date',
# 'recolor="%s"' % igc_colorset,
# 'layer_opacity=["planches IGC", "0.44"]',
'map_layers_opacity'],
'igc_private': [
'remove_wip', 'remove_non_printable_igc_private',
'remove_non_printable2',
'remove_background', 'remove_limestone', 'remove_zooms',
'remove_other=["raccords plan 2D", "parcelles", '
'"raccords gtech 2D"]',
'show_all', 'date',
# 'recolor="%s"' % igc_colorset,
# 'layer_opacity=["planches IGC", "0.44"]',
'map_layers_opacity'],
'aqueduc': ['remove_non_printable1', 'show_all',
'shift_inf_level', 'replace_symbols', 'date',
'remove_wip',
'replace_symbols', 'date', 'remove_zooms',
'remove_non_printable2', 'remove_igc',
'remove_non_aqueduc',
'layer_opacity=["XIII masses", "0.31"]',
'map_layers_opacity'],
}
return all_filters
def build_2d_map(self, xml, keep_private=True, wip=False,
filters=[], map_name=None):
# igc_colorset = 'igc'
# if map_name.startswith('igcportail'):
# igc_colorset = 'igcportail'
meta = self.get_metadata(xml)
recolor = [x for x in filters if x.startswith('recolor=')]
if meta is not None and len(recolor) == 0:
colorsets = meta.get('colorsets')
if colorsets:
colorsets = json.loads(colorsets)
def_colorset = colorsets.get(map_name)
# TODO: remove the exception here:
if def_colorset: # and not map_name.startswith('igc'):
filters.insert(0, 'recolor="%s"' % def_colorset)
# if forcing recolor=default, just remove the filter
if 'recolor="default"' in filters:
filters.remove('recolor="default"')
all_filters = self.get_filters()
map_2d = copy.deepcopy(xml)
self.xml = map_2d
self.removed_labels = set()
self.keep_private = True
self.keep_transformed_properties = set(('level', 'map_transform'))
self.map_name = map_name
self.remove_selected_layers(map_2d)
results = []
done = set()
filters = list(filters)
while filters:
filter = filters.pop(0)
value = []
filt_def = []
if isinstance(filter, str):
filt_val = filter.split('=')
if len(filt_val) > 1:
filter = filt_val[0]
value = eval(filt_val[1])
if not isinstance(value, list):
value = [value]
done.add(filter)
filt_def = all_filters[filter]
if not isinstance(filt_def, list):
done.add(filt_def)
print('apply filter:', filter, value)
result = filt_def(map_2d, *value)
results.append(result)
else:
filters = filt_def + filters
self.xml.result = results
print('build_2d_map done.')
return self.xml
_inksape_ubuntu16 = None
def get_inkscape_ub16():
global _inksape_ubuntu16
if _inksape_ubuntu16 is not None:
return _inksape_ubuntu16
inkscape_ub16 = ['inkscape']
if os.path.exists('/etc/lsb-release'):
with open('/etc/lsb-release') as f:
info = f.readlines()
info = dict([x.strip().split('=') for x in info])
release = info.get('DISTRIB_RELEASE')
if not release or release == '16.04':
return inkscape_ub16
pwd = os.getcwd()
if pwd.startswith(os.path.realpath(os.environ.get('HOME'))):
pwd = pwd.replace(os.path.realpath(os.environ.get('HOME')),
os.environ.get('HOME'))
casa_distro = distutils.spawn.find_executable('casa_distro')
if not casa_distro:
return inkscape_ub16
dist = subprocess.check_output(['casa_distro', 'list'])
dist = [x for x in dist.split('\n') if not x.startswith(' ')]
dist = [{x.split('=')[0]:x.split('=')[1] for x in d.split()}
for d in dist]
dist = [d for d in dist if d.get('system') == 'ubuntu-16.04']
for d in dist:
cmd = ['casa_distro', 'run'] \
+ ['%s=%s' % (k, v) for k, v in d.items()]
try:
subprocess.check_call(cmd + ['inkscape', '--help'])
inkscape_ub16 = cmd + ['cwd=%s' % pwd, 'inkscape']
break
except Exception:
pass
return inkscape_ub16
_inkscape_version = {}
def inkscape_version(inkscape_exe='inkscape'):
global _inkscape_version
if not isinstance(inkscape_exe, (tuple, list)):
inkscape_exe = [inkscape_exe]
ver = _inkscape_version.get(tuple(inkscape_exe))
if ver:
return ver
over = subprocess.check_output(inkscape_exe + ['--version']).decode()
print('over:', over)
ver = [int(x) for x in over.strip().split()[1].split('-')[0].split('.')]
_inkscape_version[tuple(inkscape_exe)] = ver
return ver
def export_pdf(in_file, out_file=None):
inkscape_exe = ['inkscape']
iver = inkscape_version()
if iver[0] < 1:
if iver[1] >= 92:
# 0.92 has a but in pdf export
inkscape_exe = get_inkscape_ub16()
iver = inkscape_version(inkscape_exe)
print('pdf export exe:', inkscape_exe, iver)
if iver[0] >= 1:
# 1.x commandline options have competely changed
cmd = inkscape_exe + [
'--export-pdf-version', '1.5', '--export-area-page',
'--export-type', 'pdf']
if out_file:
cmd += '-o', out_file
subprocess.check_call(cmd + [in_file])
else:
if not out_file:
out_file = in_file.replace('.svg', '.pdf')
subprocess.check_call(
inkscape_exe + ['-z',
'--export-pdf-version', '1.5', '--export-area-page',
'--export-pdf', out_file, in_file])
def export_png(in_file, resolution=180, rect_id=None, out_file=None,
ignore_errors=False):
inkscape_exe = ['inkscape']
iver = inkscape_version()
#if iver[0] == 1:
## 1.0 has a bug and crashes during png save
## (both actually for large images)
#inkscape_exe = get_inkscape_ub16()
#iver = inkscape_version(inkscape_exe)
print('png export exe:', inkscape_exe, ', version:', iver)
if not out_file:
out_file = in_file.replace('.svg', '.png')
if ignore_errors:
call = subprocess.call
else:
call = subprocess.check_call
if iver[0] >= 1:
# 1.x commandline options have competely changed
cmd = inkscape_exe + [
'--export-type', 'png', '--export-dpi', str(resolution),
'-o', out_file]
if rect_id:
cmd += ['--export-id', rect_id]
call(cmd + [in_file])
else:
cmd = inkscape_exe + ['-z',
'--export-dpi', str(resolution),
'--export-png', out_file]
if rect_id:
cmd += ['--export-id', rect_id]
call(cmd + [in_file])
def convert_to_format(png_file, format='jpg', remove=True, max_pixels=None):
outfile = png_file.replace('.png', '.%s' % format)
if PIL:
# use Pillow PIL module
if not max_pixels:
max_pixels = 30000 * 30000 # large enough
PIL.Image.MAX_IMAGE_PIXELS = max_pixels
try:
with PIL.Image.open(png_file) as im:
if format == 'jpg':
# convert to RGB with alpha and white background
front = np.array(im) # copy because the image in read only
for i in range(3):
alpha = front[:, :, 3] # uint8
front[:, :, i] = (
(255 - alpha)
+ (front[:, :, i].astype(np.float32)
* alpha / 255).astype(np.uint8))
front[:, :, 3] = 255
im = PIL.Image.fromarray(front, 'RGBA')
im = im.convert(mode='RGB')
save_options = {}
if format == 'tif':
# save_options['compression'] = 'tiff_lzw'
save_options['compression'] = 'jpeg'
else:
save_options['quality'] = 95
try:
im.save(outfile, **save_options)
except Exception:
if format == 'tif':
# we smetimes run into the error:
# JPEGSetupEncode: RowsPerStrip must be multiple of 8
# for JPEG.
# I don't know how to handle it for now, so then switch
# to LZW compression (files will be much larger)
save_options['compression'] = 'tiff_lzw'
im.save(outfile, **save_options)
else:
raise
except OSError:
print("cannot convert", png_file)
else:
# use ImageMagick convert tool
subprocess.check_call(
['convert', '-quality', '98', '-background', 'white', '-flatten',
'-alpha', 'on', png_file, outfile])
if remove:
os.unlink(png_file)
def build_2d_map(xml_et, out_filename, map_name, filters, clip_rect,
dpi, shadows=True, do_pdf=False, do_jpg=True, georef=None):
svg2d = CataMapTo2DMap()
meta = svg2d.get_metadata(xml_et)
main_clip_rects = meta.get('main_clip_rect_id', '{}')
main_clip_rects = json.loads(main_clip_rects)
main_clip_rect = main_clip_rects.get('default')
if not clip_rect:
clip_rect = main_clip_rects.get(map_name)
if not clip_rect:
clip_rect = main_clip_rect
# print('clip_rect:', clip_rect, ', main_clip_rect:', main_clip_rect)
svg2d.clip_rect_name = clip_rect
clip_rect_name = clip_rect
clip_rect = svg2d.find_clip_rect(xml_et, clip_rect)
svg2d.clip_rect = clip_rect
map2d = svg2d.build_2d_map(xml_et, filters=filters, map_name=map_name)
if clip_rect is not None:
clip_rect = clip_rect.get('id')
xscale = 1.
yscale = 1.
xoffset = 0.
yoffset = 0.
if clip_rect is not None:
svg2d.ensure_clip_rect(map2d, clip_rect, xml_et)
svg2d.clip_page(map2d, clip_rect)
if georef and clip_rect_name != main_clip_rect:
# not the same clipping as the georefed one
print('recalculate georef scaling')
cr = svg2d.clip_rect
crref = svg2d.find_clip_rect(xml_et, main_clip_rect)
print(cr, crref)
if cr is not None and crref is not None:
xscale = float(cr.get('width')) \
/ float(crref.get('width'))
yscale = float(cr.get('height')) \
/ float(crref.get('height'))
xoffset = (float(cr.get('x')) - float(crref.get('x'))) \
/ float(crref.get('width'))
yoffset = (float(cr.get('y')) - float(crref.get('y'))) \
/ float(crref.get('height'))
print('use scale/offset:', xscale, yscale, xoffset, yoffset)
map2d.write(out_filename.replace('.svg', '_%s_flat.svg' % map_name))
if shadows:
svg2d.add_shadows(map2d)
map2d.write(out_filename.replace('.svg', '_%s.svg' % map_name))
# build bitmap and pdf versions
# private
cr = svg2d.find_clip_rect(map2d, clip_rect)
width = float(cr.get('width'))
height = float(cr.get('height'))
print('w, h:', width, height)
print('dpi:', dpi)
wpix = width * float(dpi) / 25.4
hpix = height * float(dpi) / 25.4
print('in pixels:', wpix, hpix)
export_png(out_filename.replace('.svg', '_%s.svg' % map_name),
dpi, clip_rect)
if do_pdf:
export_pdf(out_filename.replace('.svg', '_%s_flat.svg' % map_name))
# TODO: add scaling
# for now to scale PDF:
# pdfjam --outfile out.pdf --papersize '{1050mm,1240.81mm}' --landscape in.pdf
# if needed, rotate:
# qpdf --rotate=270:1 plan_14_fdc_2022_11_08_poster_private_flat.pdf plan_14_fdc_2022_11_08_poster_private_flat_270.pdf
os.unlink(out_filename.replace('.svg', '_%s_flat.svg' % map_name))
if do_jpg or georef:
if georef:
format = 'tif'
else:
format = 'jpg'
print('convert to', format.upper())
convert_to_format(out_filename.replace('.svg', '_%s.png' % map_name),
format=format, max_pixels=(wpix+10)*(wpix+10))
if georef:
try:
from . import gdalcopyproj
except ImportError:
print('gdal module is not installed - georeferencing data will '
'not be copied')
return
tiff_file = out_filename.replace('.svg', '_%s.tif' % map_name)
gdalcopyproj.copy_geo_projection(
georef, tiff_file, scale_x=xscale, scale_y=yscale,
offset_x=xoffset, offset_y=yoffset)
print('geolocalization info copied.')
[docs]def scale_georef_points_file(in_filename, out_filename, scale_factor_x,
scale_factor_y=None):
''' Rescale source positions in a QGis .points file in order to adapt
to a different resolution
'''
if scale_factor_y is None:
scale_factor_y = scale_factor_x
with open(in_filename) as f:
reader = csv.reader(f)
lines = []
for row in reader:
lines.append(row)
# scale columns 2 and 3 (sourceX,sourceY)
for row in lines[2:]:
row[2] = str(float(row[2]) * scale_factor_x)
row[3] = str(float(row[3]) * scale_factor_y)
with open(out_filename, 'w') as f:
# 1st line is not parsed correctly, write it by hand
print(','.join(lines[0]), file=f)
writer = csv.writer(f)
for row in lines[1:]:
writer.writerow(row)
def main():
time_start = time.time()
do_2d = False
do_3d = False
do_igc = False
do_igc_private = False
do_split = False
do_join = False
do_recolor = False
out_dirname = 'meshes_obj'
maps_dpi = {
'default': '180',
'public': '360',
'private': '360',
'private_wip': '180',
'poster': '360',
'poster_private': '720',
'igc': '180',
'igc_private': '360',
'igcportail': '720',
'igcportail_txt': '720',
'igcportail_legend': '720',
'igcportail_tech': '720',
}
all_maps = 'public,private,wip,poster,igc,igc_private,aqueduc,No_Gtech'
class MultilineFormatter(argparse.HelpFormatter):
def _fill_text(self, text, width, indent):
text = text.replace('\n\n* ', '|n |n * ')
text = text.replace('\n* ', '|n * ')
text = text.replace('\n\n', '|n |n ')
text = self._whitespace_matcher.sub(' ', text).strip()
paragraphs = text.split('|n')
multiline_text = ''
for paragraph in paragraphs:
formatted_paragraph = _textwrap.fill(
paragraph, width, initial_indent=indent,
subsequent_indent=indent) + '\n\n'
multiline_text = multiline_text + formatted_paragraph
return multiline_text
parser = ArgumentParser(
prog='catamap',
description='''Catacombs maps using SVG source map with codes inside it.
The program allows to produce:
* 2D "readable" maps with symbolsmain changed to larger ones, second level shifted to avoid superimposition of corridors, enlarged zooms, shadowing etc.
* 3D maps to be used in a 3D visualization program, a webGL server, or the CataZoom app.
''',
formatter_class=MultilineFormatter)
parser.add_argument(
'--2d', action='store_true', dest='do_2d',
help='Build 2D maps (several maps). the maps list is given via the '
'--maps option. If the latter is not specified, do all.')
parser.add_argument(
'-m', '--maps', dest='do_2d_maps',
help='specify which 2d maps should be built, ex: '
'"public,private,igc". Values are in ("public", "private", "wip", '
'"poster", "poster_private", "igc", "igc_private", "aqueduc", '
'"No_GTech", "igcportail", '
'"igcportail_txt", "igcportail_legend", "igcportail_tech"). Default: '
'all if --2d is used. If this option is specified, --2d is implied '
'(thus is not needed)')
parser.add_argument(
'--3d', action='store_true', dest='do_3d',
help='Build 3D map meshes in the "%s/" directory' % out_dirname)
parser.add_argument(
'--color',
help='recolor the maps using a color model. Available models are '
'(currently): igc, bator, black (igc is used automatically in the '
'--igc options). The colorset "default" forces to use the default '
'colors in the map (and cancel any recolor specified in the map). The '
'list of available colorsets for a SVG map can be '
'obtained using the option --list-colorsets.')
parser.add_argument(
'--list-colorsets', action='store_true',
help='list available colorsets in this map')
parser.add_argument(
'--list-maps', action='store_true',
help='list available maps types')
parser.add_argument(
'--list-maps-details', action='store_true',
help='list available maps types and theif full definition')
parser.add_argument(
'--list-filters', action='store_true',
help='list available filters in 2D maps')
parser.add_argument(
'--split', action='store_true',
help='split the SVG file into 4 smaller ones, each containing a '
'subset of the layers')
parser.add_argument(
'--dpi', # nargs='+',
help='output JPEG images resolution. May be global (for all outputs): '
'"360", or scoped to a single map: "igc:360". Several --dpi options '
'may specify resolutions for several output maps: '
'"--dpi 200,igc:360,private:280"')
parser.add_argument(
'--clip',
help='clip using this rectangle ID in the inkscape SVG')
parser.add_argument(
'--no-pdf', action='store_true', default=None,
help='do not generate PDF versions of the map')
parser.add_argument(
'--pdf', action='store_true', default=None,
help='do generate PDF versions of the map')
parser.add_argument(
'--join', action='store_true',
help='reverse the --split operation: concatenate layers from several '
'files')
parser.add_argument(
'--output_filename',
help='base filename for output 2D maps. The filename will be suffixed '
'according to maps types (_imprimable, _imprimable_private, etc) '
'(default: same as input file). Be careful that some maps are using '
'files linked in the same input directory, and will not work if the '
'output is in a different directory.')
parser.add_argument(
'input_file',
help='input SVG Inkscape file')
parser.add_argument(
'output_3d_dir', nargs='?', default=out_dirname,
help='output 3D meshes directory (default: %(default)s)')
parser.add_argument(
'-g', '--georef',
help='Copy GeoTIFF information from the given source file to a .tif '
'export')
parser.add_argument(
'--texture', action='store_true', default=None,
help='make textured meshes in 3D mode, if some are specified in the '
'SVG file. By default it is disabled for now.')
options = parser.parse_args()
do_2d = options.do_2d
do_2d_maps = options.do_2d_maps
if do_2d_maps:
do_2d = True
elif do_2d:
do_2d_maps = all_maps
do_3d = options.do_3d
texturing = options.texture
do_split = options.split
do_join = options.join
do_pdf = None
do_list_colorsets = options.list_colorsets
do_list_maps = options.list_maps
do_list_maps_details = options.list_maps_details
do_list_filters = options.list_filters
if options.no_pdf is not None:
do_pdf = not options.no_pdf
elif options.pdf is not None:
do_pdf = options.pdf
out_filename = options.output_filename
colorset = None
if options.color:
do_recolor = True
colorset = options.color
if options.dpi:
dpi = options.dpi.split(',')
maps_dpi2 = {}
for item in dpi:
dpi_spec = item.split(':')
scope = 'default'
resol = dpi_spec[-1]
if len(dpi_spec) >= 2:
scope = dpi_spec[0]
maps_dpi2[scope] = resol
if 'default' in maps_dpi2:
# "default" overrides all builtin settings
maps_dpi = maps_dpi2
else:
# otherwise we just update
maps_dpi.update(maps_dpi2)
# print('resolutions:', maps_dpi)
clip_rect = options.clip
# print(options)
svg_filename = options.input_file
out_dirname = options.output_3d_dir
if not out_filename:
out_filename = options.input_file
if not out_filename.endswith('.svg'):
out_filename += '.svg'
if do_3d:
svg_mesh = CataSvgToMesh()
svg_mesh.enable_texturing = texturing
else:
svg_mesh = CataMapTo2DMap()
# svg_mesh.debug = True
georef = options.georef
if colorset:
svg_mesh.colorset = colorset
if do_3d or do_2d or do_split or do_recolor or do_list_colorsets \
or do_list_maps or do_list_maps_details:
print('reading SVG...')
xml_et = svg_mesh.read_xml(svg_filename)
if do_list_colorsets:
svg_mesh.list_colorsets(xml_et)
if do_2d or do_list_maps or do_list_maps_details:
maps_def = svg_mesh.read_maps_def(xml_et, colorset, do_recolor)
if do_list_maps or do_list_maps_details:
svg_mesh.list_maps(xml_et, maps_def, details=True)
if do_list_filters:
svg_mesh.list_filters()
if do_3d:
print('extracting meshes...')
meshes = svg_mesh.read_paths(xml_et)
svg_mesh.postprocess(meshes)
print('saving meshes...')
# mesh_format = ('.obj', 'WAVEFRONT')
# mesh_wf_format = ('.obj', 'WAVEFRONT')
mesh_format = '.glb'
mesh_wf_format = '.glb'
summary = svg_mesh.save_mesh_dict(
meshes, out_dirname, mesh_format, mesh_wf_format,
'map_objects.json',
out_filename.replace('.svg', '_imprimable_private.jpg'))
if do_2d:
do_2d_maps = set(do_2d_maps.split(','))
print('build 2D maps:', do_2d_maps)
for map_type in do_2d_maps:
map_def = dict(maps_def[map_type])
if clip_rect:
map_def['clip_rect'] = clip_rect
if do_pdf is not None:
map_def['do_pdf'] = do_pdf
print('clip:', map_def.get('clip_rect'))
if georef:
# don't write jpg, we will use tiff
map_def['do_jpg'] = False
build_2d_map(
xml_et,
out_filename,
map_name=map_def['name'],
filters=map_def['filters'],
clip_rect=map_def.get('clip_rect'),
dpi=maps_dpi.get(map_type, maps_dpi['default']),
shadows=map_def['shadows'],
do_pdf=map_def['do_pdf'],
do_jpg=map_def.get('do_jpg', True),
georef=georef)
if do_split:
svg2d = CataMapTo2DMap()
map2d_split = svg2d.build_2d_map(
xml_et, filters=['split_layers="default"'])
for i, m in enumerate(map2d_split.result[-1]):
m.write(out_filename.replace('.svg', '_%d.svg' % i))
if do_join:
svg2d = CataMapTo2DMap()
map2d_join = svg2d.build_2d_map(
None, filters=['join_layers="%s"' % out_filename])
map2d_join.write(out_filename.replace('.svg', '_joined.svg'))
time_len = time.time() - time_start
print('execution time: %d:%05.2f min:sec.'
% (int(time_len / 60.), time_len - int(time_len / 60.) * 60))
if __name__ == '__main__':
main()