2

In OpenJump there is a tool called ClosedVertexFinder which you can use to find vertices which are too close to each other.

https://github.com/ssinger/roadmatcher/blob/master/src/com/vividsolutions/jcs/qa/CloseVertexFinder.java

I would like to build a tool like this in QGIS.

Is there something that can already be used in order not to build the tool from scratch or will I have to calculate the distance between all vertices of a layer?

5
  • What are the inputs and expected output from the function ? – J. Monticolo Dec 2 '19 at 8:54
  • @J.Monticolo: inputs are line- or polygon-layer. output could be a memorylayer with the vertices which are too close to each other – markgraeflerland Dec 2 '19 at 9:06
  • Is the tool is for detect only consecutive vertices ? I think for example of a "bottleneck" polygon. – J. Monticolo Dec 6 '19 at 10:53
  • @J.Monticolo: No. the goal is to clean features which were produced by some tools (for example line offset) which produce vertices that lie too close to each other. – markgraeflerland Dec 6 '19 at 12:21
  • Can you add some input WKT (one or two features) that cover the two drawbacks you wrote in my answer comments please ? – J. Monticolo Dec 16 '19 at 14:23
5
+25

With QGIS 3.10, you can duplicate your layer (polyline or polygon), go in its symbology, choose Geometry generator with Point / Multi-Point and paste this code :

with_variable(
  'distance_threshold',
  5, --treshold to modify
  collect_geometries(
    array_filter(
      array_foreach(
        generate_series(1, num_points($geometry)),
          if(
            --condition
            distance(
              geometry_n(nodes_to_points($geometry), @element),
              geometry_n(nodes_to_points($geometry), @element + 1)
            ) >= @distance_threshold,
            --result_when_true
            Null,
            --result_when_false
            collect_geometries(
              geometry_n(nodes_to_points($geometry), @element),
              geometry_n(nodes_to_points($geometry), @element + 1)
            )
          )
      ),
      @element is not Null
    )
  )
)

You can adjust the threshold (3rd line) in layer units. You can replace it by a layer field (for example "my_treshold_layer_field") if you have different threshold according to the features.

Explanations :

  • creation of a threshold variable with_variable function > @distance_threshold
  • iterate over all vertices with generate_series for vertex 1 to last vertex (= num_points($geometry) and array_foreach. If the distance between the vertex and the next one is superior or equal to @distance_threshold, then create a multipoint geometry with the vertex and the next one, else Null value
  • filter the array last created with array_filter for keep only non null values
  • create a multipoint geometry collect_geometries for all filtered geometries.

The result is a point layer with points at vertices that are closer than the threshold.


I've created a QGIS 3 model that extract the segments shortest or equal to the input distance. The layer input can be a polygon or a polyline.

Here is the code to put into a .model3 file, for example CloseVertexFinder.model3 :

<!DOCTYPE model>
<Option type="Map">
  <Option type="Map" name="children">
    <Option type="Map" name="native:explodelines_1">
      <Option value="true" type="bool" name="active"/>
      <Option name="alg_config"/>
      <Option value="native:explodelines" type="QString" name="alg_id"/>
      <Option value="Explode lines" type="QString" name="component_description"/>
      <Option value="181" type="double" name="component_pos_x"/>
      <Option value="192" type="double" name="component_pos_y"/>
      <Option name="dependencies"/>
      <Option value="native:explodelines_1" type="QString" name="id"/>
      <Option name="outputs"/>
      <Option value="true" type="bool" name="outputs_collapsed"/>
      <Option value="true" type="bool" name="parameters_collapsed"/>
      <Option type="Map" name="params">
        <Option type="List" name="INPUT">
          <Option type="Map">
            <Option value="qgis:convertgeometrytype_1" type="QString" name="child_id"/>
            <Option value="OUTPUT" type="QString" name="output_name"/>
            <Option value="1" type="int" name="source"/>
          </Option>
        </Option>
      </Option>
    </Option>
    <Option type="Map" name="native:extractbyattribute_1">
      <Option value="true" type="bool" name="active"/>
      <Option name="alg_config"/>
      <Option value="native:extractbyattribute" type="QString" name="alg_id"/>
      <Option value="Extract by attribute" type="QString" name="component_description"/>
      <Option value="408" type="double" name="component_pos_x"/>
      <Option value="361" type="double" name="component_pos_y"/>
      <Option name="dependencies"/>
      <Option value="native:extractbyattribute_1" type="QString" name="id"/>
      <Option type="Map" name="outputs">
        <Option type="Map" name="Close vertices">
          <Option value="native:extractbyattribute_1" type="QString" name="child_id"/>
          <Option value="Close vertices" type="QString" name="component_description"/>
          <Option value="408" type="double" name="component_pos_x"/>
          <Option value="423" type="double" name="component_pos_y"/>
          <Option type="invalid" name="default_value"/>
          <Option value="false" type="bool" name="mandatory"/>
          <Option value="Close vertices" type="QString" name="name"/>
          <Option value="OUTPUT" type="QString" name="output_name"/>
        </Option>
      </Option>
      <Option value="true" type="bool" name="outputs_collapsed"/>
      <Option value="true" type="bool" name="parameters_collapsed"/>
      <Option type="Map" name="params">
        <Option type="List" name="FIELD">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="v_length" type="QString" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="INPUT">
          <Option type="Map">
            <Option value="qgis:fieldcalculator_1" type="QString" name="child_id"/>
            <Option value="OUTPUT" type="QString" name="output_name"/>
            <Option value="1" type="int" name="source"/>
          </Option>
        </Option>
        <Option type="List" name="OPERATOR">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="5" type="int" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="VALUE">
          <Option type="Map">
            <Option value="verticesdistance" type="QString" name="parameter_name"/>
            <Option value="0" type="int" name="source"/>
          </Option>
        </Option>
      </Option>
    </Option>
    <Option type="Map" name="qgis:convertgeometrytype_1">
      <Option value="true" type="bool" name="active"/>
      <Option name="alg_config"/>
      <Option value="qgis:convertgeometrytype" type="QString" name="alg_id"/>
      <Option value="Convert geometry type" type="QString" name="component_description"/>
      <Option value="177" type="double" name="component_pos_x"/>
      <Option value="100" type="double" name="component_pos_y"/>
      <Option name="dependencies"/>
      <Option value="qgis:convertgeometrytype_1" type="QString" name="id"/>
      <Option name="outputs"/>
      <Option value="true" type="bool" name="outputs_collapsed"/>
      <Option value="true" type="bool" name="parameters_collapsed"/>
      <Option type="Map" name="params">
        <Option type="List" name="INPUT">
          <Option type="Map">
            <Option value="layertoanalyse" type="QString" name="parameter_name"/>
            <Option value="0" type="int" name="source"/>
          </Option>
        </Option>
        <Option type="List" name="TYPE">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="3" type="int" name="static_value"/>
          </Option>
        </Option>
      </Option>
    </Option>
    <Option type="Map" name="qgis:fieldcalculator_1">
      <Option value="true" type="bool" name="active"/>
      <Option name="alg_config"/>
      <Option value="qgis:fieldcalculator" type="QString" name="alg_id"/>
      <Option value="Field calculator" type="QString" name="component_description"/>
      <Option value="182" type="double" name="component_pos_x"/>
      <Option value="286" type="double" name="component_pos_y"/>
      <Option name="dependencies"/>
      <Option value="qgis:fieldcalculator_1" type="QString" name="id"/>
      <Option name="outputs"/>
      <Option value="true" type="bool" name="outputs_collapsed"/>
      <Option value="true" type="bool" name="parameters_collapsed"/>
      <Option type="Map" name="params">
        <Option type="List" name="FIELD_LENGTH">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="10" type="int" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="FIELD_NAME">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="v_length" type="QString" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="FIELD_PRECISION">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="3" type="int" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="FIELD_TYPE">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="0" type="int" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="FORMULA">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="$length" type="QString" name="static_value"/>
          </Option>
        </Option>
        <Option type="List" name="INPUT">
          <Option type="Map">
            <Option value="native:explodelines_1" type="QString" name="child_id"/>
            <Option value="OUTPUT" type="QString" name="output_name"/>
            <Option value="1" type="int" name="source"/>
          </Option>
        </Option>
        <Option type="List" name="NEW_FIELD">
          <Option type="Map">
            <Option value="2" type="int" name="source"/>
            <Option value="true" type="bool" name="static_value"/>
          </Option>
        </Option>
      </Option>
    </Option>
  </Option>
  <Option name="help"/>
  <Option name="modelVariables"/>
  <Option value="Vector general" type="QString" name="model_group"/>
  <Option value="CloseVertexFinder" type="QString" name="model_name"/>
  <Option type="Map" name="parameterDefinitions">
    <Option type="Map" name="layertoanalyse">
      <Option type="List" name="data_types">
        <Option value="-1" type="int"/>
      </Option>
      <Option type="invalid" name="default"/>
      <Option value="Layer to analyze" type="QString" name="description"/>
      <Option value="0" type="int" name="flags"/>
      <Option name="metadata"/>
      <Option value="layertoanalyse" type="QString" name="name"/>
      <Option value="vector" type="QString" name="parameter_type"/>
    </Option>
    <Option type="Map" name="native:extractbyattribute_1:Close vertices">
      <Option value="true" type="bool" name="create_by_default"/>
      <Option value="-1" type="int" name="data_type"/>
      <Option type="invalid" name="default"/>
      <Option value="Close vertices" type="QString" name="description"/>
      <Option value="0" type="int" name="flags"/>
      <Option name="metadata"/>
      <Option value="native:extractbyattribute_1:Close vertices" type="QString" name="name"/>
      <Option value="sink" type="QString" name="parameter_type"/>
      <Option value="true" type="bool" name="supports_non_file_outputs"/>
    </Option>
    <Option type="Map" name="verticesdistance">
      <Option value="1" type="int" name="data_type"/>
      <Option value="0" type="QString" name="default"/>
      <Option value="Vertices distance" type="QString" name="description"/>
      <Option value="0" type="int" name="flags"/>
      <Option value="9999999999" type="double" name="max"/>
      <Option name="metadata"/>
      <Option value="0" type="double" name="min"/>
      <Option value="verticesdistance" type="QString" name="name"/>
      <Option value="number" type="QString" name="parameter_type"/>
    </Option>
  </Option>
  <Option type="Map" name="parameters">
    <Option type="Map" name="layertoanalyse">
      <Option value="layertoanalyse" type="QString" name="component_description"/>
      <Option value="175" type="double" name="component_pos_x"/>
      <Option value="37" type="double" name="component_pos_y"/>
      <Option value="layertoanalyse" type="QString" name="name"/>
    </Option>
    <Option type="Map" name="verticesdistance">
      <Option value="verticesdistance" type="QString" name="component_description"/>
      <Option value="443" type="double" name="component_pos_x"/>
      <Option value="35" type="double" name="component_pos_y"/>
      <Option value="verticesdistance" type="QString" name="name"/>
    </Option>
  </Option>
</Option>

Details of the code :

Inputs :

  1. Vector Layer : any geometry type / Mandatory
  2. Number : Float / Min: 0, Max: 9999999999, Default: 0 / Mandatory

Algorithms :

  1. Convert geometry type : input: Vector Layer (1), New geometry type: Multilinestrings
  2. Explode lines : input: 'Converted' from algorithm 'Convert geometry type' (3)
  3. Field calculator: input: 'Exploded' from algorithm 'Explode lines' (4), Result field name: v_length, Float, length: 10, precision: 3, Create a new field: Yes, formula: $length
  4. Extract by attribute: input: 'Calculated' from algorithm 'Field calculator' (5), Selection attribute: v_length, operator: <=, value using model input: Number (2), Extracted (attribute): Close vertices

The result is a line layer with only the segment parts shortest than the distance specified as parameter. If you want a point layer, you can edit the model and add a step with the Extract vertices algorithm.

6
  • 1
    @Monticolo: thanks for the answer. It's very interesting that you can use these functions in a geometry generator. But for my usecase I need a tool where I have features as output...for example a memory layer. My layers have nearly 100000 features so it's difficult to spot the single failures just through symbolisation – markgraeflerland Dec 6 '19 at 8:33
  • I understand, I will try to think about an other way. – J. Monticolo Dec 6 '19 at 8:36
  • 1
    @markgraeflerland You can run the Geometry by expression algorithm to create a new point layer with one feature for each source feature, with all its attributes and with the collected points geometry. QGIS LTR 3.4 doesn't include the collect_geometries function. – Gabriel De Luca Dec 6 '19 at 18:30
  • @markgraeflerland, I just added another solution. – J. Monticolo Dec 7 '19 at 16:22
  • @J.Monticolo: thanks for this input.I think it is an interesting idea to use a model for getting the close vertices. I just think that there are two drawbacks: 1. if your layer is not 'clean' the model will fail. I just tried it with the layer I wanted do clean from close vertices and the model fails on the first step (making multigeometries). The 2nd drawback is that splitting the lines to segments and measuring the length won't find linesegments which have a microgap between them so there start end endpoint lie too close to each other (< threshold) – markgraeflerland Dec 10 '19 at 7:28

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.