Merge pull request #2665 from wonder-sk/auto-trace

[FEATURE] Tracing of features (digitizing)

Tracing can be now used in various capturing map tools (add feature, add part, ...) including reshape and split tools.

Tracing is simply a new mode for these tools - when tracing is not enabled, the tools work as usual. When tracing is enabled (by clicking the new magnet icon or pressing T key), tools switch to tracing behavior:
- first click on a vertex/edge (must be snapped!) will start tracing - moving mouse on top of the map continuously updates the trace
- next click will confirm the trace and mark start of a new trace Tracing can be enabled/disabled anytime even while digitizing one feature, so it is possible to digitize some parts of the feature with tracing enabled and other parts with tracing disabled.

Tracing respects snapping configuration for the list of traceable layers.

If there are too many features in map display, tracing is disabled to avoid potentially long tracing structure preparation and large memory overhead. After zooming in or disabling some layers the tracing is enabled again.

Internally, things work like this:
- when tracing is requested, linestrings are extracted from vector layers, then noded (using GEOSNode to resolve all intersections) and finally a simple planar graph is built (vertices + edges)
- when tracing, endpoints are temporarily added to the graph (if not equal to one of existing vertices already) and Dijkstra's algorithm is run to get shortest path

Original specs for the curious ones (the interaction with QGIS is slightly improved from what has been specified): http://www.lutraconsulting.co.uk/crowdfunding/autotrace-phase-2/specification.pdf
This commit is contained in:
Martin Dobias 2016-01-13 12:18:00 +01:00
commit b83f6e359a
22 changed files with 2214 additions and 11 deletions

View File

@ -535,6 +535,7 @@
<file>flags/zh.png</file>
<file>icons/qgis-icon-16x16_xmas.png</file>
<file>icons/qgis-icon-60x60_xmas.png</file>
<file>themes/default/mActionTracing.png</file>
</qresource>
<qresource prefix="/images/tips">
<file alias="symbol_levels.png">qgis_tips/symbol_levels.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,524 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="32"
height="32"
id="svg5692">
<title
id="title2829">GIS icon theme 0.2</title>
<defs
id="defs5694" />
<metadata
id="metadata5697">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>GIS icon theme 0.2</dc:title>
<dc:creator>
<cc:Agent>
<dc:title>Robert Szczepanek</dc:title>
</cc:Agent>
</dc:creator>
<dc:rights>
<cc:Agent>
<dc:title>Robert Szczepanek</dc:title>
</cc:Agent>
</dc:rights>
<dc:subject>
<rdf:Bag>
<rdf:li>GIS icons</rdf:li>
</rdf:Bag>
</dc:subject>
<dc:coverage>GIS icons</dc:coverage>
<dc:description>http://robert.szczepanek.pl/</dc:description>
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/3.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/3.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer2"
style="display:inline">
<g
transform="translate(-112.42998,-2.426407)"
id="g4012">
<path
d="m -54.342779,117.15722 0,25.71875 8.9375,-10.65625 0.93701,-13.31348 z"
id="path4337"
style="fill:#c8e6fa;fill-opacity:1;stroke:#415a75;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m -54.342779,117.15722 9.87451,1.74902 9.811048,-1.58623 6,10 -16.748058,4.89971 z"
id="path4341"
style="fill:#ee8d8d;fill-opacity:1;stroke:none" />
<path
d="m -54.342779,117.15722 9.87451,1.74902 9.811048,-1.58623 6,10 -16.748058,4.89971 z"
id="path4343"
style="fill:none;stroke:#de1e1e;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-62.265617,88.861371)"
id="path4345"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-62.265617,114.58012)"
id="path4347"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-53.328117,103.92387)"
id="path4349"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-52.391107,90.610391)"
id="path4351"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-42.580059,89.024155)"
id="path4353"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-36.580059,99.024155)"
id="path4355"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
width="14.767767"
height="14.767767"
rx="2.9705217"
ry="2.9705215"
x="-40.110546"
y="131.38945"
id="rect4357"
style="fill:#5a8c5a;fill-opacity:1;stroke:#555753;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
<path
d="m -31.761563,131.79749 -2.204875,2.26951 3.807277,3.6803 0.207661,0.20075 a 2.2989925,2.2085813 42.304053 0 1 0.110678,3.20209 2.2989925,2.2085813 42.304053 0 1 -3.303179,-0.14628 l -0.161558,-0.1561 -3.738052,-3.61338 -2.204874,2.2695 4.061096,3.92565 0.299944,0.28999 a 4.9792011,5.3638887 45.069715 0 0 7.26171,-0.42807 4.9792011,5.3638887 45.069715 0 0 0.225173,-7.27828 l -0.161564,-0.1561 -4.199539,-4.05948 z"
id="path4359"
style="opacity:0.53899997;fill:#ffffff;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-72.111198"
y="117.48358"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect4361"
style="fill:#fcffff;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-72.111198"
y="125.05627"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect4363"
style="fill:#fcffff;fill-opacity:1" />
<path
d="m -34.074431,137.59747 -0.809483,-3.27612 -2.045363,0.25363 -1.365385,-1.70728 0.645033,3.20893 2.010296,-0.49087 z"
id="path4365"
style="opacity:0.53899997;fill:#ffffff;fill-opacity:1;stroke:#fcffff;stroke-width:0.01204192px;stroke-opacity:1" />
</g>
<g
transform="translate(-155.56349,-38.488852)"
id="g4028">
<path
d="m -55.886195,154.04809 0,25.71875 4.693913,-5.59659 4.243587,-5.05966 0.93701,-13.31348 z"
id="path3091"
style="fill:#c8e6fa;fill-opacity:1;stroke:#415a75;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m -55.886195,154.04809 9.87451,1.74902 9.811048,-1.58623 6,10 -16.748058,4.89971 z"
id="path3095"
style="opacity:0.50400002;fill:#de1e1e;fill-opacity:1;stroke:none" />
<path
d="m -55.886195,154.04809 9.87451,1.74902 9.811048,-1.58623 6,10 -16.748058,4.89971 z"
id="path3097"
style="fill:none;stroke:#de1e1e;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-63.809033,125.75224)"
id="path3099"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-63.809033,151.47099)"
id="path3101"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-54.871533,140.81474)"
id="path3103"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-53.934523,127.50126)"
id="path3105"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-44.123475,125.91503)"
id="path3107"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-38.123475,135.91502)"
id="path3109"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
width="14.767767"
height="14.767767"
rx="2.9705217"
ry="2.9705215"
x="-41.653961"
y="168.28032"
id="rect3111"
style="fill:#5a8c5a;fill-opacity:1;stroke:#555753;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
<path
d="m -33.304979,168.68836 -2.204875,2.26951 3.807277,3.6803 0.207661,0.20075 a 2.2989925,2.2085813 42.304053 0 1 0.110678,3.20209 2.2989925,2.2085813 42.304053 0 1 -3.303179,-0.14628 l -0.161558,-0.1561 -3.738052,-3.61338 -2.204874,2.2695 4.061096,3.92565 0.299944,0.28999 a 4.9792011,5.3638887 45.069715 0 0 7.26171,-0.42807 4.9792011,5.3638887 45.069715 0 0 0.225173,-7.27828 l -0.161564,-0.1561 -4.199539,-4.05948 z"
id="path3113"
style="opacity:0.53899997;fill:#ffffff;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-96.710617"
y="145.08072"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect3115"
style="fill:#fcffff;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-96.710617"
y="152.65341"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect3117"
style="fill:#fcffff;fill-opacity:1" />
<path
d="m -35.617847,174.48834 -0.809483,-3.27612 -2.045363,0.25363 -1.365385,-1.70727 0.645033,3.20892 2.010296,-0.49087 z"
id="path3119"
style="opacity:0.53899997;fill:#ffffff;fill-opacity:1;stroke:#fcffff;stroke-width:0.01204192px;stroke-opacity:1" />
</g>
<g
transform="translate(-214.96046,91.6188)"
id="g4109">
<path
d="m 3.1572213,63.157221 0,25.71875 4.6939131,-5.596589 4.2435866,-5.059661 0.93701,-13.31348 z"
id="path3906"
style="fill:#c8e6fa;fill-opacity:1;stroke:#415a75;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 3.1572213,63.157221 9.8745097,1.74902 9.811048,-1.586236 6,10 -16.748058,4.899716 z"
id="path3910"
style="opacity:0.50400002;fill:#de1e1e;fill-opacity:1;stroke:none" />
<path
d="m 3.1572213,63.157221 9.8745097,1.74902 9.811048,-1.586236 6,10 -16.748058,4.899716 z"
id="path3912"
style="fill:none;stroke:#de1e1e;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-4.7656167,34.861371)"
id="path3914"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-4.7656167,60.580121)"
id="path3916"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,4.1718833,49.923871)"
id="path3918"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,5.1088931,36.610391)"
id="path3920"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,14.919941,35.024155)"
id="path3922"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,20.919941,45.024155)"
id="path3924"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
width="14.767767"
height="14.767767"
rx="2.9705217"
ry="2.9705215"
x="17.389454"
y="77.38945"
id="rect3926"
style="fill:#5a8c5a;fill-opacity:1;stroke:#555753;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
<path
d="m 25.738437,77.797493 -2.204875,2.269506 3.807277,3.680298 0.207661,0.200753 a 2.2989925,2.2085813 42.304053 0 1 0.110678,3.202086 2.2989925,2.2085813 42.304053 0 1 -3.303179,-0.146277 l -0.161558,-0.156097 -3.738052,-3.613385 -2.204874,2.269504 4.061096,3.925651 0.299944,0.289984 a 4.9792011,5.3638887 45.069715 0 0 7.26171,-0.428063 4.9792011,5.3638887 45.069715 0 0 0.225173,-7.278283 l -0.161564,-0.156096 -4.199539,-4.059482 z"
id="path3929"
style="fill:#de1e1e;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-75.724571"
y="38.694225"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect3931"
style="fill:#eeeeec;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-75.724571"
y="46.266914"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect3933"
style="fill:#eeeeec;fill-opacity:1" />
<path
d="m 23.425569,83.59747 -0.809483,-3.276117 -2.045363,0.253629 -1.365385,-1.707277 0.645033,3.208922 2.010296,-0.490865 z"
id="path3935"
style="fill:#eeeeec;fill-opacity:1;stroke:#fcffff;stroke-width:0.01204192px;stroke-opacity:1" />
</g>
<g
transform="translate(0,40)"
id="g4487">
<path
d="m -212.84278,151.65722 0,25.71875 4.69391,-5.59659 4.24359,-5.05966 0.93701,-13.31348 z"
id="path3937"
style="fill:#c8e6fa;fill-opacity:1;stroke:#415a75;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m -212.84278,151.65722 9.87451,1.74902 9.81105,-1.58623 6,10 -16.74806,4.89971 z"
id="path3941"
style="opacity:0.50400002;fill:#de1e1e;fill-opacity:1;stroke:none" />
<path
d="m -212.84278,151.65722 9.87451,1.74902 9.81105,-1.58623 6,10 -16.74806,4.89971 z"
id="path3943"
style="fill:none;stroke:#de1e1e;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-220.76562,123.36137)"
id="path3945"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-220.76562,149.08012)"
id="path3947"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-211.82812,138.42387)"
id="path3949"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-210.89111,125.11039)"
id="path3951"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-201.08006,123.52415)"
id="path3953"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-195.08006,133.52415)"
id="path3955"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m -190.26156,166.29749 -2.20488,2.26951 3.80728,3.6803 0.20766,0.20075 a 2.2989925,2.2085813 42.304053 0 1 0.11068,3.20209 2.2989925,2.2085813 42.304053 0 1 -3.30318,-0.14628 l -0.16156,-0.1561 -3.73805,-3.61338 -2.20487,2.2695 4.06109,3.92565 0.29994,0.28999 a 4.9792011,5.3638887 45.069715 0 0 7.26172,-0.42807 4.9792011,5.3638887 45.069715 0 0 0.22517,-7.27828 l -0.16157,-0.1561 -4.19953,-4.05948 z"
id="path3959"
style="fill:#de1e1e;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="17.532269"
y="252.4493"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect3961"
style="fill:#eeeeec;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="17.532269"
y="260.02197"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect3963"
style="fill:#eeeeec;fill-opacity:1" />
<path
d="m -192.57443,172.09747 -0.80948,-3.27612 -2.04537,0.25363 -1.36538,-1.70727 0.64503,3.20892 2.0103,-0.49087 z"
id="path3965"
style="fill:#c4a000;fill-opacity:1;stroke:#fcffff;stroke-width:0.01204192px;stroke-opacity:1" />
</g>
<g
transform="translate(0,40)"
id="g4185">
<path
d="m -167.80324,114.77602 0,25.71875 4.69391,-5.59659 4.24359,-5.05966 0.93701,-13.31348 z"
id="path4142"
style="fill:#c8e6fa;fill-opacity:1;stroke:#415a75;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m -167.80324,114.77602 9.87451,1.74902 9.81105,-1.58624 6,10 -16.74806,4.89972 z"
id="path4144"
style="fill:#ee8d8d;fill-opacity:1;stroke:none" />
<path
d="m -167.80324,114.77602 9.87451,1.74902 9.81105,-1.58624 6,10 -16.74806,4.89972 z"
id="path4146"
style="fill:none;stroke:#de1e1e;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-175.72608,86.480166)"
id="path4148"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-175.72608,112.19892)"
id="path4150"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-166.78858,101.54267)"
id="path4152"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-165.85157,88.229186)"
id="path4154"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-156.04052,86.64295)"
id="path4156"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-150.04052,96.64295)"
id="path4159"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<rect
width="14.767767"
height="14.767767"
rx="2.9705217"
ry="2.9705215"
x="-153.571"
y="129.00824"
id="rect4161"
style="fill:#5a8c5a;fill-opacity:1;stroke:#555753;stroke-width:0;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline" />
<path
d="m -145.22202,129.41629 -2.20488,2.2695 3.80728,3.6803 0.20766,0.20075 a 2.2989925,2.2085813 42.304053 0 1 0.11068,3.20209 2.2989925,2.2085813 42.304053 0 1 -3.30318,-0.14628 l -0.16156,-0.15609 -3.73805,-3.61339 -2.20487,2.26951 4.06109,3.92565 0.29994,0.28998 a 4.9792011,5.3638887 45.069715 0 0 7.26172,-0.42806 4.9792011,5.3638887 45.069715 0 0 0.22517,-7.27829 l -0.16157,-0.15609 -4.19953,-4.05948 z"
id="path4163"
style="fill:#de1e1e;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="10.927393"
y="194.62854"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect4165"
style="fill:#eeeeec;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="10.927393"
y="202.20123"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect4167"
style="fill:#eeeeec;fill-opacity:1" />
<path
d="m -147.53489,135.21626 -0.80948,-3.27611 -2.04537,0.25363 -1.36538,-1.70728 0.64503,3.20892 2.0103,-0.49086 z"
id="path4169"
style="fill:#eeeeec;fill-opacity:1;stroke:#fcffff;stroke-width:0.01204192px;stroke-opacity:1" />
</g>
<path
d="m -168.84278,191.65722 0,25.71875 4.69391,-5.59659 4.24359,-5.05966 0.93701,-13.31348 z"
id="path4203"
style="fill:#c8e6fa;fill-opacity:1;stroke:#415a75;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m -168.84278,191.65722 9.87451,1.74902 9.81105,-1.58623 6,10 -16.74806,4.89971 z"
id="path4205"
style="fill:#ee8d8d;fill-opacity:1;stroke:none" />
<path
d="m -168.84278,191.65722 9.87451,1.74902 9.81105,-1.58623 6,10 -16.74806,4.89971 z"
id="path4207"
style="fill:none;stroke:#de1e1e;stroke-width:1.78699994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-176.76562,163.36137)"
id="path4209"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-176.76562,189.08012)"
id="path4211"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#415a75;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-167.82812,178.42387)"
id="path4213"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-166.89111,165.11039)"
id="path4215"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-157.08006,163.52415)"
id="path4217"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<path
d="m 4.5,12.5 a 1,1 0 1 1 -2,0 1,1 0 1 1 2,0 z"
transform="matrix(2.263668,0,0,2.263668,-151.08006,173.52415)"
id="path4219"
style="color:#000000;fill:#eeeeec;fill-opacity:1;fill-rule:evenodd;stroke:#de1e1e;stroke-width:0.78947365;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
<g
transform="matrix(1.9657739,0,0,1.9657739,-1.9275051,-3.7037649)"
id="g3258">
<path
d="m 9.9884401,3.4224899 -2.20488,2.26951 3.8072799,3.6803 0.20766,0.20075 A 2.2989925,2.2085813 42.304053 0 1 11.90918,12.77514 2.2989925,2.2085813 42.304053 0 1 8.6060001,12.62886 l -0.16156,-0.1561 -3.73805,-3.6133801 -2.20487,2.2695001 4.06109,3.92565 0.29994,0.28999 A 4.9792011,5.3638887 45.069715 0 0 14.12427,14.91645 4.9792011,5.3638887 45.069715 0 0 14.34944,7.6381699 l -0.16157,-0.1561 -4.1995299,-4.05948 z"
id="path4221"
style="fill:#de1e1e;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-12.601767"
y="-3.8346529"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect4223"
style="fill:#eeeeec;fill-opacity:1" />
<rect
width="2.5400591"
height="1.8931522"
x="-12.601767"
y="3.73804"
transform="matrix(-0.71899469,-0.69501557,-0.69682006,0.71724598,0,0)"
id="rect4225"
style="fill:#eeeeec;fill-opacity:1" />
<path
d="m 7.6755701,9.2224699 -0.80948,-3.27612 -2.04537,0.25363 -1.36538,-1.70727 0.64503,3.20892 2.0103,-0.49087 z"
id="path4227"
style="fill:#c4a000;fill-opacity:1;stroke:#fcffff;stroke-width:0.00612579px;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -120,6 +120,7 @@
%Include qgsstatisticalsummary.sip
%Include qgsstringutils.sip
%Include qgstolerance.sip
%Include qgstracer.sip
%Include qgsvectordataprovider.sip
%Include qgsvectorfilewriter.sip
%Include qgsvectorlayer.sip

View File

@ -59,15 +59,18 @@ class QgsSnappingUtils : QObject
/** Find out which strategy is used for indexing - by default hybrid indexing is used */
IndexingStrategy indexingStrategy() const;
/** Configure options used when the mode is snap to current layer */
/** Configure options used when the mode is snap to current layer or to all layers */
void setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit );
/** Query options used when the mode is snap to current layer */
/** Query options used when the mode is snap to current layer or to all layers */
void defaultSettings( int& type /Out/, double& tolerance /Out/, QgsTolerance::UnitType& unit /Out/ );
struct LayerConfig
{
LayerConfig( QgsVectorLayer* l, const QgsPointLocator::Types& t, double tol, QgsTolerance::UnitType u );
bool operator==( const QgsSnappingUtils::LayerConfig& other ) const;
bool operator!=( const QgsSnappingUtils::LayerConfig& other ) const;
QgsVectorLayer* layer;
QgsPointLocator::Types type;
double tolerance;
@ -88,6 +91,12 @@ class QgsSnappingUtils : QObject
/** Read snapping configuration from the project */
void readConfigFromProject();
signals:
/** Emitted when snapping configuration has been changed
* @note added in QGIS 2.14
*/
void configChanged();
protected:
//! Called when starting to index - can be overridden and e.g. progress dialog can be provided
virtual void prepareIndexStarting( int count );

74
python/core/qgstracer.sip Normal file
View File

@ -0,0 +1,74 @@
/** \ingroup core
* Utility class that construct a planar graph from the input vector
* layers and provides shortest path search for tracing of existing
* features.
*
* @note added in QGIS 2.14
*/
class QgsTracer : QObject
{
%TypeHeaderCode
#include <qgstracer.h>
%End
public:
QgsTracer();
~QgsTracer();
//! Get layers used for tracing
QList<QgsVectorLayer*> layers() const;
//! Set layers used for tracing
void setLayers( const QList<QgsVectorLayer*>& layers );
//! Get CRS used for tracing
QgsCoordinateReferenceSystem destinationCrs() const;
//! Set CRS used for tracing
void setDestinationCrs( const QgsCoordinateReferenceSystem& crs );
//! Get extent to which graph's features will be limited (empty extent means no limit)
QgsRectangle extent() const;
//! Set extent to which graph's features will be limited (empty extent means no limit)
void setExtent( const QgsRectangle& extent );
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
int maxFeatureCount() const;
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
void setMaxFeatureCount( int count );
//! Build the internal data structures. This may take some time
//! depending on how big the input layers are. It is not necessary
//! to call this method explicitly - it will be called by findShortestPath()
//! if necessary.
bool init();
//! Whether the internal data structures have been initialized
bool isInitialized() const;
//! Possible errors that may happen when calling findShortestPath()
enum PathError
{
ErrNone, //!< No error
ErrTooManyFeatures, //!< Max feature count treshold was reached while reading features
ErrPoint1, //!< Start point cannot be joined to the graph
ErrPoint2, //!< End point cannot be joined to the graph
ErrNoPath, //!< Points are not connected in the graph
};
//! Given two points, find the shortest path and return points on the way.
//! The optional "error" argument may receive error code (PathError enum) if it is not null
//! @return array of points - trace of linestrings of other features (empty array one error)
QVector<QgsPoint> findShortestPath( const QgsPoint& p1, const QgsPoint& p2, QgsTracer::PathError* error /Out/ = nullptr );
//! Find out whether the point is snapped to a vertex or edge (i.e. it can be used for tracing start/stop)
bool isPointSnapped( const QgsPoint& pt );
protected:
//! Allows derived classes to setup the settings just before the tracer is initialized.
//! This allows the configuration to be set in a lazy way only when it is really necessary.
//! Default implementation does nothing.
virtual void configure();
protected slots:
//! Destroy the existing graph structure if any (de-initialize)
void invalidateGraph();
};

View File

@ -93,6 +93,7 @@
%Include qgsmapcanvasmap.sip
%Include qgsmapcanvassnapper.sip
%Include qgsmapcanvassnappingutils.sip
%Include qgsmapcanvastracer.sip
%Include qgsmaplayeractionregistry.sip
%Include qgsmaplayercombobox.sip
%Include qgsmaplayermodel.sip

View File

@ -0,0 +1,38 @@
/** \ingroup gui
* Extension of QgsTracer that provides extra functionality:
* - automatic updates of own configuration based on canvas settings
* - reporting of issues to the user via message bar
*
* A simple registry of tracer instances associated to map canvas instances
* is kept for convenience. (Map tools do not need to create their local
* tracer instances and map canvas API is not "polluted" by this optional
* functionality).
*
* @note added in QGIS 2.14
*/
class QgsMapCanvasTracer : QgsTracer
{
%TypeHeaderCode
#include <qgsmapcanvastracer.h>
%End
public:
//! Create tracer associated with a particular map canvas, optionally message bar for reporting
explicit QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar = 0 );
~QgsMapCanvasTracer();
//! Access to action that user may use to toggle tracing on/off
QAction* actionEnableTracing();
//! Retrieve instance of this class associated with given canvas (if any).
//! The class keeps a simple registry of tracers associated with map canvas
//! instances for easier access to the common tracer by various map tools
static QgsMapCanvasTracer* tracerForCanvas( QgsMapCanvas* canvas );
//! Report a path finding error to the user
void reportError( QgsTracer::PathError err, bool addingVertex );
protected:
//! Sets configuration from current snapping settings and canvas settings
virtual void configure();
};

View File

@ -164,6 +164,7 @@
#include "qgslogger.h"
#include "qgsmapcanvas.h"
#include "qgsmapcanvassnappingutils.h"
#include "qgsmapcanvastracer.h"
#include "qgsmaplayer.h"
#include "qgsmaplayerregistry.h"
#include "qgsmaplayerstyleguiutils.h"
@ -556,6 +557,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, QWidget * parent,
, mComposerManager( nullptr )
, mpTileScaleWidget( nullptr )
, mpGpsWidget( nullptr )
, mTracer( nullptr )
, mSnappingUtils( nullptr )
, mProjectLastModified()
, mWelcomePage( nullptr )
@ -1024,6 +1026,7 @@ QgisApp::QgisApp()
, mMacrosWarn( nullptr )
, mUserInputDockWidget( nullptr )
, mVectorLayerTools( nullptr )
, mTracer( nullptr )
, mActionFilterLegend( nullptr )
, mLegendExpressionFilterButton( nullptr )
, mSnappingUtils( nullptr )
@ -1104,6 +1107,8 @@ QgisApp::~QgisApp()
delete mComposerManager;
delete mTracer;
delete mVectorLayerTools;
delete mWelcomePage;
@ -1975,6 +1980,9 @@ void QgisApp::createToolBars()
// Cad toolbar
mAdvancedDigitizeToolBar->insertAction( mActionUndo, mAdvancedDigitizingDockWidget->enableAction() );
mTracer = new QgsMapCanvasTracer( mMapCanvas, messageBar() );
mAdvancedDigitizeToolBar->insertAction( mActionUndo, mTracer->actionEnableTracing() );
}
void QgisApp::createStatusBar()
@ -9681,6 +9689,7 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer )
mActionMergeFeatures->setEnabled( false );
mActionMergeFeatureAttributes->setEnabled( false );
mActionRotatePointSymbols->setEnabled( false );
mTracer->actionEnableTracing()->setEnabled( false );
mActionPinLabels->setEnabled( false );
mActionShowHideLabels->setEnabled( false );
@ -9801,6 +9810,9 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer* layer )
mActionRotateFeature->setEnabled( isEditable && canChangeGeometry );
mActionNodeTool->setEnabled( isEditable && canChangeGeometry );
mTracer->actionEnableTracing()->setEnabled( isEditable && canAddFeatures &&
( vlayer->geometryType() == QGis::Line || vlayer->geometryType() == QGis::Polygon ) );
if ( vlayer->geometryType() == QGis::Point )
{
mActionAddFeature->setIcon( QgsApplication::getThemeIcon( "/mActionCapturePoint.svg" ) );

View File

@ -81,6 +81,7 @@ class QgsAdvancedDigitizingDockWidget;
class QgsSnappingDialog;
class QgsGPSInformationWidget;
class QgsStatisticalSummaryDockWidget;
class QgsMapCanvasTracer;
class QgsDecorationItem;
@ -1698,6 +1699,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
QgsVectorLayerTools* mVectorLayerTools;
//! A class that facilitates tracing of features
QgsMapCanvasTracer* mTracer;
QAction* mActionFilterLegend;
QgsLegendFilterButton* mLegendExpressionFilterButton;

View File

@ -184,6 +184,7 @@ SET(QGIS_CORE_SRCS
qgssqlexpressioncompiler.cpp
qgsstatisticalsummary.cpp
qgsstringutils.cpp
qgstracer.cpp
qgstransaction.cpp
qgstextlabelfeature.cpp
qgstolerance.cpp
@ -455,6 +456,7 @@ SET(QGIS_CORE_MOC_HDRS
qgsrelationmanager.h
qgsrunprocess.h
qgssnappingutils.h
qgstracer.h
qgstransaction.h
qgsvectordataprovider.h
qgsvectorlayercache.h
@ -660,6 +662,7 @@ SET(QGIS_CORE_HDRS
qgsstringutils.h
qgstextlabelfeature.h
qgstolerance.h
qgstracer.h
qgsvectordataprovider.h
qgsvectorlayercache.h

View File

@ -369,15 +369,35 @@ void QgsSnappingUtils::setMapSettings( const QgsMapSettings& settings )
clearAllLocators();
}
void QgsSnappingUtils::setCurrentLayer( QgsVectorLayer* layer )
{
mCurrentLayer = layer;
}
void QgsSnappingUtils::setSnapToMapMode( QgsSnappingUtils::SnapToMapMode mode )
{
if ( mSnapToMapMode == mode )
return;
mSnapToMapMode = mode;
emit configChanged();
}
void QgsSnappingUtils::setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit )
{
// force map units - can't use layer units for just any layer
if ( unit == QgsTolerance::LayerUnits )
unit = QgsTolerance::ProjectUnits;
if ( mDefaultType == type && mDefaultTolerance == tolerance && mDefaultUnit == unit )
return;
mDefaultType = type;
mDefaultTolerance = tolerance;
mDefaultUnit = unit;
if ( mSnapToMapMode != SnapAdvanced ) // does not affect advanced mode
emit configChanged();
}
void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsTolerance::UnitType& unit )
@ -387,6 +407,25 @@ void QgsSnappingUtils::defaultSettings( int& type, double& tolerance, QgsToleran
unit = mDefaultUnit;
}
void QgsSnappingUtils::setLayers( const QList<QgsSnappingUtils::LayerConfig>& layers )
{
if ( mLayers == layers )
return;
mLayers = layers;
if ( mSnapToMapMode == SnapAdvanced ) // only affects advanced mode
emit configChanged();
}
void QgsSnappingUtils::setSnapOnIntersections( bool enabled )
{
if ( mSnapOnIntersection == enabled )
return;
mSnapOnIntersection = enabled;
emit configChanged();
}
const QgsCoordinateReferenceSystem* QgsSnappingUtils::destCRS()
{
return mMapSettings.hasCrsTransformEnabled() ? &mMapSettings.destinationCrs() : nullptr;
@ -467,6 +506,7 @@ void QgsSnappingUtils::readConfigFromProject()
mLayers.append( LayerConfig( vlayer, t, tolIt->toDouble(), static_cast< QgsTolerance::UnitType >( tolUnitIt->toInt() ) ) );
}
emit configChanged();
}
void QgsSnappingUtils::onLayersWillBeRemoved( const QStringList& layerIds )

View File

@ -64,7 +64,7 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
const QgsMapSettings& mapSettings() const { return mMapSettings; }
/** Set current layer so that if mode is SnapCurrentLayer we know which layer to use */
void setCurrentLayer( QgsVectorLayer* layer ) { mCurrentLayer = layer; }
void setCurrentLayer( QgsVectorLayer* layer );
QgsVectorLayer* currentLayer() const { return mCurrentLayer; }
@ -79,7 +79,7 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
};
/** Set how the snapping to map is done */
void setSnapToMapMode( SnapToMapMode mode ) { mSnapToMapMode = mode; }
void setSnapToMapMode( SnapToMapMode mode );
/** Find out how the snapping to map is done */
SnapToMapMode snapToMapMode() const { return mSnapToMapMode; }
@ -95,9 +95,9 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
/** Find out which strategy is used for indexing - by default hybrid indexing is used */
IndexingStrategy indexingStrategy() const { return mStrategy; }
/** Configure options used when the mode is snap to current layer */
/** Configure options used when the mode is snap to current layer or to all layers */
void setDefaultSettings( int type, double tolerance, QgsTolerance::UnitType unit );
/** Query options used when the mode is snap to current layer */
/** Query options used when the mode is snap to current layer or to all layers */
void defaultSettings( int& type, double& tolerance, QgsTolerance::UnitType& unit );
/**
@ -107,6 +107,15 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
{
LayerConfig( QgsVectorLayer* l, const QgsPointLocator::Types& t, double tol, QgsTolerance::UnitType u ) : layer( l ), type( t ), tolerance( tol ), unit( u ) {}
bool operator==( const LayerConfig& other ) const
{
return layer == other.layer && type == other.type && tolerance == other.tolerance && unit == other.unit;
}
bool operator!=( const LayerConfig& other ) const
{
return !operator==( other );
}
//! The layer to configure.
QgsVectorLayer* layer;
//! To which geometry properties of this layers a snapping should happen.
@ -118,12 +127,12 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
};
/** Set layers which will be used for snapping */
void setLayers( const QList<LayerConfig>& layers ) { mLayers = layers; }
void setLayers( const QList<LayerConfig>& layers );
/** Query layers used for snapping */
QList<LayerConfig> layers() const { return mLayers; }
/** Set whether to consider intersections of nearby segments for snapping */
void setSnapOnIntersections( bool enabled ) { mSnapOnIntersection = enabled; }
void setSnapOnIntersections( bool enabled );
/** Query whether to consider intersections of nearby segments for snapping */
bool snapOnIntersections() const { return mSnapOnIntersection; }
@ -131,6 +140,12 @@ class CORE_EXPORT QgsSnappingUtils : public QObject
/** Read snapping configuration from the project */
void readConfigFromProject();
signals:
/** Emitted when snapping configuration has been changed
* @note added in QGIS 2.14
*/
void configChanged();
protected:
//! Called when starting to index - can be overridden and e.g. progress dialog can be provided
virtual void prepareIndexStarting( int count ) { Q_UNUSED( count ); }

676
src/core/qgstracer.cpp Normal file
View File

@ -0,0 +1,676 @@
/***************************************************************************
qgstracer.cpp
--------------------------------------
Date : January 2016
Copyright : (C) 2016 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgstracer.h"
#include "qgsgeometry.h"
#include "qgsgeometryutils.h"
#include "qgslogger.h"
#include "qgsvectorlayer.h"
#include <queue>
#include <vector>
typedef std::pair<int, double> queue_item; // first = vertex index, second = distance
// utility comparator for queue items based on distance
struct comp
{
bool operator()( const queue_item &a, const queue_item &b )
{
return a.second > b.second;
}
};
// TODO: move to geometry utils
double distance_2d( const QgsPolyline& coords )
{
int np = coords.count();
if ( np == 0 )
return 0;
double x0 = coords[0].x(), y0 = coords[0].y();
double x1, y1;
double dist = 0;
for ( int i = 1; i < np; ++i )
{
x1 = coords[i].x();
y1 = coords[i].y();
dist += sqrt(( x1 - x0 ) * ( x1 - x0 ) + ( y1 - y0 ) * ( y1 - y0 ) );
x0 = x1;
y0 = y1;
}
return dist;
}
// TODO: move to geometry utils
double closest_segment( const QgsPolyline& pl, const QgsPoint& pt, int& vertexAfter, double epsilon )
{
double sqrDist = std::numeric_limits<double>::max();
const QgsPoint* pldata = pl.constData();
int plcount = pl.count();
double prevX = pldata[0].x(), prevY = pldata[0].y();
double segmentPtX, segmentPtY;
for ( int i = 1; i < plcount; ++i )
{
double currentX = pldata[i].x();
double currentY = pldata[i].y();
double testDist = QgsGeometryUtils::sqrDistToLine( pt.x(), pt.y(), prevX, prevY, currentX, currentY, segmentPtX, segmentPtY, epsilon );
if ( testDist < sqrDist )
{
sqrDist = testDist;
vertexAfter = i;
}
prevX = currentX;
prevY = currentY;
}
return sqrDist;
}
/////
/** Simple graph structure for shortest path search */
struct QgsTracerGraph
{
struct E // bidirectional edge
{
//! vertices that the edge connects
int v1, v2;
//! coordinates of the edge (including endpoints)
QVector<QgsPoint> coords;
int other_vertex( int v0 ) const { return v1 == v0 ? v2 : v1; }
double weight() const { return distance_2d( coords ); }
};
struct V
{
//! location of the vertex
QgsPoint pt;
//! indices of adjacent edges (used in Dijkstra algorithm)
QVector<int> edges;
};
//! Vertices of the graph
QVector<V> v;
//! Edges of the graph
QVector<E> e;
//! Temporarily removed edges
QSet<int> inactiveEdges;
//! Temporarily added vertices (for each there are two extra edges)
int joinedVertices;
};
QgsTracerGraph* make_graph( const QVector<QgsPolyline>& edges )
{
QgsTracerGraph* g = new QgsTracerGraph;
g->joinedVertices = 0;
QHash<QgsPoint, int> point2vertex;
foreach ( const QgsPolyline& line, edges )
{
QgsPoint p1( line[0] );
QgsPoint p2( line[line.count() - 1] );
int v1 = -1, v2 = -1;
// get or add vertex 1
if ( point2vertex.contains( p1 ) )
v1 = point2vertex.value( p1 );
else
{
v1 = g->v.count();
QgsTracerGraph::V v;
v.pt = p1;
g->v.append( v );
point2vertex[p1] = v1;
}
// get or add vertex 2
if ( point2vertex.contains( p2 ) )
v2 = point2vertex.value( p2 );
else
{
v2 = g->v.count();
QgsTracerGraph::V v;
v.pt = p2;
g->v.append( v );
point2vertex[p2] = v2;
}
// add edge
QgsTracerGraph::E e;
e.v1 = v1;
e.v2 = v2;
e.coords = line;
g->e.append( e );
// link edge to vertices
int eIdx = g->e.count() - 1;
g->v[v1].edges << eIdx;
g->v[v2].edges << eIdx;
}
return g;
}
QVector<QgsPoint> shortest_path( const QgsTracerGraph& g, int v1, int v2 )
{
if ( v1 == -1 || v2 == -1 )
return QVector<QgsPoint>(); // invalid input
// priority queue to drive Dijkstra:
// first of the pair is vertex index, second is distance
std::priority_queue< queue_item, std::vector< queue_item >, comp > Q;
// shortest distances to each vertex
QVector<double> D( g.v.count(), std::numeric_limits<double>::max() );
D[v1] = 0;
// whether vertices have been already processed
QVector<bool> F( g.v.count() );
// using which edge there is shortest path to each vertex
QVector<int> S( g.v.count(), -1 );
int u = -1;
Q.push( queue_item( v1, 0 ) );
while ( !Q.empty() )
{
u = Q.top().first; // new vertex to visit
Q.pop();
if ( u == v2 )
break; // we can stop now, there won't be a shorter path
if ( F[u] )
continue; // ignore previously added path which is actually longer
const QgsTracerGraph::V& vu = g.v[u];
const int* vuEdges = vu.edges.constData();
int count = vu.edges.count();
for ( int i = 0; i < count; ++i )
{
const QgsTracerGraph::E& edge = g.e[ vuEdges[i] ];
int v = edge.other_vertex( u );
double w = edge.weight();
if ( !F[v] && D[u] + w < D[v] )
{
// found a shorter way to the vertex
D[v] = D[u] + w;
S[v] = vuEdges[i];
Q.push( queue_item( v, D[v] ) );
}
}
F[u] = 1; // mark the vertex as processed (we know the fastest path to it)
}
if ( u != v2 ) // there's no path to the end vertex
return QVector<QgsPoint>();
//qDebug("dist %f", D[u]);
QVector<QgsPoint> points;
QList<int> path;
while ( S[u] != -1 )
{
path << S[u];
const QgsTracerGraph::E& e = g.e[S[u]];
QVector<QgsPoint> edgePoints = e.coords;
if ( edgePoints[0] != g.v[u].pt )
std::reverse( edgePoints.begin(), edgePoints.end() );
if ( !points.isEmpty() )
points.remove( points.count() - 1 ); // chop last one (will be used from next edge)
points << edgePoints;
u = e.other_vertex( u );
}
std::reverse( path.begin(), path.end() );
//foreach (int x, path)
// qDebug("e: %d", x);
std::reverse( points.begin(), points.end() );
return points;
}
int point2vertex( const QgsTracerGraph& g, const QgsPoint& pt, double epsilon = 1e-6 )
{
// TODO: use spatial index
for ( int i = 0; i < g.v.count(); ++i )
{
const QgsTracerGraph::V& v = g.v.at( i );
if ( v.pt == pt || ( fabs( v.pt.x() - pt.x() ) < epsilon && fabs( v.pt.y() - pt.y() ) < epsilon ) )
return i;
}
return -1;
}
int point2edge( const QgsTracerGraph& g, const QgsPoint& pt, int& lineVertexAfter, double epsilon = 1e-6 )
{
int vertexAfter;
for ( int i = 0; i < g.e.count(); ++i )
{
if ( g.inactiveEdges.contains( i ) )
continue; // ignore temporarily disabled edges
const QgsTracerGraph::E& e = g.e.at( i );
double dist = closest_segment( e.coords, pt, vertexAfter, epsilon );
if ( dist == 0 )
{
lineVertexAfter = vertexAfter;
return i;
}
}
return -1;
}
void split_linestring( const QgsPolyline& points, const QgsPoint& pt, int lineVertexAfter, QgsPolyline& pts1, QgsPolyline& pts2 )
{
int count1 = lineVertexAfter;
int count2 = points.count() - lineVertexAfter;
for ( int i = 0; i < count1; ++i )
pts1 << points[i];
if ( points[lineVertexAfter-1] != pt )
pts1 << pt; // repeat if not split exactly at that point
if ( pt != points[lineVertexAfter] )
pts2 << pt; // repeat if not split exactly at that point
for ( int i = 0; i < count2; ++i )
pts2 << points[i + lineVertexAfter];
}
int join_vertex_to_graph( QgsTracerGraph& g, const QgsPoint& pt )
{
// find edge where the point is
int lineVertexAfter;
int eIdx = point2edge( g, pt, lineVertexAfter );
//qDebug("e: %d", eIdx);
if ( eIdx == -1 )
return -1;
const QgsTracerGraph::E& e = g.e[eIdx];
QgsTracerGraph::V& v1 = g.v[e.v1];
QgsTracerGraph::V& v2 = g.v[e.v2];
QgsPolyline out1, out2;
split_linestring( e.coords, pt, lineVertexAfter, out1, out2 );
int vIdx = g.v.count();
int e1Idx = g.e.count();
int e2Idx = e1Idx + 1;
// prepare new vertex and edges
QgsTracerGraph::V v;
v.pt = pt;
v.edges << e1Idx << e2Idx;
QgsTracerGraph::E e1;
e1.v1 = e.v1;
e1.v2 = vIdx;
e1.coords = out1;
QgsTracerGraph::E e2;
e2.v1 = vIdx;
e2.v2 = e.v2;
e2.coords = out2;
// update edge connectivity of existing vertices
v1.edges.replace( v1.edges.indexOf( eIdx ), e1Idx );
v2.edges.replace( v2.edges.indexOf( eIdx ), e2Idx );
g.inactiveEdges << eIdx;
// add new vertex and edges to the graph
g.v.append( v );
g.e.append( e1 );
g.e.append( e2 );
g.joinedVertices++;
return vIdx;
}
int point_in_graph( QgsTracerGraph& g, const QgsPoint& pt )
{
// try to use existing vertex in the graph
int v = point2vertex( g, pt );
if ( v != -1 )
return v;
// try to add the vertex to an edge (may fail if point is not on edge)
return join_vertex_to_graph( g, pt );
}
void reset_graph( QgsTracerGraph& g )
{
// remove extra vertices and edges
g.v.resize( g.v.count() - g.joinedVertices );
g.e.resize( g.e.count() - g.joinedVertices * 2 );
g.joinedVertices = 0;
// fix vertices of deactivated edges
foreach ( int eIdx, g.inactiveEdges )
{
if ( eIdx >= g.e.count() )
continue;
const QgsTracerGraph::E& e = g.e[eIdx];
QgsTracerGraph::V& v1 = g.v[e.v1];
for ( int i = 0; i < v1.edges.count(); ++i )
{
if ( v1.edges[i] >= g.e.count() )
v1.edges.remove( i-- );
}
v1.edges << eIdx;
QgsTracerGraph::V& v2 = g.v[e.v2];
for ( int i = 0; i < v2.edges.count(); ++i )
{
if ( v2.edges[i] >= g.e.count() )
v2.edges.remove( i-- );
}
v2.edges << eIdx;
}
g.inactiveEdges.clear();
}
void extract_linework( QgsGeometry* g, QgsMultiPolyline& mpl )
{
switch ( QgsWKBTypes::flatType( g->geometry()->wkbType() ) )
{
case QgsWKBTypes::LineString:
mpl << g->asPolyline();
break;
case QgsWKBTypes::Polygon:
foreach ( const QgsPolyline& ring, g->asPolygon() )
mpl << ring;
break;
case QgsWKBTypes::MultiLineString:
foreach ( const QgsPolyline& linestring, g->asMultiPolyline() )
mpl << linestring;
break;
case QgsWKBTypes::MultiPolygon:
foreach ( const QgsPolygon& polygon, g->asMultiPolygon() )
foreach ( const QgsPolyline& ring, polygon )
mpl << ring;
break;
default:
break; // unkown type - do nothing
}
}
// -------------
QgsTracer::QgsTracer()
: mGraph( 0 )
, mMaxFeatureCount( 0 )
{
}
bool QgsTracer::initGraph()
{
if ( mGraph )
return true; // already initialized
QgsFeature f;
QgsMultiPolyline mpl;
// extract linestrings
// TODO: use QgsPointLocator as a source for the linework
QTime t1, t2, t2a, t3;
t1.start();
int featuresCounted = 0;
foreach ( QgsVectorLayer* vl, mLayers )
{
QgsCoordinateTransform ct( vl->crs(), mCRS );
QgsFeatureRequest request;
request.setSubsetOfAttributes( QgsAttributeList() );
if ( !mExtent.isEmpty() )
request.setFilterRect( ct.transformBoundingBox( mExtent, QgsCoordinateTransform::ReverseTransform ) );
QgsFeatureIterator fi = vl->getFeatures( request );
while ( fi.nextFeature( f ) )
{
if ( !f.geometry() )
continue;
if ( !ct.isShortCircuited() )
{
try
{
f.geometry()->transform( ct );
}
catch ( QgsCsException& )
{
continue; // ignore if the transform failed
}
}
extract_linework( f.geometry(), mpl );
++featuresCounted;
if ( mMaxFeatureCount != 0 && featuresCounted >= mMaxFeatureCount )
return false;
}
}
int timeExtract = t1.elapsed();
// resolve intersections
t2.start();
#if 0
// without noding - if data are known to be noded beforehand
int timeNodingCall = 0;
#else
QgsGeometry* all_geom = QgsGeometry::fromMultiPolyline( mpl );
t2a.start();
GEOSGeometry* all_noded = GEOSNode_r( QgsGeometry::getGEOSHandler(), all_geom->asGeos() );
int timeNodingCall = t2a.elapsed();
QgsGeometry* noded = new QgsGeometry;
noded->fromGeos( all_noded );
delete all_geom;
mpl = noded->asMultiPolyline();
delete noded;
#endif
int timeNoding = t2.elapsed();
t3.start();
mGraph = make_graph( mpl );
int timeMake = t3.elapsed();
Q_UNUSED( timeExtract );
Q_UNUSED( timeNoding );
Q_UNUSED( timeNodingCall );
Q_UNUSED( timeMake );
QgsDebugMsg( QString( "tracer extract %1 ms, noding %2 ms (call %3 ms), make %4 ms" )
.arg( timeExtract ).arg( timeNoding ).arg( timeNodingCall ).arg( timeMake ) );
return true;
}
QgsTracer::~QgsTracer()
{
invalidateGraph();
}
void QgsTracer::setLayers( const QList<QgsVectorLayer*>& layers )
{
if ( mLayers == layers )
return;
foreach ( QgsVectorLayer* layer, mLayers )
{
disconnect( layer, SIGNAL( featureAdded( QgsFeatureId ) ), this, SLOT( onFeatureAdded( QgsFeatureId ) ) );
disconnect( layer, SIGNAL( featureDeleted( QgsFeatureId ) ), this, SLOT( onFeatureDeleted( QgsFeatureId ) ) );
disconnect( layer, SIGNAL( geometryChanged( QgsFeatureId, QgsGeometry& ) ), this, SLOT( onGeometryChanged( QgsFeatureId, QgsGeometry& ) ) );
}
mLayers = layers;
foreach ( QgsVectorLayer* layer, mLayers )
{
connect( layer, SIGNAL( featureAdded( QgsFeatureId ) ), this, SLOT( onFeatureAdded( QgsFeatureId ) ) );
connect( layer, SIGNAL( featureDeleted( QgsFeatureId ) ), this, SLOT( onFeatureDeleted( QgsFeatureId ) ) );
connect( layer, SIGNAL( geometryChanged( QgsFeatureId, QgsGeometry& ) ), this, SLOT( onGeometryChanged( QgsFeatureId, QgsGeometry& ) ) );
}
invalidateGraph();
}
void QgsTracer::setDestinationCrs( const QgsCoordinateReferenceSystem& crs )
{
if ( mCRS == crs )
return;
mCRS = crs;
invalidateGraph();
}
void QgsTracer::setExtent( const QgsRectangle& extent )
{
if ( mExtent == extent )
return;
mExtent = extent;
invalidateGraph();
}
bool QgsTracer::init()
{
if ( mGraph )
return true;
// configuration from derived class?
configure();
return initGraph();
}
void QgsTracer::invalidateGraph()
{
delete mGraph;
mGraph = 0;
}
void QgsTracer::onFeatureAdded( QgsFeatureId fid )
{
Q_UNUSED( fid );
invalidateGraph();
}
void QgsTracer::onFeatureDeleted( QgsFeatureId fid )
{
Q_UNUSED( fid );
invalidateGraph();
}
void QgsTracer::onGeometryChanged( QgsFeatureId fid, QgsGeometry& geom )
{
Q_UNUSED( fid );
Q_UNUSED( geom );
invalidateGraph();
}
QVector<QgsPoint> QgsTracer::findShortestPath( const QgsPoint& p1, const QgsPoint& p2, PathError* error )
{
init(); // does nothing if the graph exists already
if ( !mGraph )
{
if ( error ) *error = ErrTooManyFeatures;
return QVector<QgsPoint>();
}
QTime t;
t.start();
int v1 = point_in_graph( *mGraph, p1 );
int v2 = point_in_graph( *mGraph, p2 );
int tPrep = t.elapsed();
if ( v1 == -1 )
{
if ( error ) *error = ErrPoint1;
return QVector<QgsPoint>();
}
if ( v2 == -1 )
{
if ( error ) *error = ErrPoint2;
return QVector<QgsPoint>();
}
QTime t2;
t2.start();
QgsPolyline points = shortest_path( *mGraph, v1, v2 );
int tPath = t2.elapsed();
Q_UNUSED( tPrep );
Q_UNUSED( tPath );
QgsDebugMsg( QString( "path timing: prep %1 ms, path %2 ms" ).arg( tPrep ).arg( tPath ) );
reset_graph( *mGraph );
if ( error )
*error = points.isEmpty() ? ErrNoPath : ErrNone;
return points;
}
bool QgsTracer::isPointSnapped( const QgsPoint& pt )
{
init(); // does nothing if the graph exists already
if ( !mGraph )
return false;
if ( point2vertex( *mGraph, pt ) != -1 )
return true;
int lineVertexAfter;
int e = point2edge( *mGraph, pt, lineVertexAfter );
return e != -1;
}

125
src/core/qgstracer.h Normal file
View File

@ -0,0 +1,125 @@
/***************************************************************************
qgstracer.h
--------------------------------------
Date : January 2016
Copyright : (C) 2016 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#ifndef QGSTRACER_H
#define QGSTRACER_H
class QgsVectorLayer;
#include <QSet>
#include <QVector>
#include "qgscoordinatereferencesystem.h"
#include "qgsfeature.h"
#include "qgspoint.h"
#include "qgsrectangle.h"
struct QgsTracerGraph;
/** \ingroup core
* Utility class that construct a planar graph from the input vector
* layers and provides shortest path search for tracing of existing
* features.
*
* @note added in QGIS 2.14
*/
class CORE_EXPORT QgsTracer : public QObject
{
Q_OBJECT
public:
QgsTracer();
~QgsTracer();
//! Get layers used for tracing
QList<QgsVectorLayer*> layers() const { return mLayers; }
//! Set layers used for tracing
void setLayers( const QList<QgsVectorLayer*>& layers );
//! Get CRS used for tracing
QgsCoordinateReferenceSystem destinationCrs() const { return mCRS; }
//! Set CRS used for tracing
void setDestinationCrs( const QgsCoordinateReferenceSystem& crs );
//! Get extent to which graph's features will be limited (empty extent means no limit)
QgsRectangle extent() const { return mExtent; }
//! Set extent to which graph's features will be limited (empty extent means no limit)
void setExtent( const QgsRectangle& extent );
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
int maxFeatureCount() const { return mMaxFeatureCount; }
//! Get maximum possible number of features in graph. If the number is exceeded, graph is not created.
void setMaxFeatureCount( int count ) { mMaxFeatureCount = count; }
//! Build the internal data structures. This may take some time
//! depending on how big the input layers are. It is not necessary
//! to call this method explicitly - it will be called by findShortestPath()
//! if necessary.
bool init();
//! Whether the internal data structures have been initialized
bool isInitialized() const { return mGraph != nullptr; }
//! Possible errors that may happen when calling findShortestPath()
enum PathError
{
ErrNone, //!< No error
ErrTooManyFeatures, //!< Max feature count treshold was reached while reading features
ErrPoint1, //!< Start point cannot be joined to the graph
ErrPoint2, //!< End point cannot be joined to the graph
ErrNoPath, //!< Points are not connected in the graph
};
//! Given two points, find the shortest path and return points on the way.
//! The optional "error" argument may receive error code (PathError enum) if it is not null
//! @return array of points - trace of linestrings of other features (empty array one error)
QVector<QgsPoint> findShortestPath( const QgsPoint& p1, const QgsPoint& p2, PathError* error = nullptr );
//! Find out whether the point is snapped to a vertex or edge (i.e. it can be used for tracing start/stop)
bool isPointSnapped( const QgsPoint& pt );
protected:
//! Allows derived classes to setup the settings just before the tracer is initialized.
//! This allows the configuration to be set in a lazy way only when it is really necessary.
//! Default implementation does nothing.
virtual void configure() {}
protected slots:
//! Destroy the existing graph structure if any (de-initialize)
void invalidateGraph();
private:
bool initGraph();
private slots:
void onFeatureAdded( QgsFeatureId fid );
void onFeatureDeleted( QgsFeatureId fid );
void onGeometryChanged( QgsFeatureId fid, QgsGeometry& geom );
private:
//! Graph data structure for path searching
QgsTracerGraph* mGraph;
//! Input layers for the graph building
QList<QgsVectorLayer*> mLayers;
//! Destination CRS in which graph is built and tracing done
QgsCoordinateReferenceSystem mCRS;
//! Extent for graph building (empty extent means no limit)
QgsRectangle mExtent;
//! Limit of how many features can be in the graph (0 means no limit).
//! This is to avoid possibly long graph preparation for complicated layers
int mMaxFeatureCount;
};
#endif // QGSTRACER_H

View File

@ -217,6 +217,7 @@ SET(QGIS_GUI_SRCS
qgsmapcanvasmap.cpp
qgsmapcanvassnapper.cpp
qgsmapcanvassnappingutils.cpp
qgsmapcanvastracer.cpp
qgsmaplayeractionregistry.cpp
qgsmaplayercombobox.cpp
qgsmaplayermodel.cpp
@ -351,6 +352,7 @@ SET(QGIS_GUI_MOC_HDRS
qgsmanageconnectionsdialog.h
qgsmapcanvas.h
qgsmapcanvassnappingutils.h
qgsmapcanvastracer.h
qgsmaplayeractionregistry.h
qgsmaplayercombobox.h
qgsmaplayermodel.h
@ -566,6 +568,7 @@ SET(QGIS_GUI_HDRS
qgsmapcanvasmap.h
qgsmapcanvassnapper.h
qgsmapcanvassnappingutils.h
qgsmapcanvastracer.h
qgsmaptip.h
qgsmapmouseevent.h
qgsnumericsortlistviewitem.h

View File

@ -0,0 +1,131 @@
#include "qgsmapcanvastracer.h"
#include "qgsapplication.h"
#include "qgsmapcanvas.h"
#include "qgsmaplayerregistry.h"
#include "qgsmessagebar.h"
#include "qgsmessagebaritem.h"
#include "qgssnappingutils.h"
#include "qgsvectorlayer.h"
#include <QAction>
QHash<QgsMapCanvas*, QgsMapCanvasTracer*> QgsMapCanvasTracer::sTracers;
QgsMapCanvasTracer::QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar )
: mCanvas( canvas )
, mMessageBar( messageBar )
, mLastMessage( nullptr )
{
sTracers.insert( canvas, this );
// when things change we just invalidate the graph - and set up new parameters again only when necessary
connect( canvas, SIGNAL( destinationCrsChanged() ), this, SLOT( invalidateGraph() ) );
connect( canvas, SIGNAL( layersChanged() ), this, SLOT( invalidateGraph() ) );
connect( canvas, SIGNAL( extentsChanged() ), this, SLOT( invalidateGraph() ) );
connect( canvas, SIGNAL( currentLayerChanged( QgsMapLayer* ) ), this, SLOT( onCurrentLayerChanged() ) );
connect( canvas->snappingUtils(), SIGNAL( configChanged() ), this, SLOT( invalidateGraph() ) );
mActionEnableTracing = new QAction( QIcon( QgsApplication::getThemeIcon( "/mActionTracing.png" ) ), tr( "Enable Tracing" ), this );
mActionEnableTracing->setShortcut( Qt::Key_T );
mActionEnableTracing->setCheckable( true );
// arbitrarily chosen limit that should allow for fairly fast initialization
// of the underlying graph structure
setMaxFeatureCount( QSettings().value( "/qgis/digitizing/tracing_max_feature_count", 10000 ).toInt() );
}
QgsMapCanvasTracer::~QgsMapCanvasTracer()
{
sTracers.remove( mCanvas );
}
QgsMapCanvasTracer* QgsMapCanvasTracer::tracerForCanvas( QgsMapCanvas* canvas )
{
return sTracers.value( canvas, 0 );
}
void QgsMapCanvasTracer::reportError( QgsTracer::PathError err, bool addingVertex )
{
if ( !mMessageBar )
return;
// remove previous message (if any)
mMessageBar->popWidget( mLastMessage );
mLastMessage = nullptr;
QString message;
switch ( err )
{
case ErrTooManyFeatures:
message = tr( "Disabled - there are too many features displayed. Try zooming in or disable some layers." );
break;
case ErrPoint1:
message = tr( "The start point needs to be snapped and in the visible map view" );
break;
case ErrPoint2:
if ( addingVertex )
message = tr( "The end point needs to be snapped" );
break;
case ErrNoPath:
if ( addingVertex )
message = tr( "Endpoints are not connected" );
break;
case ErrNone:
default:
break;
}
if ( message.isEmpty() )
return;
mLastMessage = new QgsMessageBarItem( tr( "Tracing" ), message, QgsMessageBar::WARNING,
QSettings().value( "/qgis/messageTimeout", 5 ).toInt() );
mMessageBar->pushItem( mLastMessage );
}
void QgsMapCanvasTracer::configure()
{
setDestinationCrs( mCanvas->mapSettings().destinationCrs() );
setExtent( mCanvas->extent() );
QList<QgsVectorLayer*> layers;
QStringList visibleLayerIds = mCanvas->mapSettings().layers();
switch ( mCanvas->snappingUtils()->snapToMapMode() )
{
default:
case QgsSnappingUtils::SnapCurrentLayer:
{
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( mCanvas->currentLayer() );
if ( vl && visibleLayerIds.contains( vl->id() ) )
layers << vl;
}
break;
case QgsSnappingUtils::SnapAllLayers:
foreach ( const QString& layerId, visibleLayerIds )
{
QgsVectorLayer* vl = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( layerId ) );
if ( vl )
layers << vl;
}
break;
case QgsSnappingUtils::SnapAdvanced:
foreach ( const QgsSnappingUtils::LayerConfig& cfg, mCanvas->snappingUtils()->layers() )
{
if ( visibleLayerIds.contains( cfg.layer->id() ) )
layers << cfg.layer;
}
break;
}
setLayers( layers );
}
void QgsMapCanvasTracer::onCurrentLayerChanged()
{
// no need to bother if we are not snapping
if ( mCanvas->snappingUtils()->snapToMapMode() == QgsSnappingUtils::SnapCurrentLayer )
invalidateGraph();
}

View File

@ -0,0 +1,60 @@
#ifndef QGSMAPCANVASTRACER_H
#define QGSMAPCANVASTRACER_H
#include "qgstracer.h"
class QAction;
class QgsMapCanvas;
class QgsMessageBar;
class QgsMessageBarItem;
/** \ingroup gui
* Extension of QgsTracer that provides extra functionality:
* - automatic updates of own configuration based on canvas settings
* - reporting of issues to the user via message bar
*
* A simple registry of tracer instances associated to map canvas instances
* is kept for convenience. (Map tools do not need to create their local
* tracer instances and map canvas API is not "polluted" by this optional
* functionality).
*
* @note added in QGIS 2.14
*/
class GUI_EXPORT QgsMapCanvasTracer : public QgsTracer
{
Q_OBJECT
public:
//! Create tracer associated with a particular map canvas, optionally message bar for reporting
explicit QgsMapCanvasTracer( QgsMapCanvas* canvas, QgsMessageBar* messageBar = 0 );
~QgsMapCanvasTracer();
//! Access to action that user may use to toggle tracing on/off
QAction* actionEnableTracing() { return mActionEnableTracing; }
//! Retrieve instance of this class associated with given canvas (if any).
//! The class keeps a simple registry of tracers associated with map canvas
//! instances for easier access to the common tracer by various map tools
static QgsMapCanvasTracer* tracerForCanvas( QgsMapCanvas* canvas );
//! Report a path finding error to the user
void reportError( PathError err, bool addingVertex );
protected:
//! Sets configuration from current snapping settings and canvas settings
virtual void configure();
private slots:
void onCurrentLayerChanged();
private:
QgsMapCanvas* mCanvas;
QgsMessageBar* mMessageBar;
QgsMessageBarItem* mLastMessage;
QAction* mActionEnableTracing;
static QHash<QgsMapCanvas*, QgsMapCanvasTracer*> sTracers;
};
#endif // QGSMAPCANVASTRACER_H

View File

@ -21,6 +21,7 @@
#include "qgslinestringv2.h"
#include "qgslogger.h"
#include "qgsmapcanvas.h"
#include "qgsmapcanvastracer.h"
#include "qgsmapmouseevent.h"
#include "qgsmaprenderer.h"
#include "qgspolygonv2.h"
@ -134,6 +135,124 @@ void QgsMapToolCapture::currentLayerChanged( QgsMapLayer *layer )
}
}
bool QgsMapToolCapture::tracingEnabled()
{
QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
return tracer && tracer->actionEnableTracing()->isChecked();
}
QgsPoint QgsMapToolCapture::tracingStartPoint()
{
try
{
QgsMapLayer* layer = mCanvas->currentLayer();
if ( !layer )
return QgsPoint();
QgsPointV2 v = mCaptureCurve.endPoint();
return toMapCoordinates( layer, QgsPoint( v.x(), v.y() ) );
}
catch ( QgsCsException & )
{
QgsDebugMsg( "transformation to layer coordinate failed" );
return QgsPoint();
}
}
void QgsMapToolCapture::tracingMouseMove( QgsMapMouseEvent* e )
{
if ( !e->isSnapped() )
return;
QgsPoint pt0 = tracingStartPoint();
if ( pt0 == QgsPoint() )
return;
QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
if ( !tracer )
return; // this should not happen!
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
QgsTracer::PathError err;
QVector<QgsPoint> points = tracer->findShortestPath( pt0, e->mapPoint(), &err );
if ( points.isEmpty() )
{
tracer->reportError( err, false );
return;
}
if ( mCaptureMode == CapturePolygon )
mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
// update rubberband
for ( int i = 0; i < points.count(); ++i )
mTempRubberBand->addPoint( points.at( i ), i == points.count() - 1 );
}
bool QgsMapToolCapture::tracingAddVertex( const QgsPoint& point )
{
QgsMapCanvasTracer* tracer = QgsMapCanvasTracer::tracerForCanvas( mCanvas );
if ( !tracer )
return false; // this should not happen!
if ( mCaptureCurve.numPoints() == 0 )
{
if ( !tracer->init() )
{
tracer->reportError( QgsTracer::ErrTooManyFeatures, true );
return false;
}
// only accept first point if it is snapped to the graph (to vertex or edge)
bool res = tracer->isPointSnapped( point );
if ( res )
{
QgsPoint layerPoint;
nextPoint( point, layerPoint ); // assuming the transform went fine earlier
mRubberBand->addPoint( point );
mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
}
return res;
}
QgsPoint pt0 = tracingStartPoint();
if ( pt0 == QgsPoint() )
return false;
QgsTracer::PathError err;
QVector<QgsPoint> points = tracer->findShortestPath( pt0, point, &err );
if ( points.isEmpty() )
{
tracer->reportError( err, true );
return false; // ignore the vertex - can't find path to the end point!
}
// transform points
QList<QgsPointV2> layerPoints;
QgsPoint lp; // in layer coords
for ( int i = 1; i < points.count(); ++i )
{
if ( nextPoint( points[i], lp ) != 0 )
return false;
layerPoints << QgsPointV2( lp.x(), lp.y() );
}
for ( int i = 1; i < points.count(); ++i )
{
if ( points[i] == points[i-1] )
continue; // avoid duplicate vertices if there are any
mRubberBand->addPoint( points[i], i == points.count() - 1 );
mCaptureCurve.addVertex( layerPoints[i-1] );
}
return true;
}
void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent * e )
{
QgsMapToolAdvancedDigitizing::cadCanvasMoveEvent( e );
@ -165,9 +284,30 @@ void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent * e )
mTempRubberBand->addPoint( point );
}
if ( mCaptureMode != CapturePoint && mTempRubberBand && mCapturing )
{
mTempRubberBand->movePoint( point );
if ( tracingEnabled() && mCaptureCurve.numPoints() != 0 )
{
tracingMouseMove( e );
}
else
{
if ( mCaptureCurve.numPoints() > 0 &&
(( mCaptureMode == CaptureLine && mTempRubberBand->numberOfVertices() != 2 ) ||
( mCaptureMode == CapturePolygon && mTempRubberBand->numberOfVertices() != 3 ) ) )
{
// fix temporary rubber band after tracing which may have added multiple points
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
if ( mCaptureMode == CapturePolygon )
mTempRubberBand->addPoint( *mRubberBand->getPoint( 0, 0 ), false );
QgsPointV2 pt = mCaptureCurve.endPoint();
mTempRubberBand->addPoint( QgsPoint( pt.x(), pt.y() ) );
mTempRubberBand->addPoint( point );
}
else
mTempRubberBand->movePoint( point );
}
}
} // mouseMoveEvent
@ -220,8 +360,6 @@ int QgsMapToolCapture::addVertex( const QgsPoint& point )
{
mRubberBand = createRubberBand( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
}
mRubberBand->addPoint( point );
mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
if ( !mTempRubberBand )
{
@ -231,6 +369,20 @@ int QgsMapToolCapture::addVertex( const QgsPoint& point )
{
mTempRubberBand->reset( mCaptureMode == CapturePolygon ? QGis::Polygon : QGis::Line );
}
if ( tracingEnabled() )
{
bool res = tracingAddVertex( point );
if ( !res )
return 1; // early exit if the point cannot be accepted
}
else
{
// ordinary digitizing
mRubberBand->addPoint( point );
mCaptureCurve.addVertex( QgsPointV2( layerPoint.x(), layerPoint.y() ) );
}
if ( mCaptureMode == CaptureLine )
{
mTempRubberBand->addPoint( point );

View File

@ -140,6 +140,16 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing
*/
void closePolygon();
private:
//! whether tracing has been requested by the user
bool tracingEnabled();
//! first point that will be used as a start of the trace
QgsPoint tracingStartPoint();
//! handle of mouse movement when tracing enabled and capturing has started
void tracingMouseMove( QgsMapMouseEvent* e );
//! handle of addition of clicked point (with the rest of the trace) when tracing enabled
bool tracingAddVertex( const QgsPoint& point );
private:
/** Flag to indicate a map canvas capture operation is taking place */
bool mCapturing;

View File

@ -170,6 +170,7 @@ ADD_QGIS_TEST(stringutilstest testqgsstringutils.cpp)
ADD_QGIS_TEST(stylev2test testqgsstylev2.cpp)
ADD_QGIS_TEST(svgmarkertest testqgssvgmarker.cpp)
ADD_QGIS_TEST(symbolv2test testqgssymbolv2.cpp)
ADD_QGIS_TEST(tracertest testqgstracer.cpp)
#for some obscure reason calling this test "fontutils" kills the build on Ubuntu 15.10
ADD_QGIS_TEST(typographicstylingutils testqgsfontutils.cpp)
ADD_QGIS_TEST(vectordataprovidertest testqgsvectordataprovider.cpp)

View File

@ -0,0 +1,323 @@
/***************************************************************************
testqgslayertree.cpp
--------------------------------------
Date : January 2016
Copyright : (C) 2016 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include <QtTest/QtTest>
#include <qgsapplication.h>
#include <qgsgeometry.h>
#include <qgsmaplayerregistry.h>
#include <qgstracer.h>
#include <qgsvectorlayer.h>
class TestQgsTracer : public QObject
{
Q_OBJECT
public:
private slots:
void initTestCase();
void cleanupTestCase();
void testSimple();
void testPolygon();
void testButterfly();
void testLayerUpdates();
void testExtent();
void testReprojection();
private:
};
namespace QTest
{
template<>
char* toString( const QgsPoint& point )
{
QByteArray ba = "QgsPoint(" + QByteArray::number( point.x() ) +
", " + QByteArray::number( point.y() ) + ")";
return qstrdup( ba.data() );
}
}
static QgsFeature make_feature( const QString& wkt )
{
QgsFeature f;
f.setGeometry( QgsGeometry::fromWkt( wkt ) );
return f;
}
static QgsVectorLayer* make_layer( const QStringList& wkts )
{
QgsVectorLayer* vl = new QgsVectorLayer( "LineString", "x", "memory" );
Q_ASSERT( vl->isValid() );
vl->startEditing();
foreach ( const QString& wkt, wkts )
{
QgsFeature f( make_feature( wkt ) );
vl->addFeature( f, false );
}
vl->commitChanges();
return vl;
}
void print_shortest_path( QgsTracer& tracer, const QgsPoint& p1, const QgsPoint& p2 )
{
qDebug( "from (%f,%f) to (%f,%f)", p1.x(), p1.y(), p2.x(), p2.y() );
QVector<QgsPoint> points = tracer.findShortestPath( p1, p2 );
if ( points.isEmpty() )
qDebug( "no path!" );
foreach ( QgsPoint p, points )
qDebug( "p: %f %f", p.x(), p.y() );
}
void TestQgsTracer::initTestCase()
{
QgsApplication::init();
QgsApplication::initQgis();
}
void TestQgsTracer::cleanupTestCase()
{
QgsApplication::exitQgis();
}
void TestQgsTracer::testSimple()
{
QStringList wkts;
wkts << "LINESTRING(0 0, 0 10)"
<< "LINESTRING(0 0, 10 0)"
<< "LINESTRING(0 10, 20 10)"
<< "LINESTRING(10 0, 20 10)";
/* This shape - nearly a square (one side is shifted to have exactly one shortest
* path between corners):
* 0,10 +----+ 20,10
* | /
* 0,0 +--+ 10,0
*/
QgsVectorLayer* vl = make_layer( wkts );
QgsTracer tracer;
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 20, 10 ) );
QCOMPARE( points1.count(), 3 );
QCOMPARE( points1[0], QgsPoint( 0, 0 ) );
QCOMPARE( points1[1], QgsPoint( 10, 0 ) );
QCOMPARE( points1[2], QgsPoint( 20, 10 ) );
// one joined point
QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 5, 10 ), QgsPoint( 0, 0 ) );
QCOMPARE( points2.count(), 3 );
QCOMPARE( points2[0], QgsPoint( 5, 10 ) );
QCOMPARE( points2[1], QgsPoint( 0, 10 ) );
QCOMPARE( points2[2], QgsPoint( 0, 0 ) );
// two joined points
QgsPolyline points3 = tracer.findShortestPath( QgsPoint( 0, 1 ), QgsPoint( 11, 1 ) );
QCOMPARE( points3.count(), 4 );
QCOMPARE( points3[0], QgsPoint( 0, 1 ) );
QCOMPARE( points3[1], QgsPoint( 0, 0 ) );
QCOMPARE( points3[2], QgsPoint( 10, 0 ) );
QCOMPARE( points3[3], QgsPoint( 11, 1 ) );
// two joined points on one line
QgsPolyline points4 = tracer.findShortestPath( QgsPoint( 11, 1 ), QgsPoint( 19, 9 ) );
QCOMPARE( points4.count(), 2 );
QCOMPARE( points4[0], QgsPoint( 11, 1 ) );
QCOMPARE( points4[1], QgsPoint( 19, 9 ) );
// no path to (1,1)
QgsPolyline points5 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 1, 1 ) );
QCOMPARE( points5.count(), 0 );
delete vl;
}
void TestQgsTracer::testPolygon()
{
// the same shape as in testSimple() but with just one polygon ring
// to check extraction from polygons work + routing along one ring works
QStringList wkts;
wkts << "POLYGON((0 0, 0 10, 20 10, 10 0, 0 0))";
QgsVectorLayer* vl = make_layer( wkts );
QgsTracer tracer;
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
QgsPolyline points = tracer.findShortestPath( QgsPoint( 1, 0 ), QgsPoint( 0, 1 ) );
QCOMPARE( points.count(), 3 );
QCOMPARE( points[0], QgsPoint( 1, 0 ) );
QCOMPARE( points[1], QgsPoint( 0, 0 ) );
QCOMPARE( points[2], QgsPoint( 0, 1 ) );
delete vl;
}
void TestQgsTracer::testButterfly()
{
// checks whether tracer internally splits linestrings at intersections
QStringList wkts;
wkts << "LINESTRING(0 0, 0 10, 10 0, 10 10, 0 0)";
/* This shape (without a vertex where the linestring crosses itself):
* + + 10,10
* |\/|
* |/\|
* + +
* 0,0
*/
QgsVectorLayer* vl = make_layer( wkts );
QgsTracer tracer;
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
QgsPolyline points = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
QCOMPARE( points.count(), 3 );
QCOMPARE( points[0], QgsPoint( 0, 0 ) );
QCOMPARE( points[1], QgsPoint( 5, 5 ) );
QCOMPARE( points[2], QgsPoint( 10, 0 ) );
delete vl;
}
void TestQgsTracer::testLayerUpdates()
{
// check whether the tracer is updated on added/removed/changed features
// same shape as in testSimple()
QStringList wkts;
wkts << "LINESTRING(0 0, 0 10)"
<< "LINESTRING(0 0, 10 0)"
<< "LINESTRING(0 10, 20 10)"
<< "LINESTRING(10 0, 20 10)";
QgsVectorLayer* vl = make_layer( wkts );
QgsTracer tracer;
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
tracer.init();
QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
QCOMPARE( points1.count(), 3 );
QCOMPARE( points1[0], QgsPoint( 10, 0 ) );
QCOMPARE( points1[1], QgsPoint( 20, 10 ) );
QCOMPARE( points1[2], QgsPoint( 10, 10 ) );
vl->startEditing();
// add a shortcut
QgsFeature f( make_feature( "LINESTRING(10 0, 10 10)" ) );
vl->addFeature( f );
QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
QCOMPARE( points2.count(), 2 );
QCOMPARE( points2[0], QgsPoint( 10, 0 ) );
QCOMPARE( points2[1], QgsPoint( 10, 10 ) );
// delete the shortcut
vl->deleteFeature( f.id() );
QgsPolyline points3 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
QCOMPARE( points3.count(), 3 );
QCOMPARE( points3[0], QgsPoint( 10, 0 ) );
QCOMPARE( points3[1], QgsPoint( 20, 10 ) );
QCOMPARE( points3[2], QgsPoint( 10, 10 ) );
// make the shortcut again from a different feature
QgsGeometry* g = QgsGeometry::fromWkt( "LINESTRING(10 0, 10 10)" );
vl->changeGeometry( 2, g ); // change bottom line (second item in wkts)
delete g;
QgsPolyline points4 = tracer.findShortestPath( QgsPoint( 10, 0 ), QgsPoint( 10, 10 ) );
QCOMPARE( points4.count(), 2 );
QCOMPARE( points4[0], QgsPoint( 10, 0 ) );
QCOMPARE( points4[1], QgsPoint( 10, 10 ) );
QgsPolyline points5 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
QCOMPARE( points5.count(), 4 );
QCOMPARE( points5[0], QgsPoint( 0, 0 ) );
QCOMPARE( points5[1], QgsPoint( 0, 10 ) );
QCOMPARE( points5[2], QgsPoint( 10, 10 ) );
QCOMPARE( points5[3], QgsPoint( 10, 0 ) );
vl->rollBack();
delete vl;
}
void TestQgsTracer::testExtent()
{
// check whether the tracer correctly handles the extent limitation
// same shape as in testSimple()
QStringList wkts;
wkts << "LINESTRING(0 0, 0 10)"
<< "LINESTRING(0 0, 10 0)"
<< "LINESTRING(0 10, 20 10)"
<< "LINESTRING(10 0, 20 10)";
QgsVectorLayer* vl = make_layer( wkts );
QgsTracer tracer;
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
tracer.setExtent( QgsRectangle( 0, 0, 5, 5 ) );
tracer.init();
QgsPolyline points1 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) );
QCOMPARE( points1.count(), 2 );
QCOMPARE( points1[0], QgsPoint( 0, 0 ) );
QCOMPARE( points1[1], QgsPoint( 10, 0 ) );
QgsPolyline points2 = tracer.findShortestPath( QgsPoint( 0, 0 ), QgsPoint( 20, 10 ) );
QCOMPARE( points2.count(), 0 );
}
void TestQgsTracer::testReprojection()
{
QStringList wkts;
wkts << "LINESTRING(1 0, 2 0)";
QgsVectorLayer* vl = make_layer( wkts );
QgsCoordinateReferenceSystem dstCrs( "EPSG:3857" );
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:4326" ), dstCrs );
QgsPoint p1 = ct.transform( QgsPoint( 1, 0 ) );
QgsPoint p2 = ct.transform( QgsPoint( 2, 0 ) );
QgsTracer tracer;
tracer.setLayers( QList<QgsVectorLayer*>() << vl );
tracer.setDestinationCrs( dstCrs );
tracer.init();
QgsPolyline points1 = tracer.findShortestPath( p1, p2 );
QCOMPARE( points1.count(), 2 );
}
QTEST_MAIN( TestQgsTracer )
#include "testqgstracer.moc"