├── BezierEditing.png
├── i18n
├── bezierediting_hu.qm
├── bezierediting_ja.qm
├── bezierediting_ja.ts
└── bezierediting_hu.ts
├── bezierediting.pro
├── icon
├── unspliticon.svg
├── handle.svg
├── mCrossHair.svg
├── anchor.svg
├── handle_del.svg
├── handle_add.svg
├── undoicon.svg
├── anchor_del.svg
├── anchor_add.svg
├── drawline.svg
├── spliticon.svg
├── showhandleicon.svg
├── freehandicon.svg
└── beziericon.svg
├── resources.qrc
├── bezier.py
├── __init__.py
├── LICENSE
├── .gitignore
├── README.md
├── CLAUDE.md
├── metadata.txt
├── fitCurves.py
├── BezierMarker.py
├── bezierediting.py
├── BezierGeometry.py
└── beziereditingtool.py
/BezierEditing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tmizu23/BezierEditing/HEAD/BezierEditing.png
--------------------------------------------------------------------------------
/i18n/bezierediting_hu.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tmizu23/BezierEditing/HEAD/i18n/bezierediting_hu.qm
--------------------------------------------------------------------------------
/i18n/bezierediting_ja.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tmizu23/BezierEditing/HEAD/i18n/bezierediting_ja.qm
--------------------------------------------------------------------------------
/bezierediting.pro:
--------------------------------------------------------------------------------
1 | SOURCES = \
2 | bezier.py \
3 | bezierediting.py \
4 | beziereditingtool.py \
5 | BezierGeometry.py \
6 | BezierMarker.py \
7 | fitCurves.py \
8 | __init__.py
9 |
10 | TRANSLATIONS = \
11 | i18n/bezierediting_ja.ts \
12 | i18n/bezierediting_hu.ts \
13 |
14 |
--------------------------------------------------------------------------------
/icon/unspliticon.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/icon/handle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
--------------------------------------------------------------------------------
/icon/mCrossHair.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | icon/beziericon.svg
4 | icon/freehandicon.svg
5 | icon/spliticon.svg
6 | icon/unspliticon.svg
7 | icon/showhandleicon.svg
8 | icon/undoicon.svg
9 | icon/anchor.svg
10 | icon/anchor_add.svg
11 | icon/anchor_del.svg
12 | icon/handle.svg
13 | icon/handle_add.svg
14 | icon/handle_del.svg
15 | icon/drawline.svg
16 | icon/mCrossHair.svg
17 |
18 |
19 |
--------------------------------------------------------------------------------
/icon/anchor.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
--------------------------------------------------------------------------------
/icon/handle_del.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
--------------------------------------------------------------------------------
/icon/handle_add.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
--------------------------------------------------------------------------------
/bezier.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function
2 |
3 | from numpy import *
4 |
5 |
6 | # evaluates cubic bezier at t, return point
7 | def q(ctrlPoly, t):
8 | return (1.0 - t) ** 3 * ctrlPoly[0] + 3 * (1.0 - t) ** 2 * t * ctrlPoly[1] + 3 * (1.0 - t) * t ** 2 * ctrlPoly[
9 | 2] + t ** 3 * ctrlPoly[3]
10 |
11 |
12 | # evaluates cubic bezier first derivative at t, return point
13 | def qprime(ctrlPoly, t):
14 | return 3 * (1.0 - t) ** 2 * (ctrlPoly[1] - ctrlPoly[0]) + 6 * (1.0 - t) * t * (
15 | ctrlPoly[2] - ctrlPoly[1]) + 3 * t ** 2 * (ctrlPoly[3] - ctrlPoly[2])
16 |
17 |
18 | # evaluates cubic bezier second derivative at t, return point
19 | def qprimeprime(ctrlPoly, t):
20 | return 6 * (1.0 - t) * (ctrlPoly[2] - 2 * ctrlPoly[1] + ctrlPoly[0]) + 6 * (t) * (
21 | ctrlPoly[3] - 2 * ctrlPoly[2] + ctrlPoly[1])
22 |
--------------------------------------------------------------------------------
/icon/undoicon.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/icon/anchor_del.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
--------------------------------------------------------------------------------
/icon/anchor_add.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """"
3 | /***************************************************************************
4 | BezierEditing
5 | --------------------------------------
6 | Date : 01 05 2019
7 | Copyright : (C) 2019 Takayuki Mizutani
8 | Email : mizutani at ecoris dot co dot jp
9 | ***************************************************************************
10 | * *
11 | * This program is free software; you can redistribute it and/or modify *
12 | * it under the terms of the GNU General Public License as published by *
13 | * the Free Software Foundation; either version 2 of the License, or *
14 | * (at your option) any later version. *
15 | * *
16 | ***************************************************************************/
17 | """
18 | def classFactory(iface):
19 | from .bezierediting import BezierEditing
20 | return BezierEditing(iface)
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 BezierEditing Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/icon/drawline.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
--------------------------------------------------------------------------------
/icon/spliticon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
21 |
--------------------------------------------------------------------------------
/icon/showhandleicon.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/icon/freehandicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
35 |
--------------------------------------------------------------------------------
/icon/beziericon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
40 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | BezierEditing plugin - version 1.3.10
2 | ===================================
3 | This is a [QGIS plugin](https://plugins.qgis.org/plugins/BezierEditing/) which edits features with Bezier curves.
4 |
5 |
6 | 
7 |
8 | Install
9 | -------------
10 |
11 | You can install this plugin from QGIS menu --> plugin --> Manage and Install plugins... --> Bezier Editing
12 |
13 | Documentation
14 | -------------
15 |
16 | [English Document](https://github.com/tmizu23/BezierEditing/wiki/Document-(English)).
17 |
18 | [日本語のドキュメント](https://github.com/tmizu23/BezierEditing/wiki/%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%88%EF%BC%88Japanese%EF%BC%89).
19 |
20 |
21 | Dependent Python libraries and resources
22 | --------------------------------------------
23 |
24 | * [fitCurves](https://github.com/volkerp/fitCurves) for fitting one or more cubic Bezier curves to a polyline.
25 | * https://github.com/tmizu23/cubic_bezier_curve/blob/master/cubic_bezier_curve.ipynb
26 |
27 |
28 | Change Log
29 | --------------------------------------------
30 | Version 1.3.10
31 | - fixed an issue where installation failed in Linux environments.
32 |
33 | Version 1.3.9
34 | - added streaming mode for freehand tool (click-move-click drawing without dragging)
35 | - added context menu for freehand tool settings (Ctrl+right-click)
36 | - fixed reuseLastValues error on linux
37 |
38 | Version 1.3.8
39 | - fixed a bug where attributes disappear in the split tool.
40 |
41 | Version 1.3.7
42 | - fixed a bug where the tool button does not switch.
43 | - fixed a bug where the setting for disable_enter_attribute_values_dialog is not applied.
44 | - fixed a bug where the UseLastValue setting is not applied.
45 | - fixed a bug where the default values of the form are not applied.
46 |
47 | Version 1.3.6
48 | - added Hungarian translation contributed by @BathoryPeter
49 | - added detailed tooltip for Bezier Edit button
50 | - Rewording messages
51 | - fixed unsplit bug on mac
52 |
53 | Version 1.3.5
54 | - added support for reuse last value
55 | - fixed autofill of fid
56 |
57 | Version 1.3.4
58 | - fixed initGui() bug
59 |
60 | Version 1.3.3
61 | - added support for moving the both handles [drag with alt]
62 | - added support for fixing the first handle in adding anchor [click & drag with alt]
63 | - added support for fixing the second handle to the anchor [click & drag with shift]
64 | - added support for setting the number of interpolations
65 | - changed to show handles by default
66 |
67 |
68 | Contribution
69 | =======
70 |
71 | Translation
72 | --------------------------------------------
73 |
74 | * Open bezierediting.pro and add bezierediting_{lang}.ts to the TRANSLATION section. {lang} must be a two letter language code.
75 | * Run `pylupdate5 bezierediting.pro` which generates the translation files. On debian, you can install pylupdate `apt install pyqt5-dev-tools`.
76 | * Open the newly generated .ts in i18n directory with QtLinguist or a text editor and do the translation.
77 | * When ready, generate qm file with `lrelease bezierediting.pro`.
78 | * (optional) To test, copy the .qm file to the plugins folder in your QGIS install (on Linux _~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/BezierEditing/i18n_) and start QGIS.
79 | * Create pull request on GitHub or send the .ts file.
80 |
81 | License
82 | =======
83 |
84 | BezierEditing plugin are released under the GNU Public License (GPL) Version 2.
85 |
86 | _Copyright (c) 2019 Takayuki Mizutani_
87 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Project Overview
6 |
7 | BezierEditing is a QGIS plugin that provides advanced digitizing tools for editing geographic features using Bezier curves. The plugin allows users to create and edit vector geometries with smooth curves using anchor points and control handles, supporting both manual Bezier drawing and freehand sketching modes.
8 |
9 | ## Development Commands
10 |
11 | ### Building Resources
12 | ```bash
13 | # Compile Qt resources (icons, UI elements)
14 | pyrcc5 -o resources.py resources.qrc
15 | ```
16 |
17 | ### Translation Management
18 | ```bash
19 | # Extract translatable strings from Python sources
20 | pylupdate5 bezierediting.pro
21 |
22 | # Edit translations (requires Qt Linguist)
23 | linguist i18n/bezierediting_ja.ts
24 | linguist i18n/bezierediting_hu.ts
25 |
26 | # Compile translation files
27 | lrelease bezierediting.pro
28 | ```
29 |
30 | ## Architecture
31 |
32 | ### Core Components
33 |
34 | **BezierEditing (bezierediting.py)** - Main plugin class that:
35 | - Initializes the plugin interface with QGIS
36 | - Creates toolbar and menu items
37 | - Manages tool activation states
38 | - Handles translation loading
39 |
40 | **BezierEditingTool (beziereditingtool.py)** - Primary map tool class that:
41 | - Handles all mouse events for drawing and editing
42 | - Manages tool modes (bezier, freehand, split, unsplit)
43 | - Controls snapping behavior and smart guides
44 | - Manages feature attribute dialogs
45 | - Coordinates between BezierGeometry and BezierMarker
46 |
47 | **BezierGeometry (BezierGeometry.py)** - Geometry management class that:
48 | - Stores and manipulates anchor points and control handles
49 | - Converts between Bezier representation and QGIS geometries
50 | - Handles coordinate transformations for different CRS
51 | - Implements Bezier curve interpolation algorithms
52 | - Manages undo/redo history
53 |
54 | **BezierMarker (BezierMarker.py)** - Visual feedback class that:
55 | - Renders anchor points, control handles, and Bezier curves on the map canvas
56 | - Updates visual elements during editing operations
57 | - Manages rubber band displays for curves and guides
58 |
59 | **fitCurves.py** - Bezier curve fitting algorithms for converting polylines to smooth curves
60 |
61 | ## Key Technical Details
62 |
63 | - **Coordinate Systems**: The plugin handles CRS transformations internally, converting between layer CRS and a working CRS (EPSG:3857) for calculations when dealing with geographic coordinates
64 | - **Interpolation**: Bezier curves are interpolated with a configurable number of points (default 10) between anchors
65 | - **Attribute Handling**: Supports QGIS form configurations including "Reuse Last Values" and default value expressions
66 | - **State Management**: Uses mouse_state and editing flags to track the current editing context
67 |
68 | ## Plugin Installation Path
69 |
70 | The plugin is installed in the QGIS user profile directory:
71 | `~/.local/share/QGIS/QGIS3/profiles/default/python/plugins/BezierEditing/` (Linux)
72 | `~/Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins/BezierEditing/` (macOS)
73 | `%APPDATA%/QGIS/QGIS3/profiles/default/python/plugins/BezierEditing/` (Windows)
74 |
75 | ## Testing Considerations
76 |
77 | - The plugin requires an active QGIS instance with a vector layer loaded
78 | - Test with different geometry types (Point, LineString, Polygon)
79 | - Verify CRS handling with both projected and geographic coordinate systems
80 | - Check snapping behavior with various snap settings enabled
--------------------------------------------------------------------------------
/metadata.txt:
--------------------------------------------------------------------------------
1 | ; the next section is mandatory
2 | [general]
3 | name=Bezier Editing
4 | qgisMinimumVersion=3.20.0
5 | description=Bezier curve and freehand digitizing tools. You can also use a tablet pen.
6 | about=This plugin digitizes features using Bezier curves. You can create Bezier curve by plotting anchors and handles. You can also draw Bezier curves freehand and use a tablet pen.
7 | category=Vector
8 | version=version 1.3.10
9 | author=Takayuki Mizutani
10 | email=mizutani@ecoris.co.jp
11 | ; end of mandatory metadata
12 |
13 | ; start of optional metadata
14 | changelog=
15 | Version 1.3.10
16 | - fixed an issue where installation failed in Linux environments.
17 |
18 | Version 1.3.9
19 | - added streaming mode for freehand tool (click-move-click drawing without dragging)
20 | - added context menu for freehand tool settings (Ctrl+right-click)
21 | - fixed reuseLastValues error on linux
22 |
23 | Version 1.3.8
24 | - fixed a bug where attributes disappear in the split tool.
25 |
26 | Version 1.3.7
27 | - fixed a bug where the tool button does not switch.
28 | - fixed a bug where the setting for disable_enter_attribute_values_dialog is not applied.
29 | - fixed a bug where the UseLastValue setting is not applied.
30 | - fixed a bug where the default values of the form are not applied.
31 |
32 | Version 1.3.6
33 | - added Hungarian translation contributed by @BathoryPeter
34 | - added detailed tooltip for Bezier Edit button
35 | - Rewording messages
36 | - fixed unsplit bug on mac
37 |
38 | Version 1.3.5
39 | - added support for reuse last value
40 | - fixed autofill of fid
41 |
42 | Version 1.3.4
43 | - fixed initGui() bug
44 |
45 | Version 1.3.3
46 | - added support for moving the both handles [drag with alt]
47 | - added support for fixing the first handle in adding anchor [click & drag with alt]
48 | - added support for fixing the second handle to the anchor [click & drag with shift]
49 | - added support for setting the number of interpolations
50 | - changed to show handles by default
51 |
52 | Version 1.3.2
53 | - fixed tool buttons cannot be toggled after drawing on Mac.
54 | - fixed error on clicking with the pen tool.
55 | - fixed polygon with only two points created.
56 |
57 | Version 1.3.1
58 | - fixed warning message that ver 1.3 or higher is required for conversion.
59 | - fixed latitude exceeded limit error.
60 | - change the behavior of the guide.
61 |
62 | Version 1.3.0
63 | - support latlon map projection.
64 |
65 | Version 1.2.2
66 | - fixed unable to edit linestrig with Z M.
67 |
68 | Version 1.2.1
69 | - support editing closed polygon.
70 | - support converting to Bezier of features created by QGIS editing tools.
71 | - fixed not to display blue marker when activated.
72 |
73 | Version 1.1.1
74 | - fixed to delete anchor error with smart guide.
75 |
76 | Version 1.1.0
77 | - support smart guide
78 | - added online document in the menu.
79 |
80 | Version 1.0.2
81 | - fixed to snap to vertex and edge.
82 | - fixed to no flip for a point with right click.
83 | - fixed the release event of alt,ctrl,shift.
84 | - fixed to deactive the tool.
85 | - fixed to convert selected features to Bezier editing first.
86 |
87 | Version 1.0.1
88 | - fixed undo error if no feature.
89 | - fixed split error if there is only one feature.
90 |
91 | Version 1.0.0
92 | - released
93 |
94 | ; tags are in comma separated value format, spaces are allowed
95 | tags=digitizing,vector,bezier,freehand
96 |
97 | homepage=https://github.com/tmizu23/BezierEditing
98 | tracker=https://github.com/tmizu23/BezierEditing/issues
99 | repository=https://github.com/tmizu23/BezierEditing
100 | icon=BezierEditing.png
101 |
102 | ; experimental flag
103 | experimental=False
104 |
105 | ; deprecated flag (applies to the whole plugin and not only to the uploaded version)
106 | deprecated=False
107 |
--------------------------------------------------------------------------------
/fitCurves.py:
--------------------------------------------------------------------------------
1 | """ Python implementation of
2 | Algorithm for Automatically Fitting Digitized Curves
3 | by Philip J. Schneider
4 | "Graphics Gems", Academic Press, 1990
5 | """
6 |
7 | from __future__ import print_function
8 | from numpy import *
9 | from . import bezier
10 | from qgis.core import *
11 |
12 | # Fit one (ore more) Bezier curves to a set of points
13 | def fitCurve(points, maxError):
14 | leftTangent = normalize(points[1] - points[0])
15 | rightTangent = normalize(points[-2] - points[-1])
16 | return fitCubic(points, leftTangent, rightTangent, maxError)
17 |
18 |
19 | def fitCubic(points, leftTangent, rightTangent, error):
20 | # Use heuristic if region only has two points in it
21 | if (len(points) == 2):
22 | dist = linalg.norm(points[0] - points[1]) / 3.0
23 | bezCurve = [points[0], points[0] + leftTangent * dist, points[1] + rightTangent * dist, points[1]]
24 | return [bezCurve]
25 |
26 | # Parameterize points, and attempt to fit curve
27 | u = chordLengthParameterize(points)
28 | #return leftTangent
29 | bezCurve = generateBezier(points, u, leftTangent, rightTangent)
30 | # Find max deviation of points to fitted curve
31 | maxError, splitPoint = computeMaxError(points, bezCurve, u)
32 | if maxError < error:
33 | return [bezCurve]
34 |
35 | # # If error not too large, try some reparameterization and iteration
36 | # if maxError < error**2:
37 | # for i in range(200):
38 | # uPrime = reparameterize(bezCurve, points, u)
39 | # bezCurve = generateBezier(points, uPrime, leftTangent, rightTangent)
40 | # maxError, splitPoint = computeMaxError(points, bezCurve, uPrime)
41 | # if maxError < error:
42 | # mylog("C{}".format(i))
43 | # return [bezCurve]
44 | # u = uPrime
45 |
46 | # # Fitting failed -- split at max error point and fit recursively
47 | beziers = []
48 | centerTangent = normalize(points[splitPoint-1] - points[splitPoint+1])
49 | beziers += fitCubic(points[:splitPoint+1], leftTangent, centerTangent, error)
50 | beziers += fitCubic(points[splitPoint:], -centerTangent, rightTangent, error)
51 |
52 | return beziers
53 |
54 |
55 | def generateBezier(points, parameters, leftTangent, rightTangent):
56 | bezCurve = [points[0], None, None, points[-1]]
57 |
58 | # compute the A's
59 | A = zeros((len(parameters), 2, 2))
60 | for i, u in enumerate(parameters):
61 | A[i][0] = leftTangent * 3*(1-u)**2 * u
62 | A[i][1] = rightTangent * 3*(1-u) * u**2
63 |
64 | # Create the C and X matrices
65 | C = zeros((2, 2))
66 | X = zeros(2)
67 |
68 | for i, (point, u) in enumerate(zip(points, parameters)):
69 | C[0][0] += dot(A[i][0], A[i][0])
70 | C[0][1] += dot(A[i][0], A[i][1])
71 | C[1][0] += dot(A[i][0], A[i][1])
72 | C[1][1] += dot(A[i][1], A[i][1])
73 |
74 | tmp = point - bezier.q([points[0], points[0], points[-1], points[-1]], u)
75 |
76 | X[0] += dot(A[i][0], tmp)
77 | X[1] += dot(A[i][1], tmp)
78 |
79 | # Compute the determinants of C and X
80 | det_C0_C1 = C[0][0] * C[1][1] - C[1][0] * C[0][1]
81 | det_C0_X = C[0][0] * X[1] - C[1][0] * X[0]
82 | det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1]
83 |
84 | # Finally, derive alpha values
85 | alpha_l = 0.0 if det_C0_C1 == 0 else det_X_C1 / det_C0_C1
86 | alpha_r = 0.0 if det_C0_C1 == 0 else det_C0_X / det_C0_C1
87 |
88 | # If alpha negative, use the Wu/Barsky heuristic (see text) */
89 | # (if alpha is 0, you get coincident control points that lead to
90 | # divide by zero in any subsequent NewtonRaphsonRootFind() call. */
91 | segLength = linalg.norm(points[0] - points[-1])
92 | epsilon = 1.0e-6 * segLength
93 | if alpha_l < epsilon or alpha_r < epsilon:
94 | # fall back on standard (probably inaccurate) formula, and subdivide further if needed.
95 | bezCurve[1] = bezCurve[0] + leftTangent * (segLength / 3.0)
96 | bezCurve[2] = bezCurve[3] + rightTangent * (segLength / 3.0)
97 |
98 | else:
99 | # First and last control points of the Bezier curve are
100 | # positioned exactly at the first and last data points
101 | # Control points 1 and 2 are positioned an alpha distance out
102 | # on the tangent vectors, left and right, respectively
103 | bezCurve[1] = bezCurve[0] + leftTangent * alpha_l
104 | bezCurve[2] = bezCurve[3] + rightTangent * alpha_r
105 |
106 | return bezCurve
107 |
108 |
109 | def reparameterize(bezier, points, parameters):
110 | return [newtonRaphsonRootFind(bezier, point, u) for point, u in zip(points, parameters)]
111 |
112 |
113 | def newtonRaphsonRootFind(bez, point, u):
114 | """
115 | Newton's root finding algorithm calculates f(x)=0 by reiterating
116 | x_n+1 = x_n - f(x_n)/f'(x_n)
117 |
118 | We are trying to find curve parameter u for some point p that minimizes
119 | the distance from that point to the curve. Distance point to curve is d=q(u)-p.
120 | At minimum distance the point is perpendicular to the curve.
121 | We are solving
122 | f = q(u)-p * q'(u) = 0
123 | with
124 | f' = q'(u) * q'(u) + q(u)-p * q''(u)
125 |
126 | gives
127 | u_n+1 = u_n - |q(u_n)-p * q'(u_n)| / |q'(u_n)**2 + q(u_n)-p * q''(u_n)|
128 | """
129 | d = bezier.q(bez, u)-point
130 | numerator = (d * bezier.qprime(bez, u)).sum()
131 | denominator = (bezier.qprime(bez, u)**2 + d * bezier.qprimeprime(bez, u)).sum()
132 |
133 | if denominator == 0.0:
134 | return u
135 | else:
136 | return u - numerator/denominator
137 |
138 |
139 | def chordLengthParameterize(points):
140 | u = [0.0]
141 | for i in range(1, len(points)):
142 | u.append(u[i-1] + linalg.norm(points[i] - points[i-1]))
143 |
144 | for i, _ in enumerate(u):
145 | u[i] = u[i] / u[-1]
146 |
147 | return u
148 |
149 |
150 | def computeMaxError(points, bez, parameters):
151 | maxDist = 0.0
152 | splitPoint = len(points)/2
153 | for i, (point, u) in enumerate(zip(points, parameters)):
154 | dist = linalg.norm(bezier.q(bez, u)-point)**2
155 | if dist > maxDist:
156 | maxDist = dist
157 | splitPoint = i
158 |
159 | return maxDist, splitPoint
160 |
161 |
162 | def normalize(v):
163 | if allclose(v, array([0, 0])):
164 | v = array([0.00001, 0.00001])
165 | return v / linalg.norm(v)
166 |
167 | def mylog(msg):
168 | QgsMessageLog.logMessage(msg, 'MyPlugin', Qgis.Info)
--------------------------------------------------------------------------------
/BezierMarker.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """"
3 | /***************************************************************************
4 | BezierEditing
5 | --------------------------------------
6 | Date : 01 05 2019
7 | Copyright : (C) 2019 Takayuki Mizutani
8 | Email : mizutani at ecoris dot co dot jp
9 | ***************************************************************************
10 | * *
11 | * This program is free software; you can redistribute it and/or modify *
12 | * it under the terms of the GNU General Public License as published by *
13 | * the Free Software Foundation; either version 2 of the License, or *
14 | * (at your option) any later version. *
15 | * *
16 | ***************************************************************************/
17 | """
18 | from qgis.PyQt.QtGui import *
19 | from qgis.core import *
20 | from qgis.gui import *
21 |
22 |
23 | class BezierMarker:
24 |
25 | def __init__(self, canvas, bezier_geometry):
26 | self.canvas = canvas
27 | self.bg = bezier_geometry
28 | self.anchor_marks = [] # anchor marker list
29 | self.handle_marks = [] # handle marker list
30 | self.handle_rbls = [] # handle line list
31 |
32 | # bezier curve line
33 | self.bezier_rbl = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
34 | self.bezier_rbl.setColor(QColor(255, 0, 0, 150))
35 | self.bezier_rbl.setWidth(2)
36 |
37 | def reset(self):
38 | """
39 | reset bezier curve
40 | """
41 | self._removeAllMarker(self.anchor_marks)
42 | self._removeAllMarker(self.handle_marks)
43 | self._removeAllRubberBand(self.handle_rbls)
44 | self.anchor_marks = []
45 | self.handle_marks = []
46 | self.handle_rbls = []
47 | self.bezier_rbl.reset(QgsWkbTypes.LineGeometry)
48 |
49 | def show(self, show_handle=None):
50 | """
51 | show bezier curve and marker
52 | """
53 | self.reset()
54 | for point in self.bg.getAnchorList(revert=True):
55 | self._setAnchorHandleMarker(self.anchor_marks, len(self.anchor_marks), point)
56 | self._setAnchorHandleMarker(self.handle_marks, len(self.handle_marks), point, QColor(125, 125, 125))
57 | self._setAnchorHandleMarker(self.handle_marks, len(self.handle_marks), point, QColor(125, 125, 125))
58 | self._setHandleLine(self.handle_rbls, len(self.handle_marks), point)
59 | self._setHandleLine(self.handle_rbls, len(self.handle_marks), point)
60 | for idx, point in enumerate(self.bg.getHandleList(revert=True)):
61 | self.handle_rbls[idx].movePoint(1, point, 0)
62 | self.handle_marks[idx].setCenter(point)
63 | self._setBezierLine(self.bg.getPointList(revert=True), self.bezier_rbl)
64 |
65 | if show_handle is not None:
66 | self.show_handle(show_handle)
67 |
68 | def add_anchor(self, idx, point):
69 | """
70 | add anchor and update bezier curve
71 | """
72 | self._setAnchorHandleMarker(self.anchor_marks, idx, point)
73 | self._setAnchorHandleMarker(self.handle_marks, 2 * idx, point, QColor(125, 125, 125))
74 | self._setAnchorHandleMarker(self.handle_marks, 2 * idx, point, QColor(125, 125, 125))
75 | self._setHandleLine(self.handle_rbls, 2 * idx, point)
76 | self._setHandleLine(self.handle_rbls, 2 * idx, point)
77 | self._setBezierLine(self.bg.getPointList(revert=True), self.bezier_rbl)
78 |
79 | # アンカーを削除してベジエ曲線の表示を更新
80 | def delete_anchor(self, idx):
81 | """
82 | delete anchor and update bezier curve
83 | """
84 | self._removeMarker(self.handle_marks, 2 * idx)
85 | self._removeRubberBand(self.handle_rbls, 2 * idx)
86 | self._removeMarker(self.handle_marks, 2 * idx)
87 | self._removeRubberBand(self.handle_rbls, 2 * idx)
88 | self._removeMarker(self.anchor_marks, idx)
89 | self._setBezierLine(self.bg.getPointList(revert=True), self.bezier_rbl)
90 |
91 | # アンカーを移動してベジエ曲線の表示を更新
92 | def move_anchor(self, idx, point):
93 | """
94 | move anchor and update bezier curve
95 | """
96 | self.anchor_marks[idx].setCenter(point)
97 | self.handle_marks[idx * 2].setCenter(self.bg.getHandle(idx * 2,revert=True))
98 | self.handle_marks[idx * 2 + 1].setCenter(self.bg.getHandle(idx * 2 + 1,revert=True))
99 | self.handle_rbls[idx * 2].movePoint(0, point, 0)
100 | self.handle_rbls[idx * 2 + 1].movePoint(0, point, 0)
101 | self.handle_rbls[idx * 2].movePoint(1, self.bg.getHandle(idx * 2,revert=True), 0)
102 | self.handle_rbls[idx * 2 + 1].movePoint(1, self.bg.getHandle(idx * 2 + 1,revert=True), 0)
103 | self._setBezierLine(self.bg.getPointList(revert=True), self.bezier_rbl)
104 |
105 | def move_handle(self, idx, point):
106 | """"
107 | move handle and update bezier curve
108 | """
109 | self.handle_rbls[idx].movePoint(1, point, 0)
110 | self.handle_marks[idx].setCenter(point)
111 | self._setBezierLine(self.bg.getPointList(revert=True), self.bezier_rbl)
112 |
113 | def show_handle(self, show):
114 | """
115 | change handle visibility
116 | """
117 | if show:
118 | self._showAllMarker(self.handle_marks)
119 | self._showAllRubberBand(self.handle_rbls)
120 | else:
121 | self._hideAllMarker(self.handle_marks)
122 | self._hideAllRubberBand(self.handle_rbls)
123 | self.canvas.refresh()
124 |
125 | def _setBezierLine(self, points, rbl):
126 | rbl.reset(QgsWkbTypes.LineGeometry)
127 | for point in points:
128 | update = point is points[-1]
129 | rbl.addPoint(point, update)
130 |
131 | def _setAnchorHandleMarker(self, markers, idx, point, color=QColor(0, 0, 0)):
132 | # insert anchor or handle marker
133 | marker = QgsVertexMarker(self.canvas)
134 | marker.setIconType(QgsVertexMarker.ICON_BOX)
135 | marker.setColor(color)
136 | marker.setPenWidth(2)
137 | marker.setIconSize(5)
138 | marker.setCenter(point)
139 | marker.show()
140 | markers.insert(idx, marker)
141 | return markers
142 |
143 | def _setHandleLine(self, rbls, idx, point):
144 | rbl = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
145 | rbl.setColor(QColor(0, 0, 0))
146 | rbl.setWidth(1)
147 | rbl.addPoint(point)
148 | rbl.addPoint(point)
149 | rbls.insert(idx, rbl)
150 | return rbls
151 |
152 | def _removeMarker(self, markers, idx):
153 | m = markers[idx]
154 | self.canvas.scene().removeItem(m)
155 | del markers[idx]
156 |
157 | def _removeAllMarker(self, markers):
158 | for m in markers:
159 | self.canvas.scene().removeItem(m)
160 |
161 | def _showAllMarker(self, markers):
162 | for m in markers[1:-1]:
163 | m.show()
164 |
165 | def _hideAllMarker(self, markers):
166 | for m in markers:
167 | m.hide()
168 |
169 | def _removeRubberBand(self, rbls, index):
170 | rbl = rbls[index]
171 | self.canvas.scene().removeItem(rbl)
172 | del rbls[index]
173 |
174 | def _removeAllRubberBand(self, rbls):
175 | for rbl in rbls:
176 | self.canvas.scene().removeItem(rbl)
177 |
178 | def _showAllRubberBand(self, rbls):
179 | for rbl in rbls[1:-1]:
180 | rbl.setColor(QColor(0, 0, 0, 255))
181 |
182 | def _hideAllRubberBand(self, rbls):
183 | for rbl in rbls:
184 | rbl.setColor(QColor(0, 0, 0, 0))
185 |
--------------------------------------------------------------------------------
/bezierediting.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """"
3 | /***************************************************************************
4 | BezierEditing
5 | --------------------------------------
6 | Date : 01 05 2019
7 | Copyright : (C) 2019 Takayuki Mizutani
8 | Email : mizutani at ecoris dot co dot jp
9 | ***************************************************************************
10 | * *
11 | * This program is free software; you can redistribute it and/or modify *
12 | * it under the terms of the GNU General Public License as published by *
13 | * the Free Software Foundation; either version 2 of the License, or *
14 | * (at your option) any later version. *
15 | * *
16 | ***************************************************************************/
17 | """
18 | from qgis.PyQt.QtCore import *
19 | from qgis.PyQt.QtGui import *
20 | from qgis.PyQt.QtWidgets import *
21 | from qgis.core import *
22 | from qgis.gui import *
23 | import os
24 | import webbrowser
25 |
26 | from . import resources
27 | from .beziereditingtool import BezierEditingTool
28 |
29 |
30 | class BezierEditing(object):
31 |
32 | def __init__(self, iface):
33 | self.iface = iface
34 | self.canvas = self.iface.mapCanvas()
35 | self.active = False
36 |
37 | # setup translation
38 | if QSettings().value('locale/overrideFlag', type=bool):
39 | locale = QSettings().value('locale/userLocale')
40 | else:
41 | locale = QLocale.system().name()
42 |
43 | locale_path = os.path.join(
44 | os.path.dirname(__file__),
45 | 'i18n',
46 | 'bezierediting_{}.qm'.format(locale[0:2]))
47 |
48 | if os.path.exists(locale_path):
49 | self.translator = QTranslator()
50 | self.translator.load(locale_path)
51 | QCoreApplication.installTranslator(self.translator)
52 |
53 | def initGui(self):
54 | # Init the tool
55 | self.beziertool = BezierEditingTool(self.canvas, self.iface)
56 |
57 | # create menu for this plugin
58 | self.action = QAction(
59 | QIcon(":/plugins/BezierEditing/icon/beziericon.svg"),
60 | self.tr("Online Documentation"),
61 | self.iface.mainWindow()
62 | )
63 | # connect the action to the run method
64 | self.action.triggered.connect(self.open_browser)
65 | self.iface.addPluginToMenu(self.tr("&Bezier Editing"), self.action)
66 |
67 | # create toolbar for this plugin
68 | self.toolbar = self.iface.addToolBar(self.tr("Bezier Editing"))
69 | self.toolbar.setObjectName("bezierEditing_toolbar")
70 |
71 | # Create bezier action
72 | self.bezier_edit = QAction(QIcon(":/plugins/BezierEditing/icon/beziericon.svg"), self.tr("Bezier Edit"),
73 | self.iface.mainWindow())
74 | self.bezier_edit.setObjectName("BezierEditing_edit")
75 | self.bezier_edit.setEnabled(False)
76 | self.bezier_edit.setCheckable(True)
77 | self.bezier_edit.setText(self.tr(
78 | """Bezier Edit
79 | Click to add anchor
80 |
- click&&drag: add curved anchor (with two hanles)
81 | - click&&drag+Alt: add anchor moving only backward handle
82 | - click&&drag+Shift: add anchor without forward handle
83 | Right click to commit feature
84 | Ctrl shows guide
85 | On feature:
86 | - Right click to enter drawing mode again
87 | - Alt+click inserts anchor
88 | On anchor:
89 | - Alt+click pulls handle from anchor
90 | - Shift+click deletes anchor
91 | On handle:
92 | - Alt+drag moves both handles
93 | - Shift+click deletes handle
94 | On first anchor, right click to flip Bezier direction.
95 | Ctrl + right click shows context menu."""
96 | ))
97 | self.bezier_edit.triggered.connect(self.bezierediting)
98 | self.toolbar.addAction(self.bezier_edit)
99 |
100 | # Create freehand action
101 | self.freehand = QAction(QIcon(":/plugins/BezierEditing/icon/freehandicon.svg"), self.tr("Edit Bezier Freehand"),
102 | self.iface.mainWindow())
103 | self.freehand.setObjectName("BezierEditing_freehand")
104 | self.freehand.setEnabled(False)
105 | self.freehand.setCheckable(True)
106 | self.freehand.setText(self.tr(
107 | """Edit Bezier Freehand
108 | - Drag to draw a line
109 | - Retrace a segment and the line will be modified
110 | - Right click to commit feature / enter edit mode again"""
111 | ))
112 | self.freehand.triggered.connect(self.freehandediting)
113 | self.toolbar.addAction(self.freehand)
114 |
115 | # Create split action
116 | self.split = QAction(QIcon(":/plugins/BezierEditing/icon/spliticon.svg"), self.tr("Split Bezier Curve"),
117 | self.iface.mainWindow())
118 | self.split.setObjectName("BezierEditing_split")
119 | self.split.setEnabled(False)
120 | self.split.setCheckable(True)
121 | self.split.triggered.connect(self.spliting)
122 | self.toolbar.addAction(self.split)
123 |
124 | # Create unsplit action
125 | self.unsplit = QAction(QIcon(":/plugins/BezierEditing/icon/unspliticon.svg"), self.tr("Merge Bezier Curves"),
126 | self.iface.mainWindow())
127 | self.unsplit.setObjectName("BezierEditing_unsplit")
128 | self.unsplit.setEnabled(False)
129 | self.unsplit.setCheckable(True)
130 | self.unsplit.setText(self.tr(
131 | """Merge Bezier Curves
132 | 1. Click or click&&drag to select features
133 | 2. Right click to merge."""
134 | ))
135 | self.unsplit.triggered.connect(self.unspliting)
136 | self.toolbar.addAction(self.unsplit)
137 |
138 | # Create show anchor option
139 | self.show_handle = QAction(QIcon(":/plugins/BezierEditing/icon/showhandleicon.svg"), self.tr("Show Bezier Handles"),
140 | self.iface.mainWindow())
141 | self.show_handle.setObjectName("BezierEditing_show_handle")
142 | self.show_handle.setCheckable(True)
143 | self.show_handle.setEnabled(False)
144 | self.show_handle.setChecked(True)
145 | self.show_handle.toggled.connect(self.showhandle)
146 | self.toolbar.addAction(self.show_handle)
147 |
148 | # Create undo option
149 | self.undo = QAction(QIcon(
150 | ":/plugins/BezierEditing/icon/undoicon.svg"),
151 | self.tr("Undo"),
152 | self.iface.mainWindow()
153 | )
154 | self.undo.setObjectName("BezierEditing_undo")
155 | self.undo.setEnabled(False)
156 | self.undo.triggered.connect(self.beziertool.undo)
157 | self.toolbar.addAction(self.undo)
158 |
159 | # Connect to signals for button behaviour
160 | self.iface.layerTreeView().currentLayerChanged.connect(self.toggle)
161 | self.canvas.mapToolSet.connect(self.maptoolChanged)
162 |
163 | self.currentTool = None
164 | self.toggle()
165 |
166 | def tr(self, message):
167 | return QCoreApplication.translate('BezierEditing', message)
168 |
169 | def open_browser(self):
170 | webbrowser.open('https://github.com/tmizu23/BezierEditing/wiki')
171 |
172 | def toggleAllOff(self):
173 | self.bezier_edit.setChecked(False)
174 | self.freehand.setChecked(False)
175 | self.split.setChecked(False)
176 | self.unsplit.setChecked(False)
177 |
178 | def bezierediting(self):
179 | self.currentTool = self.beziertool
180 | self.canvas.setMapTool(self.beziertool)
181 | self.toggleAllOff()
182 | self.bezier_edit.setChecked(True)
183 | self.beziertool.mode = "bezier"
184 |
185 | def freehandediting(self):
186 | self.currentTool = self.beziertool
187 | self.canvas.setMapTool(self.beziertool)
188 | self.toggleAllOff()
189 | self.freehand.setChecked(True)
190 | self.beziertool.mode = "freehand"
191 |
192 | def spliting(self):
193 | self.currentTool = self.beziertool
194 | self.canvas.setMapTool(self.beziertool)
195 | self.toggleAllOff()
196 | self.split.setChecked(True)
197 | self.beziertool.mode = "split"
198 |
199 | def unspliting(self):
200 | self.currentTool = self.beziertool
201 | self.canvas.setMapTool(self.beziertool)
202 | self.toggleAllOff()
203 | self.unsplit.setChecked(True)
204 | self.beziertool.mode = "unsplit"
205 |
206 | def showhandle(self, checked):
207 | self.beziertool.showHandle(checked)
208 |
209 | def toggle(self):
210 | mc = self.canvas
211 | layer = mc.currentLayer()
212 | if layer is None:
213 | return
214 |
215 | if layer.isEditable() and layer.type() == QgsMapLayer.VectorLayer:
216 | self.bezier_edit.setEnabled(True)
217 | self.freehand.setEnabled(True)
218 | self.split.setEnabled(True)
219 | self.unsplit.setEnabled(True)
220 | self.show_handle.setEnabled(True)
221 | self.undo.setEnabled(True)
222 |
223 | try:
224 | layer.editingStopped.disconnect(self.toggle)
225 | except TypeError:
226 | pass
227 | layer.editingStopped.connect(self.toggle)
228 | try:
229 | layer.editingStarted.disconnect(self.toggle)
230 | except TypeError:
231 | pass
232 | else:
233 | self.bezier_edit.setEnabled(False)
234 | self.freehand.setEnabled(False)
235 | self.split.setEnabled(False)
236 | self.unsplit.setEnabled(False)
237 | self.show_handle.setEnabled(False)
238 | self.undo.setEnabled(False)
239 |
240 | if layer.type() == QgsMapLayer.VectorLayer:
241 | try:
242 | layer.editingStarted.disconnect(self.toggle)
243 | except TypeError:
244 | pass
245 | layer.editingStarted.connect(self.toggle)
246 | try:
247 | layer.editingStopped.disconnect(self.toggle)
248 | except TypeError:
249 | pass
250 |
251 | def maptoolChanged(self):
252 | self.bezier_edit.setChecked(False)
253 | self.freehand.setChecked(False)
254 | self.split.setChecked(False)
255 | self.unsplit.setChecked(False)
256 | if self.iface.mapCanvas().mapTool() != self.currentTool:
257 | self.iface.mapCanvas().unsetMapTool(self.currentTool)
258 | self.currentTool = None
259 |
260 | def unload(self):
261 | self.toolbar.removeAction(self.bezier_edit)
262 | self.toolbar.removeAction(self.freehand)
263 | self.toolbar.removeAction(self.split)
264 | self.toolbar.removeAction(self.unsplit)
265 | self.toolbar.removeAction(self.show_handle)
266 | self.toolbar.removeAction(self.undo)
267 | del self.toolbar
268 | self.iface.removePluginMenu(self.tr("&Bezier Editing"), self.action)
269 | self.iface.mapCanvas().mapToolSet.disconnect(self.maptoolChanged)
270 |
271 | def log(self, msg):
272 | QgsMessageLog.logMessage(msg, 'BezierEditing', Qgis.Info)
273 |
--------------------------------------------------------------------------------
/i18n/bezierediting_ja.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BezierEditing
6 |
7 |
8 | Online Documentation
9 | オンラインドキュメント
10 |
11 |
12 |
13 | &Bezier Editing
14 | &Bezier Editing
15 |
16 |
17 |
18 | Bezier Editing
19 | Bezier Editing
20 |
21 |
22 |
23 | Bezier Edit
24 | ベジエ曲線を編集
25 |
26 |
27 |
28 | Edit Bezier Freehand
29 | ベジエ曲線をフリーハンドで編集
30 |
31 |
32 |
33 | Split Bezier Curve
34 | ベジエ曲線を分割
35 |
36 |
37 |
38 | Merge Bezier Curves
39 | ベジエ曲線を結合
40 |
41 |
42 |
43 | Show Bezier Handles
44 | ハンドル表示を切り替え
45 |
46 |
47 |
48 | Undo
49 | アンドゥ
50 |
51 |
52 |
53 | <b>Bezier Edit</b><br><br>
54 | Click to add anchor
55 | <dd>- click&&drag: add curved anchor (with two hanles)</dd>
56 | <dd>- click&&drag+Alt: add anchor moving only backward handle</dd>
57 | <dd>- click&&drag+Shift: add anchor without forward handle</dd>
58 | Right click to commit feature<br>
59 | Ctrl shows guide<br>
60 | On feature:
61 | <dd>- Right click to enter drawing mode again</dd>
62 | <dd>- Alt+click inserts anchor</dd>
63 | On anchor:
64 | <dd>- Alt+click pulls handle from anchor</dd>
65 | <dd>- Shift+click deletes anchor</dd>
66 | On handle:
67 | <dd>- Alt+drag moves both handles</dd>
68 | <dd>- Shift+click deletes handle</dd>
69 | On first anchor, right click to flip Bezier direction.<br>
70 | Ctrl + right click shows context menu.
71 | <b>ベジエ曲線を編集</b><br><br>クリックでアンカーを追加<dd>- クリック&&ドラッグ: アンカーを追加 (両側のハンドルを移動)</dd><dd>- クリック&&ドラッグ+Shift: アンカーを追加して次のハンドルは削除</dd><dd>- クリック&&ドラッグ+Alt: アンカーを追加して次のハンドルのみ移動</dd>右クリックでフィーチャーを確定<br>Ctrlでガイドを表示<br>フィーチャー上で:<dd>- 右クリック編集の再開</dd><dd>- Alt+クリック: アンカーの挿入</dd>アンカー上で:<dd>- Alt+クリック: アンカーからハンドルを引き出す</dd><dd>- Shift+クリック: アンカーを削除</dd>ハンドル上で:<dd>- Alt+ドラッグ: 両側のハンドルを移動</dd><dd>- Shift+クリック: ハンドルを削除</dd>始点のアンカーを右クリックで始点を反対にする<br>Ctrl + 右クリックでメニューを表示
72 |
73 |
74 |
75 | <b>Edit Bezier Freehand</b><br><br>
76 | - Drag to draw a line<br>
77 | - Retrace a segment and the line will be modified<br>
78 | - Right click to commit feature / enter edit mode again
79 | <b>ベジエ曲線をフリーハンドで編集</b><br><br>- ドラッグでラインを書く<br>- ラインをなぞり直すと修正される<br>- 右クリックで編集の確定/編集の再開
80 |
81 |
82 |
83 | <b>Merge Bezier Curves</b><br><br>
84 | 1. Click or click&&drag to select features<br>
85 | 2. Right click to merge.
86 | <b>ベジエラインを結合</b><br><br>1. クリック か クリック&&ドラッグでフィーチャーを選択<br>2. 右クリックで結合
87 |
88 |
89 |
90 | BezierEditingTool
91 |
92 |
93 | No feature to split.
94 | 切断できるフィーチャーがありません。
95 |
96 |
97 |
98 | Do you want to continue editing?
99 | 編集を続けますか?
100 |
101 |
102 |
103 | Close
104 | 閉じる
105 |
106 |
107 |
108 | Can't be set while editing.
109 | 編集中は設定できません。
110 |
111 |
112 |
113 | Be careful when changing values, as Bezier curves with different numbers of interpolants will not be converted accurately.
114 | 補間点数の異なるベジェ曲線は正確に変換できないので、値を変更する場合は注意してください。
115 |
116 |
117 |
118 | Count
119 | 補間点数
120 |
121 |
122 |
123 | Warning
124 | Warning
125 |
126 |
127 |
128 | Reset editing data
129 | 編集中のデータはリセットされます
130 |
131 |
132 |
133 | Geometry type is different
134 | Geometry type is different
135 |
136 |
137 |
138 | Only line geometry can be split.
139 | 分割できるのはラインだけです。
140 |
141 |
142 |
143 | No feature
144 | No feature
145 |
146 |
147 |
148 | Continue editing?
149 | Continue editing?
150 |
151 |
152 |
153 | Geometry type of the layer is different, or polygon isn't closed. Do you want to continue editing?
154 | レイヤのジオメトリタイプが違います。もしくは、ポリゴンが閉じていません。編集を続けますか?
155 |
156 |
157 |
158 | No feature found. Do you want to continue editing?
159 | フィーチャーがありません。編集を続けますか?
160 |
161 |
162 |
163 | Line
164 | ライン
165 |
166 |
167 |
168 | Curve
169 | カーブ
170 |
171 |
172 |
173 | Convert to Bezier
174 | ベジエ曲線に変換
175 |
176 |
177 |
178 | The feature isn't created by Bezier Tool or ver 1.3 higher.
179 |
180 | Do you want to convert to Bezier?
181 |
182 | Conversion can be done either to line segments or to fitting curve.
183 | Please select conversion mode.
184 | このフィーチャーはベジエツールまたはVer1.3以上で作成されていません。.\n\nベジエツールで編集できるように変換しますか?ラインでの変換とフィッティングカーブでの変換を選択できます。
185 |
186 |
187 |
188 | Not supported type
189 | Not supported type
190 |
191 |
192 |
193 | Geometry type of the layer is not supported.
194 | このレイヤのジオメトリタイプはサポートしていません。
195 |
196 |
197 |
198 | Bezier added
199 | Bezier added
200 |
201 |
202 |
203 | Bezier edited
204 | Bezier edited
205 |
206 |
207 |
208 | Reset guide
209 | ガイドの設定をリセット
210 |
211 |
212 |
213 | Set snap to angle
214 | スナップの角度
215 |
216 |
217 |
218 | Enter snap angle (degree)
219 | スナップする角度を入力してください
220 |
221 |
222 |
223 | Set snap to length
224 | スナップの長さ
225 |
226 |
227 |
228 | Enter snap length (in case of LatLon in seconds)
229 | スナップする長さを入力してください(緯度経度の場合は秒単位)
230 |
231 |
232 |
233 | Enter Interpolate Point Count (default is 10)
234 | 補間点数を入力してください(デフォルト10)
235 |
236 |
237 |
238 | Bezier unsplit
239 | Bezier unsplit
240 |
241 |
242 |
243 | Select exactly two feature.
244 | ラインを2本だけ選択してください。
245 |
246 |
247 |
248 | Select a line Layer.
249 | ラインレイヤを選択してください。
250 |
251 |
252 |
253 | Guide settings...
254 | ガイドの設定...
255 |
256 |
257 |
258 | Advanced settings...
259 | 高度な設定...
260 |
261 |
262 |
263 |
--------------------------------------------------------------------------------
/i18n/bezierediting_hu.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BezierEditing
6 |
7 |
8 | Bezier Edit
9 | Szerkesztés Bézier-görbével
10 |
11 |
12 |
13 | Edit Bezier Freehand
14 | Szerkesztés szabadkézi Bézier-görbével
15 |
16 |
17 |
18 | Split Bezier Curve
19 | Bézier-görbe elvágása
20 |
21 |
22 |
23 | Show Bezier Handles
24 | Bézier-fogantyúk megjelenítése
25 |
26 |
27 |
28 | Undo
29 | Visszavonás
30 |
31 |
32 |
33 | Merge Bezier Curves
34 | Bézier-görbék egyesítése
35 |
36 |
37 |
38 | Online Documentation
39 | Online dokumentáció
40 |
41 |
42 |
43 | &Bezier Editing
44 | &Bézier Editing
45 |
46 |
47 |
48 | Bezier Editing
49 | Bézier szerkesztés
50 |
51 |
52 |
53 | <b>Bezier Edit</b><br><br>
54 | Click to add anchor
55 | <dd>- click&&drag: add curved anchor (with two hanles)</dd>
56 | <dd>- click&&drag+Alt: add anchor moving only backward handle</dd>
57 | <dd>- click&&drag+Shift: add anchor without forward handle</dd>
58 | Right click to commit feature<br>
59 | Ctrl shows guide<br>
60 | On feature:
61 | <dd>- Right click to enter drawing mode again</dd>
62 | <dd>- Alt+click inserts anchor</dd>
63 | On anchor:
64 | <dd>- Alt+click pulls handle from anchor</dd>
65 | <dd>- Shift+click deletes anchor</dd>
66 | On handle:
67 | <dd>- Alt+drag moves both handles</dd>
68 | <dd>- Shift+click deletes handle</dd>
69 | On first anchor, right click to flip Bezier direction.<br>
70 | Ctrl + right click shows context menu.
71 | <b>Bézier-görbe szerkesztése</b><br><br>Kattintson egy csomópont hozzáadásához<dd>- kattintás&&húzás: íves csomópont hozzáadása(két fogantyúval)</dd><dd>- kattingás&&húzás+Alt: csomópont hozzáadása csak hátra irányú fogantyút mozgatva</dd><dd>- kattintás&&húzás+Shift: előre irányú fogantyú nélkülicsomópont hozzáadása</dd>Jobb kattintással véglegesítheti az elemet<br>Ctrl megjeleníti a segédvonalat<br>Elemen:<dd>- Jobb kattintással újraszerkesztő módba léphet</dd><dd>- Alt+kattintás csomópontot szúr be</dd>Csomóponton:<dd>- Alt+kattintás új fogantyút húz ki a csomópontból</dd><dd>- Shift+kattintás törli a csomópontot</dd>Fogantyún:<dd>- Alt+húzás mindkét fogantyút mozgatja</dd><dd>- Shift+kattintás törli a fogantyút</dd>Jobb kattintás az első csomópontra megfirdítja a Bézier rajzolásának irányát.<br>Ctrl + jobb kattintás megnyitja a helyi menüt.
72 |
73 |
74 |
75 | <b>Edit Bezier Freehand</b><br><br>
76 | - Drag to draw a line<br>
77 | - Retrace a segment and the line will be modified<br>
78 | - Right click to commit feature / enter edit mode again
79 | <b>Bézier-görbe szerkesztése szabadkézzel</b><br><br>- Vonal rajzolásához húzza a kurzort<br>- Egy vonalszakaszt újra átrajzolva módosíthatja a vonalat<br>- Jobb kattintással véglegesítheti az elemet / újra beléphet a szerkesztő módba
80 |
81 |
82 |
83 | <b>Merge Bezier Curves</b><br><br>
84 | 1. Click or click&&drag to select features<br>
85 | 2. Right click to merge.
86 | <b>Bézier-görbék egyesítése</b><br><br>1. Kattintson vagy kattintson és húzza a kurzort az elemek kiválasztásához<br>2. Egyesítse őke jobb kattintással.
87 |
88 |
89 |
90 | BezierEditingTool
91 |
92 |
93 | No feature to split.
94 | Nincs elvágandó elem.
95 |
96 |
97 |
98 | Do you want to continue editing?
99 | Szeretné folytatni a szerkesztést?
100 |
101 |
102 |
103 | Close
104 | Mégse
105 |
106 |
107 |
108 | Can't be set while editing.
109 | Szerkesztés alatt nem állítható.
110 |
111 |
112 |
113 | Be careful when changing values, as Bezier curves with different numbers of interpolants will not be converted accurately.
114 | Az értékek módosításakor legyen óvatos, az eltérő interpolánsú Bézier-görbék nem konvertálhatóak pontosan.
115 |
116 |
117 |
118 | Count
119 | Darabszám
120 |
121 |
122 |
123 | Reset editing data
124 | Szerkesztett adatok visszállítása
125 |
126 |
127 |
128 | Geometry type is different
129 | A geometriatípus eltérő
130 |
131 |
132 |
133 | Only line geometry can be split.
134 | Csak vonalgeometriát lehet elvágni.
135 |
136 |
137 |
138 | No feature
139 | Nem található elem
140 |
141 |
142 |
143 | Geometry type of the layer is different, or polygon isn't closed. Do you want to continue editing?
144 | A réteg geometriatípusa eltérő, vagy a felület nem zárt. Szeretné folytatni a szerkesztést?
145 |
146 |
147 |
148 | No feature found. Do you want to continue editing?
149 | Nem található elem. Szeretné folytatni a szerkesztést?
150 |
151 |
152 |
153 | Convert to Bezier
154 | Átalakítás Bézier-görbévé
155 |
156 |
157 |
158 | Line
159 | Vonal
160 |
161 |
162 |
163 | Curve
164 | Görbe
165 |
166 |
167 |
168 | Not supported type
169 | Nem támogatott típus
170 |
171 |
172 |
173 | Geometry type of the layer is not supported.
174 | A réteg geometriatípusa nem támogatott.
175 |
176 |
177 |
178 | Bezier added
179 | Bézier hozzáadva
180 |
181 |
182 |
183 | Bezier edited
184 | Bézier szerkesztve
185 |
186 |
187 |
188 | Continue editing?
189 | Folytatja a szerkesztést?
190 |
191 |
192 |
193 | Reset guide
194 | Segédvonal visszaállítása
195 |
196 |
197 |
198 | Enter Interpolate Point Count (default is 10)
199 | Adja meg az interpolációs pontok számát (az alapértelmezett 10)
200 |
201 |
202 |
203 | Bezier unsplit
204 | Bézier elvágva
205 |
206 |
207 |
208 | Warning
209 | Figyelmeztetés
210 |
211 |
212 |
213 | Enter snap angle (degree)
214 | Adja meg az illesztési szöget (fok)
215 |
216 |
217 |
218 | Set snap to length
219 | Távolsághoz illesztés beállítása
220 |
221 |
222 |
223 | Select exactly two feature.
224 | Pontosan két elemet válasszon ki.
225 |
226 |
227 |
228 | Select a line Layer.
229 | Válasszon ki egy vonalréteget.
230 |
231 |
232 |
233 | Enter snap length (in case of LatLon in seconds)
234 | Adja meg az illesztési távolságot (LatLon esetén másodpercben)
235 |
236 |
237 |
238 | Set snap to angle
239 | Szöghöz illesztés beállítása
240 |
241 |
242 |
243 | The feature isn't created by Bezier Tool or ver 1.3 higher.
244 |
245 | Do you want to convert to Bezier?
246 |
247 | Conversion can be done either to line segments or to fitting curve.
248 | Please select conversion mode.
249 | Az elem nem a Bézier eszközzel vagy annak legalább 1.3-as verziójával lett létrehozva.
250 |
251 | Szeretné Bezier-görbévé alakítani?
252 |
253 | A konvertálást vonalszakaszokkal vagy görbére illesztéssel lehet elvégezni.
254 | Válassza ki az átalakítás módját.
255 |
256 |
257 |
258 | Guide settings...
259 | Segédvonal beállításai...
260 |
261 |
262 |
263 | Advanced settings...
264 | Speciális beállítások...
265 |
266 |
267 |
268 |
--------------------------------------------------------------------------------
/BezierGeometry.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """"
3 | /***************************************************************************
4 | BezierEditing
5 | --------------------------------------
6 | Date : 01 05 2019
7 | Copyright : (C) 2019 Takayuki Mizutani
8 | Email : mizutani at ecoris dot co dot jp
9 | ***************************************************************************
10 | * *
11 | * This program is free software; you can redistribute it and/or modify *
12 | * it under the terms of the GNU General Public License as published by *
13 | * the Free Software Foundation; either version 2 of the License, or *
14 | * (at your option) any later version. *
15 | * *
16 | ***************************************************************************/
17 | """
18 | from qgis.core import *
19 | from .fitCurves import *
20 | import copy
21 | import math
22 | import numpy as np
23 |
24 |
25 | class BezierGeometry:
26 | INTERPOLATION = 10 # interpolation count from anchor to anchor
27 |
28 | def __init__(self, projectCRS):
29 | self.projectCRS = projectCRS
30 | self.points = [] # bezier line points list
31 | self.anchor = [] # anchor list
32 | self.handle = [] # handle list
33 | self.history = [] # undo history
34 |
35 | @classmethod
36 | def convertPointToBezier(cls, projectCRS, point):
37 | bg = cls(projectCRS)
38 | point = bg._trans(point)
39 | bg._addAnchor(0, point)
40 | return bg
41 |
42 | @classmethod
43 | def checkIsBezier(cls, projectCRS, polyline):
44 | is_bezier = True
45 | bg = cls(projectCRS)
46 | # if polyline length isn't match cause of edited other tool, points are interpolated.
47 | if len(polyline) % bg.INTERPOLATION != 1:
48 | is_bezier = False
49 | else:
50 | polyline = [bg._trans(p) for p in polyline]
51 | point_list = bg._lineToPointList(polyline)
52 | # Check if the number of points accidentally matches with the case of Bezier
53 | # if not bezier, calculation of anchor position is different from "A" and "B"
54 | for points in point_list:
55 | psA, csA, peA, ceA = bg._convertPointListToAnchorAndHandle(
56 | points, "A")
57 | psB, csB, peB, ceB = bg._convertPointListToAnchorAndHandle(
58 | points, "B")
59 |
60 | if not(abs(csA[0] - csB[0]) < 0.0001 and abs(csA[1] - csB[1]) < 0.0001 and abs(ceA[0] - ceB[0]) < 0.0001 and abs(ceA[1] - ceB[1]) < 0.0001):
61 | bg.log("{} {} {} {}".format(abs(
62 | csA[0] - csB[0]), abs(csA[1] - csB[1]), abs(ceA[0] - ceB[0]), abs(ceA[1] - ceB[1])))
63 | is_bezier = False
64 |
65 | return is_bezier
66 |
67 | @classmethod
68 | def convertLineToBezier(cls, projectCRS, polyline, linetype="bezier"): # bezier,line,curve
69 | bg = cls(projectCRS)
70 | polyline = [bg._trans(p) for p in polyline]
71 | if linetype == "bezier":
72 | point_list = bg._lineToPointList(polyline)
73 | bg._invertBezierPointListToBezier(point_list)
74 | elif linetype == "line":
75 | point_list = bg._lineToInterpolatePointList(polyline)
76 | bg._invertBezierPointListToBezier(point_list)
77 | elif linetype == "curve":
78 | geom = QgsGeometry.fromPolylineXY(polyline)
79 | bg._convertGeometryToBezier(geom, 0, scale=1.0, last=True)
80 |
81 | return bg
82 |
83 | def setCRS(self, projectCRS):
84 | self.projectCRS = projectCRS
85 |
86 | def asGeometry(self, layer_type, layer_wkbtype):
87 | """
88 | return a geometry of specified layer type
89 | """
90 | result = None
91 | geom = None
92 | num_anchor = self.anchorCount()
93 |
94 | if layer_type == QgsWkbTypes.PointGeometry and num_anchor == 1:
95 | geom = QgsGeometry.fromPointXY(self.points[0])
96 | result = True
97 | elif layer_type == QgsWkbTypes.LineGeometry and num_anchor >= 2:
98 | if QgsWkbTypes.isMultiType(layer_wkbtype):
99 | geom = QgsGeometry.fromMultiPolylineXY([self.points])
100 | result = True
101 | else:
102 | geom = QgsGeometry.fromPolylineXY(self.points)
103 | result = True
104 | elif layer_type == QgsWkbTypes.PolygonGeometry and num_anchor >= 3 and self.points[0] == self.points[-1]:
105 | geom = QgsGeometry.fromPolygonXY([self.points])
106 | result = True
107 | elif layer_type == QgsWkbTypes.PolygonGeometry and num_anchor >= 3 and self.points[0] != self.points[-1]:
108 | # if first point and last point is different, interpolate points.
109 | point_list = self._lineToInterpolatePointList(
110 | [self.points[-1], self.points[0]])
111 | geom = QgsGeometry.fromPolygonXY(
112 | [self.points + point_list[0][1:-1]])
113 | result = True
114 | elif layer_type == QgsWkbTypes.LineGeometry and num_anchor < 2:
115 | result = None
116 | elif layer_type == QgsWkbTypes.PolygonGeometry and num_anchor < 3:
117 | result = None
118 | else:
119 | result = False
120 | geom = self._transgeom(geom, revert=True)
121 | return result, geom
122 |
123 | def asPolyline(self):
124 | """
125 | return bezier line points list
126 | """
127 | points = [self._trans(p, revert=True) for p in self.points]
128 | return points
129 |
130 | def add_anchor(self, idx, point, undo=True):
131 | point = self._trans(point)
132 | if undo:
133 | self.history.append({"state": "add_anchor", "pointidx": idx})
134 | self._addAnchor(idx, point)
135 |
136 | def move_anchor(self, idx, point, undo=True):
137 | point = self._trans(point)
138 | if undo:
139 | self.history.append(
140 | {"state": "move_anchor", "pointidx": idx, "point": point})
141 | self._moveAnchor(idx, point)
142 |
143 | def move_anchor2(self, idx, point):
144 | point = self._trans(point)
145 | self.history.append(
146 | {"state": "move_anchor2", "pointidx": idx, "point": point})
147 | self._moveAnchor(idx, point)
148 | self._moveAnchor(0, point)
149 |
150 | def delete_anchor(self, idx, point, undo=True):
151 | if undo:
152 | self.history.append(
153 | {"state": "delete_anchor",
154 | "pointidx": idx,
155 | "point": point,
156 | "ctrlpoint0": self.getHandle(idx * 2),
157 | "ctrlpoint1": self.getHandle(idx * 2 + 1)
158 | }
159 | )
160 | self._deleteAnchor(idx)
161 |
162 | def delete_anchor2(self, idx, point):
163 | point = self._trans(point)
164 | self.history.append(
165 | {"state": "delete_anchor2",
166 | "pointidx": idx,
167 | "point": point,
168 | "ctrlpoint0": self.getHandle(1),
169 | "ctrlpoint1": self.getHandle(idx * 2)
170 | }
171 | )
172 | self._deleteAnchor(idx)
173 | self._deleteAnchor(0)
174 | self._addAnchor(self.anchorCount(), self.getAnchor(0))
175 |
176 | def move_handle(self, idx, point, undo=True):
177 | point = self._trans(point)
178 | if undo:
179 | self.history.append(
180 | {"state": "move_handle", "pointidx": idx, "point": point})
181 | self._moveHandle(idx, point)
182 |
183 | def other_handle(self, handle_idx, point):
184 | point = self._trans(point)
185 | other_handle_idx = handle_idx + \
186 | 1 if handle_idx % 2 == 0 else handle_idx - 1
187 | anchor_idx = math.floor(handle_idx/2)
188 | anchor_point = self.getAnchor(anchor_idx)
189 | other_point = QgsPointXY(
190 | anchor_point[0] - (point[0] - anchor_point[0]), anchor_point[1] - (point[1] - anchor_point[1]))
191 | other_point = self._trans(other_point, revert=True)
192 | return other_handle_idx, other_point
193 |
194 | def move_handle2(self, anchor_idx, point, fix_first=False, remove_second=False):
195 | """
196 | move the handles on both sides of the anchor as you drag the anchor.
197 | fix the first handle with fix_first option.
198 | move the second handle to the anchor position with remove_second option.
199 | """
200 | point = self._trans(point)
201 | handle_idx = anchor_idx * 2
202 | p = self.getAnchor(anchor_idx)
203 | pb = QgsPointXY(p[0] - (point[0] - p[0]), p[1] - (point[1] - p[1]))
204 | if remove_second:
205 | self._moveHandle(handle_idx, pb)
206 | self._moveHandle(handle_idx + 1, p)
207 | elif fix_first:
208 | self._moveHandle(handle_idx + 1, point)
209 | else:
210 | self._moveHandle(handle_idx, pb)
211 | self._moveHandle(handle_idx + 1, point)
212 | pb = self._trans(pb, revert=True)
213 | p = self._trans(p, revert=True)
214 | return handle_idx, pb, p
215 |
216 | def delete_handle(self, idx, point):
217 | point = self._trans(point)
218 | self.history.append(
219 | {"state": "delete_handle",
220 | "pointidx": idx,
221 | "point": point,
222 | }
223 | )
224 | pnt = self.getAnchor(int(idx / 2))
225 | self._moveHandle(idx, pnt)
226 |
227 | def flip_line(self):
228 | self.history.append({"state": "flip_line"})
229 | self._flipBezierLine()
230 |
231 | def insert_anchor(self, point_idx, point):
232 | point = self._trans(point)
233 | anchor_idx = self._AnchorIdx(point_idx)
234 | self.history.append(
235 | {"state": "insert_anchor",
236 | "pointidx": anchor_idx,
237 | "ctrlpoint0": self.getHandle((anchor_idx - 1) * 2 + 1),
238 | "ctrlpoint1": self.getHandle((anchor_idx - 1) * 2 + 2)
239 | }
240 | )
241 | self._insertAnchorPointToBezier(point_idx, anchor_idx, point)
242 |
243 | def modified_by_geometry(self, update_geom, layer_type, scale, snap_to_start):
244 | """
245 | update bezier line by geometry. if no bezier line, added new.
246 | """
247 |
248 | update_geom = self._transgeom(update_geom)
249 | dist = scale / 250
250 | bezier_line = self.points
251 | update_line = update_geom.asPolyline()
252 | bezier_geom = QgsGeometry.fromPolylineXY(bezier_line)
253 |
254 | if len(update_line) < 3:
255 | return
256 |
257 | # no bezier line or only a point.
258 | # The number of anchors is 1 instead of 0 because anchors are added on click if no bezier line.
259 | if self.anchorCount() == 1:
260 | # if there is no point and update line is a point insted of line
261 | # The number of update_line points is 2 instead of 1 because rubberband points are added two at first.
262 | if len(update_line) == 2:
263 | self._deleteAnchor(0)
264 | self.add_anchor(0, update_line[0])
265 | # if there is no point and update line is line
266 | elif len(self.history) == 0 and len(update_line) > 2:
267 | self._deleteAnchor(0)
268 | self.history.append({"state": "start_freehand"})
269 | geom = self._smoothingGeometry(update_line)
270 | pointnum, _, _ = self._convertGeometryToBezier(
271 | geom, 0, scale, last=True)
272 | self.history.append(
273 | {"state": "insert_geom", "pointidx": 0, "pointnum": pointnum, "cp_first": None,
274 | "cp_last": None})
275 | self.history.append(
276 | {"state": "end_freehand", "direction": "forward"})
277 | # if there is only a point and update line is line
278 | elif len(self.history) > 0 and len(update_line) > 2:
279 | self.history.append({"state": "start_freehand"})
280 | geom = self._smoothingGeometry(update_line)
281 | pointnum, _, _ = self._convertGeometryToBezier(
282 | geom, 1, scale, last=True)
283 | self.history.append(
284 | {"state": "insert_geom", "pointidx": 1, "pointnum": pointnum, "cp_first": None,
285 | "cp_last": None})
286 | self.history.append(
287 | {"state": "end_freehand", "direction": "forward"})
288 | # there is bezier line and update line is line
289 | else:
290 | startpnt = update_line[0]
291 | lastpnt = update_line[-1]
292 | startpnt_is_near, start_anchoridx, start_vertexidx = self._closestAnchorOfGeometry(
293 | startpnt, bezier_geom, dist)
294 | lastpnt_is_near, last_anchoridx, last_vertexidx = self._closestAnchorOfGeometry(
295 | lastpnt, bezier_geom, dist)
296 |
297 | # Calculate inner product of vectors around intersection of bezier_line and update_line.
298 | # Forward if positive, backward if negative
299 | v1 = np.array(bezier_line[start_vertexidx]) - \
300 | np.array(bezier_line[start_vertexidx - 1])
301 | v2 = np.array(update_line[1]) - np.array(update_line[0])
302 | direction = np.dot(v1, v2)
303 |
304 | self.history.append({"state": "start_freehand"})
305 | # if backward, flip bezier line
306 | if direction < 0:
307 | self._flipBezierLine()
308 | reversed_geom = QgsGeometry.fromPolylineXY(bezier_line)
309 | startpnt_is_near, start_anchoridx, start_vertexidx = self._closestAnchorOfGeometry(startpnt,
310 | reversed_geom, dist)
311 | lastpnt_is_near, last_anchoridx, last_vertexidx = self._closestAnchorOfGeometry(lastpnt, reversed_geom,
312 | dist)
313 |
314 | point_list = self._lineToPointList(bezier_line)
315 |
316 | # modify middle of bezier line.
317 | if lastpnt_is_near and last_vertexidx > start_vertexidx and last_anchoridx <= len(point_list):
318 |
319 | polyline = point_list[start_anchoridx - 1][0:self._pointListIdx(start_vertexidx)] + \
320 | update_line + \
321 | point_list[last_anchoridx -
322 | 1][self._pointListIdx(last_vertexidx):]
323 |
324 | geom = self._smoothingGeometry(polyline)
325 | for i in range(start_anchoridx, last_anchoridx):
326 | self.history.append(
327 | {"state": "delete_anchor",
328 | "pointidx": start_anchoridx,
329 | "point": self.getAnchor(start_anchoridx),
330 | "ctrlpoint0": self.getHandle(start_anchoridx * 2),
331 | "ctrlpoint1": self.getHandle(start_anchoridx * 2 + 1)
332 | }
333 | )
334 | self._deleteAnchor(start_anchoridx)
335 |
336 | pointnum, cp_first, cp_last = self._convertGeometryToBezier(
337 | geom, start_anchoridx, scale, last=False)
338 | self.history.append(
339 | {"state": "insert_geom", "pointidx": start_anchoridx, "pointnum": pointnum, "cp_first": cp_first,
340 | "cp_last": cp_last})
341 | # modify polygon
342 | elif layer_type == QgsWkbTypes.PolygonGeometry and lastpnt_is_near and last_vertexidx <= start_vertexidx:
343 | polyline = point_list[start_anchoridx - 1][0:self._pointListIdx(start_vertexidx)] + update_line + \
344 | point_list[last_anchoridx -
345 | 1][self._pointListIdx(last_vertexidx):]
346 | geom = self._smoothingGeometry(polyline)
347 | for i in range(start_anchoridx, self.anchorCount()):
348 | self.history.append(
349 | {"state": "delete_anchor",
350 | "pointidx": start_anchoridx,
351 | "point": self.getAnchor(start_anchoridx),
352 | "ctrlpoint0": self.getHandle(start_anchoridx * 2),
353 | "ctrlpoint1": self.getHandle(start_anchoridx * 2 + 1)
354 | }
355 | )
356 | self._deleteAnchor(start_anchoridx)
357 |
358 | pointnum, cp_first, cp_last = self._convertGeometryToBezier(
359 | geom, start_anchoridx, scale, last=True)
360 | self.history.append(
361 | {"state": "insert_geom", "pointidx": start_anchoridx, "pointnum": pointnum, "cp_first": cp_first,
362 | "cp_last": cp_last})
363 | for i in range(0, last_anchoridx):
364 | self.history.append(
365 | {"state": "delete_anchor",
366 | "pointidx": 0,
367 | "point": self.getAnchor(0),
368 | "ctrlpoint0": self.getHandle(0),
369 | "ctrlpoint1": self.getHandle(1)
370 | }
371 | )
372 | self._deleteAnchor(0)
373 |
374 | # modify end line, return to backward, end line is near of last anchor
375 | elif not lastpnt_is_near or (lastpnt_is_near and last_vertexidx <= start_vertexidx) or last_anchoridx > len(
376 | point_list):
377 |
378 | if start_anchoridx == self.anchorCount():
379 | polyline = update_line
380 | else:
381 | polyline = point_list[start_anchoridx -
382 | 1][0:self._pointListIdx(start_vertexidx)] + update_line
383 | last_anchoridx = self.anchorCount()
384 |
385 | geom = self._smoothingGeometry(polyline)
386 | for i in range(start_anchoridx, last_anchoridx):
387 | self.history.append(
388 | {"state": "delete_anchor",
389 | "pointidx": start_anchoridx,
390 | "point": self.getAnchor(start_anchoridx),
391 | "ctrlpoint0": self.getHandle(start_anchoridx * 2),
392 | "ctrlpoint1": self.getHandle(start_anchoridx * 2 + 1)
393 | }
394 | )
395 | self._deleteAnchor(start_anchoridx)
396 |
397 | pointnum, cp_first, cp_last = self._convertGeometryToBezier(
398 | geom, start_anchoridx, scale, last=True)
399 | self.history.append(
400 | {"state": "insert_geom", "pointidx": start_anchoridx, "pointnum": pointnum, "cp_first": cp_first,
401 | "cp_last": cp_last})
402 |
403 | self.history.append(
404 | {"state": "end_freehand", "direction": "forward"})
405 | # return to direction
406 | if direction < 0:
407 | self._flipBezierLine()
408 | self.history[-1]["direction"] = "reverse"
409 |
410 | # If it was snapped to the start point, move the last point shifted to the first point for smooth processing
411 | if snap_to_start:
412 | self._moveAnchor(self.anchorCount() - 1, self.getAnchor(0))
413 |
414 | def split_line(self, idx, point, isAnchor):
415 | """
416 | return two bezier line split at point
417 | """
418 | # if split position is on anchor
419 | point = self._trans(point)
420 | if isAnchor:
421 | lineA = self.points[0:self._pointsIdx(idx) + 1]
422 | lineB = self.points[self._pointsIdx(idx):]
423 | # if split position is on line, insert anchor at the position first
424 | else:
425 | anchor_idx = self._AnchorIdx(idx)
426 | self._insertAnchorPointToBezier(idx, anchor_idx, point)
427 | lineA = self.points[0:self._pointsIdx(anchor_idx) + 1]
428 | lineB = self.points[self._pointsIdx(anchor_idx):]
429 | lineA = [self._trans(p, revert=True) for p in lineA]
430 | lineB = [self._trans(p, revert=True) for p in lineB]
431 |
432 | return lineA, lineB
433 |
434 | def anchorCount(self):
435 | return len(self.anchor)
436 |
437 | def getAnchorList(self, revert=False):
438 | if revert:
439 | anchorList = [self._trans(p, revert=True) for p in self.anchor]
440 | else:
441 | anchorList = self.anchor
442 | return anchorList
443 |
444 | def getAnchor(self, idx, revert=False):
445 | p = self.anchor[idx]
446 | if revert:
447 | p = self._trans(p, revert=True)
448 | return p
449 |
450 | def getHandleList(self, revert=False):
451 | if revert:
452 | handleList = [self._trans(p, revert=True) for p in self.handle]
453 | else:
454 | handleList = self.handle
455 | return handleList
456 |
457 | def getHandle(self, idx, revert=False):
458 | p = self.handle[idx]
459 | if revert:
460 | p = self._trans(p, revert=True)
461 | return p
462 |
463 | def getPointList(self, revert=False):
464 | if revert:
465 | pointList = [self._trans(p, revert=True) for p in self.points]
466 | else:
467 | pointList = self.points
468 | return pointList
469 |
470 | def reset(self):
471 | self.points = []
472 | self.anchor = []
473 | self.handle = []
474 | self.history = []
475 |
476 | def checkSnapToAnchor(self, point, clicked_idx, d):
477 | point = self._trans(point)
478 | snapped = False
479 | snap_point = None
480 | snap_idx = None
481 | for i, p in reversed(list(enumerate(self.anchor))):
482 | near = self._eachPointIsNear(p, point, d)
483 | # if anchor is not moving
484 | if clicked_idx is None:
485 | if near:
486 | snapped = True
487 | snap_idx = i
488 | snap_point = self._trans(p, revert=True)
489 | break
490 | # if the anchor is moving, except for snapping to itself
491 | elif clicked_idx != i:
492 | if near:
493 | snapped = True
494 | snap_idx = i
495 | snap_point = self._trans(p, revert=True)
496 | break
497 | return snapped, snap_point, snap_idx
498 |
499 | def checkSnapToHandle(self, point, d):
500 | point = self._trans(point)
501 | snapped = False
502 | snap_point = None
503 | snap_idx = None
504 | for i, p in reversed(list(enumerate(self.handle))):
505 | if i == 0 or i == len(self.handle)-1:
506 | continue
507 | near = self._eachPointIsNear(p, point, d)
508 | if near:
509 | snapped = True
510 | snap_idx = i
511 | snap_point = self._trans(p, revert=True)
512 | break
513 | return snapped, snap_point, snap_idx
514 |
515 | def checkSnapToLine(self, point, d):
516 | point = self._trans(point)
517 | snapped = False
518 | snap_point = None
519 | snap_idx = None
520 | if self.anchorCount() > 1:
521 | geom = QgsGeometry.fromPolylineXY(self.points)
522 | (dist, minDistPoint, afterVertex,
523 | leftOf) = geom.closestSegmentWithContext(point)
524 | if math.sqrt(dist) < d:
525 | snapped = True
526 | snap_idx = afterVertex
527 | snap_point = self._trans(minDistPoint, revert=True)
528 |
529 | return snapped, snap_point, snap_idx
530 |
531 | def checkSnapToStart(self, point, d):
532 | point = self._trans(point)
533 | snapped = False
534 | snap_point = None
535 | snap_idx = None
536 | if self.anchorCount() > 0:
537 | start_anchor = self.getAnchor(0)
538 | near = self._eachPointIsNear(start_anchor, point, d)
539 | if near:
540 | snapped = True
541 | snap_idx = 0
542 | snap_point = self._trans(start_anchor, revert=True)
543 |
544 | return snapped, snap_point, snap_idx
545 |
546 | def undo(self):
547 | """
548 | do invert process from history
549 | """
550 | if len(self.history) > 0:
551 | act = self.history.pop()
552 | if act["state"] == "add_anchor":
553 | self._deleteAnchor(act["pointidx"])
554 | elif act["state"] == "move_anchor":
555 | self._moveAnchor(act["pointidx"], act["point"])
556 | elif act["state"] == "move_anchor2":
557 | self._moveAnchor(act["pointidx"], act["point"])
558 | self._moveAnchor(0, act["point"])
559 | elif act["state"] == "move_handle":
560 | self._moveHandle(act["pointidx"], act["point"])
561 | elif act["state"] == "insert_anchor":
562 | self._deleteAnchor(act["pointidx"])
563 | self._moveHandle((act["pointidx"] - 1) *
564 | 2 + 1, act["ctrlpoint0"])
565 | self._moveHandle((act["pointidx"] - 1) *
566 | 2 + 2, act["ctrlpoint1"])
567 | elif act["state"] == "delete_anchor":
568 | self._addAnchor(act["pointidx"], act["point"])
569 | self._moveHandle(act["pointidx"] * 2, act["ctrlpoint0"])
570 | self._moveHandle(act["pointidx"] * 2 + 1, act["ctrlpoint1"])
571 | elif act["state"] == "delete_anchor2":
572 | self._deleteAnchor(self.anchorCount()-1)
573 | self._addAnchor(0, act["point"])
574 | self._moveHandle(1, act["ctrlpoint0"])
575 | self._addAnchor(act["pointidx"], act["point"])
576 | self._moveHandle(act["pointidx"] * 2, act["ctrlpoint1"])
577 | elif act["state"] == "delete_handle":
578 | self._moveHandle(act["pointidx"], act["point"])
579 | elif act["state"] == "flip_line":
580 | self._flipBezierLine()
581 | self.undo()
582 | elif act["state"] == "end_freehand":
583 | direction = act["direction"]
584 | if direction == "reverse":
585 | self._flipBezierLine()
586 | act = self.history.pop()
587 | while act["state"] != "start_freehand":
588 | if act["state"] == "insert_geom":
589 | for i in range(act["pointnum"]):
590 | self._deleteAnchor(act["pointidx"])
591 | if act["cp_first"] is not None:
592 | self._moveHandle(
593 | act["pointidx"] * 2 - 1, act["cp_first"])
594 | if act["cp_last"] is not None:
595 | self._moveHandle(
596 | act["pointidx"] * 2, act["cp_last"])
597 | elif act["state"] == "delete_anchor":
598 | self._addAnchor(act["pointidx"], act["point"])
599 | self._moveHandle(
600 | act["pointidx"] * 2, act["ctrlpoint0"])
601 | self._moveHandle(
602 | act["pointidx"] * 2 + 1, act["ctrlpoint1"])
603 | act = self.history.pop()
604 | if direction == "reverse":
605 | self._flipBezierLine()
606 |
607 | # self.dump_history()
608 | return len(self.history)
609 |
610 | def _eachPointIsNear(self, snap_point, point, d):
611 | near = False
612 | if (snap_point.x() - d <= point.x() <= snap_point.x() + d) and (
613 | snap_point.y() - d <= point.y() <= snap_point.y() + d):
614 | near = True
615 | return near
616 |
617 | def _insertAnchorPointToBezier(self, point_idx, anchor_idx, point):
618 | """
619 | insert anchor to bezier line. move handle for not changing bezier curve
620 | """
621 | c1a, c2a, c1b, c2b = self._recalcHandlePosition(
622 | point_idx, anchor_idx, point)
623 | self._addAnchor(anchor_idx, point)
624 | self._moveHandle((anchor_idx - 1) * 2 + 1, c1a)
625 | self._moveHandle((anchor_idx - 1) * 2 + 2, c2a)
626 | self._moveHandle((anchor_idx - 1) * 2 + 3, c1b)
627 | self._moveHandle((anchor_idx - 1) * 2 + 4, c2b)
628 |
629 | def _convertGeometryToBezier(self, geom, offset, scale, last=True):
630 | """
631 | convert geometry to anchor and handle list by fitCurve, then add it to bezier line
632 | if last=F, don't insert last point
633 | """
634 | polyline = geom.asPolyline()
635 | points = np.array(polyline)
636 | # This expression returns the same point distance at any scale.
637 | # This value was determined by a manual test.
638 | maxError = 25**(math.log(scale/2000, 5))
639 | beziers = fitCurve(points, maxError)
640 | pointnum = 0
641 |
642 | if offset != 0:
643 | cp_first = self.getHandle(offset * 2 - 1)
644 | else:
645 | cp_first = None
646 | if last == False:
647 | cp_last = self.getHandle(offset * 2)
648 | else:
649 | cp_last = None
650 |
651 | for i, bezier in enumerate(beziers):
652 | if offset == 0:
653 | if i == 0:
654 | p0 = QgsPointXY(bezier[0][0], bezier[0][1])
655 | self._addAnchor(0, p0)
656 | pointnum = pointnum + 1
657 | p1 = QgsPointXY(bezier[3][0], bezier[3][1])
658 | c1 = QgsPointXY(bezier[1][0], bezier[1][1])
659 | c2 = QgsPointXY(bezier[2][0], bezier[2][1])
660 | self._moveHandle(i * 2 + 1, c1)
661 | self._addAnchor(i + 1, p1)
662 | self._moveHandle((i + 1) * 2, c2)
663 | pointnum = pointnum + 1
664 |
665 | elif offset > 0:
666 | p1 = QgsPointXY(bezier[3][0], bezier[3][1])
667 | c1 = QgsPointXY(bezier[1][0], bezier[1][1])
668 | c2 = QgsPointXY(bezier[2][0], bezier[2][1])
669 | idx = (offset - 1 + i) * 2 + 1
670 | self._moveHandle(idx, c1)
671 |
672 | if i != len(beziers) - 1 or last:
673 | self._addAnchor(offset + i, p1)
674 | pointnum = pointnum + 1
675 | self._moveHandle(idx + 1, c2)
676 |
677 | return pointnum, cp_first, cp_last
678 |
679 | def _addAnchor(self, idx, point):
680 | """
681 | insert anchor at idx and recalc bezier line. both handle also added at the same position of anchor
682 | """
683 | if idx == -1:
684 | idx = self.anchorCount()
685 | self.anchor.insert(idx, point)
686 | self.handle.insert(idx * 2, point)
687 | self.handle.insert(idx * 2, point)
688 | pointsA = []
689 | pointsB = []
690 | # calc bezier line of right side of the anchor.
691 | if idx < self.anchorCount() - 1:
692 | p1 = self.getAnchor(idx)
693 | p2 = self.getAnchor(idx + 1)
694 | c1 = self.getHandle(idx * 2 + 1)
695 | c2 = self.getHandle(idx * 2 + 2)
696 | pointsA = self._bezier(p1, c1, p2, c2)
697 | # calc bezier line of left side of the anchor
698 | if idx >= 1:
699 | p1 = self.getAnchor(idx - 1)
700 | p2 = self.getAnchor(idx)
701 | c1 = self.getHandle(idx * 2 - 1)
702 | c2 = self.getHandle(idx * 2)
703 | pointsB = self._bezier(p1, c1, p2, c2)
704 |
705 | # first anchor
706 | if idx == 0 and len(pointsA) == 0:
707 | self.points = copy.copy(self.anchor)
708 | # the case of undo that of polygon's first anchor delete
709 | elif idx == 0 and len(pointsA) > 0:
710 | self.points = pointsA[0:-1] + self.points
711 | # second anchor
712 | elif idx == 1 and idx == self.anchorCount() - 1:
713 | self.points = pointsB
714 | # third point and after
715 | elif idx >= 2 and idx == self.anchorCount() - 1:
716 | self.points = self.points + pointsB[1:]
717 | # insert anchor
718 | else:
719 | self.points[self._pointsIdx(
720 | idx - 1):self._pointsIdx(idx) + 1] = pointsB + pointsA[1:]
721 |
722 | def _deleteAnchor(self, idx):
723 | # first anchor
724 | if idx == 0:
725 | del self.points[0:self.INTERPOLATION]
726 | # end anchor
727 | elif idx + 1 == self.anchorCount():
728 | del self.points[self._pointsIdx(idx - 1) + 1:]
729 | else:
730 | p1 = self.getAnchor(idx - 1)
731 | p2 = self.getAnchor(idx + 1)
732 | c1 = self.getHandle((idx - 1) * 2 + 1)
733 | c2 = self.getHandle((idx + 1) * 2)
734 | points = self._bezier(p1, c1, p2, c2)
735 | self.points[self._pointsIdx(
736 | idx - 1):self._pointsIdx(idx + 1) + 1] = points
737 | self._delHandle(2 * idx)
738 | self._delHandle(2 * idx)
739 | self._delAnchor(idx)
740 |
741 | return
742 |
743 | def _moveAnchor(self, idx, point):
744 | diff = point - self.getAnchor(idx)
745 | self._setAnchor(idx, point)
746 | self._setHandle(idx * 2, self.getHandle(idx * 2) + diff)
747 | self._setHandle(idx * 2 + 1, self.getHandle(idx * 2 + 1) + diff)
748 | # if only one anchor
749 | if idx == 0 and self.anchorCount() == 1:
750 | self.points = copy.copy(self.anchor)
751 | else:
752 | # calc bezier line of right side of the anchor.
753 | if idx < self.anchorCount() - 1:
754 | p1 = self.getAnchor(idx)
755 | p2 = self.getAnchor(idx + 1)
756 | c1 = self.getHandle(idx * 2 + 1)
757 | c2 = self.getHandle(idx * 2 + 2)
758 | points = self._bezier(p1, c1, p2, c2)
759 | self.points[self._pointsIdx(
760 | idx):self._pointsIdx(idx + 1) + 1] = points
761 | # calc bezier line of left side of the anchor.
762 | if idx >= 1:
763 | p1 = self.getAnchor(idx - 1)
764 | p2 = self.getAnchor(idx)
765 | c1 = self.getHandle(idx * 2 - 1)
766 | c2 = self.getHandle(idx * 2)
767 | points = self._bezier(p1, c1, p2, c2)
768 | self.points[self._pointsIdx(
769 | idx - 1):self._pointsIdx(idx) + 1] = points
770 |
771 | def _moveHandle(self, idx, point):
772 | self._setHandle(idx, point)
773 | if self.anchorCount() > 1:
774 | # right side handle
775 | if idx % 2 == 1 and idx < self._handleCount() - 1:
776 | idxP = idx // 2
777 | p1 = self.getAnchor(idxP)
778 | p2 = self.getAnchor(idxP + 1)
779 | c1 = self.getHandle(idx)
780 | c2 = self.getHandle(idx + 1)
781 | # left side handle
782 | elif idx % 2 == 0 and idx >= 1:
783 | idxP = (idx - 1) // 2
784 | p1 = self.getAnchor(idxP)
785 | p2 = self.getAnchor(idxP + 1)
786 | c1 = self.getHandle(idx - 1)
787 | c2 = self.getHandle(idx)
788 | else:
789 | return
790 | points = self._bezier(p1, c1, p2, c2)
791 | self.points[self._pointsIdx(
792 | idxP):self._pointsIdx(idxP + 1) + 1] = points
793 |
794 | def _recalcHandlePosition(self, point_idx, anchor_idx, pnt):
795 | """
796 | Recalculate handle positions on both sides from point list between anchors when adding anchors to Bezier curve
797 | """
798 | bezier_idx = self._pointListIdx(point_idx)
799 |
800 | # calc handle position of left size of anchor
801 | # If point counts of left side of insert point are 4 points or more, handle position can be recalculated .
802 | if 2 < bezier_idx:
803 | pointsA = self.points[self._pointsIdx(
804 | anchor_idx - 1):point_idx] + [pnt]
805 | ps, cs, pe, ce = self._convertPointListToAnchorAndHandle(pointsA)
806 | c1a = QgsPointXY(cs[0], cs[1])
807 | c2a = QgsPointXY(ce[0], ce[1])
808 | # If it is less than 4 points, make the position of the handle the same as the anchor.
809 | # and then connect with a straight line
810 | else:
811 | c1a = self.points[self._pointsIdx(anchor_idx - 1)]
812 | c2a = pnt
813 | # calc handle position of right size of anchor
814 | # The way of thinking is the same as the left side
815 | if self.INTERPOLATION - 1 > bezier_idx:
816 | pointsB = [pnt] + \
817 | self.points[point_idx:self._pointsIdx(anchor_idx) + 1]
818 | ps, cs, pe, ce = self._convertPointListToAnchorAndHandle(
819 | pointsB, type="B")
820 | c1b = QgsPointXY(cs[0], cs[1])
821 | c2b = QgsPointXY(ce[0], ce[1])
822 | else:
823 | c1b = pnt
824 | c2b = self.points[self._pointsIdx(anchor_idx)]
825 |
826 | return (c1a, c2a, c1b, c2b)
827 |
828 | def _bezier(self, p1, c1, p2, c2):
829 | """
830 | Returns a list of Bezier line points defined by the start and end point anchors and handles
831 | """
832 | points = []
833 | for t in range(0, self.INTERPOLATION + 1):
834 | t = 1.0 * t / self.INTERPOLATION
835 | bx = (1 - t) ** 3 * p1.x() + 3 * t * (1 - t) ** 2 * \
836 | c1.x() + 3 * t ** 2 * (1 - t) * c2.x() + t ** 3 * p2.x()
837 | by = (1 - t) ** 3 * p1.y() + 3 * t * (1 - t) ** 2 * \
838 | c1.y() + 3 * t ** 2 * (1 - t) * c2.y() + t ** 3 * p2.y()
839 | points.append(QgsPointXY(bx, by))
840 | return points
841 |
842 | def _invertBezierPointListToBezier(self, point_list):
843 | """
844 | invert from the Bezier pointList to anchor and handle coordinate
845 | """
846 | for i, points_i in enumerate(point_list):
847 | ps, cs, pe, ce = self._convertPointListToAnchorAndHandle(points_i)
848 | p0 = QgsPointXY(ps[0], ps[1])
849 | p1 = QgsPointXY(pe[0], pe[1])
850 | c0 = QgsPointXY(ps[0], ps[1])
851 | c1 = QgsPointXY(cs[0], cs[1])
852 | c2 = QgsPointXY(ce[0], ce[1])
853 | c3 = QgsPointXY(pe[0], pe[1])
854 | if i == 0:
855 | self._addAnchor(-1, p0)
856 | self._moveHandle(i * 2, c0)
857 | self._moveHandle(i * 2 + 1, c1)
858 | self._addAnchor(-1, p1)
859 | self._moveHandle((i + 1) * 2, c2)
860 | self._moveHandle((i + 1) * 2 + 1, c3)
861 | else:
862 | self._moveHandle(i * 2 + 1, c1)
863 | self._addAnchor(-1, p1)
864 | self._moveHandle((i + 1) * 2, c2)
865 | self._moveHandle((i + 1) * 2 + 1, c3)
866 |
867 | def _convertPointListToAnchorAndHandle(self, points, type="A"):
868 | """
869 | convert to anchor and handle coordinate from the element of pointList
870 | the element of pointList is points between anchor to anchor
871 | it is solved the equation from the coordinates of t1 and t2
872 | type B solves a system of equations using the last two points. It is used for right side processing when inserting.
873 | """
874 |
875 | ps = np.array(points[0])
876 | pe = np.array(points[-1])
877 |
878 | tnum = len(points) - 1
879 | if type == "A":
880 | t1 = 1.0 / tnum
881 | p1 = np.array(points[1])
882 | t2 = 2.0 / tnum
883 | p2 = np.array(points[2])
884 | elif type == "B":
885 | t1 = (tnum - 1) / tnum
886 | p1 = np.array(points[-2])
887 | t2 = (tnum - 2) / tnum
888 | p2 = np.array(points[-3])
889 |
890 | aa = 3 * t1 * (1 - t1) ** 2
891 | bb = 3 * t1 ** 2 * (1 - t1)
892 | cc = ps * (1 - t1) ** 3 + pe * t1 ** 3 - p1
893 | dd = 3 * t2 * (1 - t2) ** 2
894 | ee = 3 * t2 ** 2 * (1 - t2)
895 | ff = ps * (1 - t2) ** 3 + pe * t2 ** 3 - p2
896 | c0 = (bb * ff - cc * ee) / (aa * ee - bb * dd)
897 | c1 = (aa * ff - cc * dd) / (bb * dd - aa * ee)
898 | return ps, c0, pe, c1
899 |
900 | def _flipBezierLine(self):
901 | self.anchor.reverse()
902 | self.handle.reverse()
903 | self.points.reverse()
904 |
905 | def _lineToInterpolatePointList(self, polyline):
906 |
907 | return [self._bezier(polyline[i], polyline[i], polyline[i+1], polyline[i+1]) for i in range(0, len(polyline)-1)]
908 |
909 | def _lineToPointList(self, polyline):
910 | """
911 | convert to pointList from polyline. pointList is points list between anchor to anchor
912 | The number of elements in pointList is INTERPOLATION + 1 because each anchor overlaps.
913 | """
914 | return [polyline[i:i + self.INTERPOLATION + 1] for i in range(0, len(polyline), self.INTERPOLATION)][:-1]
915 |
916 | def _pointListIdx(self, point_idx):
917 | """
918 | convert to pointList idx from bezier line points idx
919 | """
920 | return (point_idx - 1) % self.INTERPOLATION + 1
921 |
922 | def _pointsIdx(self, anchor_idx):
923 | """
924 | convert to bezier line points idx from anchor idx
925 | """
926 | return anchor_idx * self.INTERPOLATION
927 |
928 | def _AnchorIdx(self, point_idx):
929 | """
930 | convert to bezier anchor idx from bezier line points idx
931 | It is the first anchor behind point.
932 | """
933 | return (point_idx - 1) // self.INTERPOLATION + 1
934 |
935 | def _setAnchor(self, idx, point):
936 | self.anchor[idx] = point
937 |
938 | def _delAnchor(self, idx):
939 | del self.anchor[idx]
940 |
941 | def _handleCount(self):
942 | return len(self.handle)
943 |
944 | def _setHandle(self, idx, point):
945 | self.handle[idx] = point
946 |
947 | def _delHandle(self, idx):
948 | del self.handle[idx]
949 |
950 | def _closestAnchorOfGeometry(self, point, geom, d):
951 | """
952 | return anchor idx and vertex idx which is closest point with bezier line
953 | """
954 | near = False
955 | (dist, minDistPoint, vertexidx, leftOf) = geom.closestSegmentWithContext(point)
956 | anchoridx = self._AnchorIdx(vertexidx)
957 | if math.sqrt(dist) < d:
958 | near = True
959 | return near, anchoridx, vertexidx
960 |
961 | def _smoothing(self, polyline):
962 | """
963 | smoothing by moving average
964 | """
965 | poly = np.reshape(polyline, (-1, 2)).T
966 | num = 8
967 | b = np.ones(num) / float(num)
968 | x_pad = np.pad(poly[0], (num - 1, 0), 'edge')
969 | y_pad = np.pad(poly[1], (num - 1, 0), 'edge')
970 | x_smooth = np.convolve(x_pad, b, mode='valid')
971 | y_smooth = np.convolve(y_pad, b, mode='valid')
972 | poly_smooth = [QgsPointXY(x, y) for x, y in zip(x_smooth, y_smooth)]
973 | return poly_smooth
974 |
975 | def _smoothingGeometry(self, polyline):
976 | """
977 | convert polyline to smoothing geometry
978 | """
979 | #polyline = self._smoothing(polyline)
980 | geom = QgsGeometry.fromPolylineXY(polyline)
981 | smooth_geom = geom.smooth()
982 | return smooth_geom
983 |
984 | def _trans(self, p, revert=False):
985 | if self.projectCRS.projectionAcronym() == "longlat" and revert == False:
986 | # check latitude exceeded limits
987 | if p.y() > 90:
988 | p = QgsPointXY(p.x(), 89.9999999)
989 | elif p.y() < -90:
990 | p = QgsPointXY(p.x(), -89.9999999)
991 |
992 | destCrs = QgsCoordinateReferenceSystem("EPSG:3857")
993 | if revert:
994 | tr = QgsCoordinateTransform(
995 | destCrs, self.projectCRS, QgsProject.instance())
996 | else:
997 | tr = QgsCoordinateTransform(
998 | self.projectCRS, destCrs, QgsProject.instance())
999 | p = tr.transform(p)
1000 | return p
1001 |
1002 | def _transgeom(self, geom, revert=False):
1003 | g = QgsGeometry(geom)
1004 | destCrs = QgsCoordinateReferenceSystem("EPSG:3857")
1005 | if revert:
1006 | tr = QgsCoordinateTransform(
1007 | destCrs, self.projectCRS, QgsProject.instance())
1008 | else:
1009 | tr = QgsCoordinateTransform(
1010 | self.projectCRS, destCrs, QgsProject.instance())
1011 | g.transform(tr)
1012 | return g
1013 |
1014 | # for debug
1015 | def dump_history(self):
1016 | self.log("##### history dump ######")
1017 | for h in self.history:
1018 | self.log("{}".format(h.items()))
1019 | self.log("##### end ######")
1020 |
1021 | def log(self, msg):
1022 | QgsMessageLog.logMessage(msg, 'MyPlugin', Qgis.Info)
1023 |
--------------------------------------------------------------------------------
/beziereditingtool.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """"
3 | /***************************************************************************
4 | BezierEditing
5 | --------------------------------------
6 | Date : 01 05 2019
7 | Copyright : (C) 2019 Takayuki Mizutani
8 | Email : mizutani at ecoris dot co dot jp
9 | ***************************************************************************
10 | * *
11 | * This program is free software; you can redistribute it and/or modify *
12 | * it under the terms of the GNU General Public License as published by *
13 | * the Free Software Foundation; either version 2 of the License, or *
14 | * (at your option) any later version. *
15 | * *
16 | ***************************************************************************/
17 | """
18 | from qgis.PyQt.QtCore import Qt
19 | from qgis.PyQt.QtCore import QObject, QLocale, QTranslator, QCoreApplication, QPointF
20 | from qgis.PyQt.QtGui import QColor, QCursor, QPixmap, QFont, QTextDocument, QIcon
21 | from qgis.PyQt.QtWidgets import QApplication, QAction, QAbstractButton, QGraphicsItemGroup, QMenu, QInputDialog, QMessageBox, QPushButton
22 | from qgis.core import QgsSettingsRegistryCore, QgsSettingsEntryBool, QgsWkbTypes, QgsProject, QgsVectorLayer, QgsGeometry, QgsPointXY, QgsFeature, QgsEditFormConfig, QgsFeatureRequest, QgsDistanceArea, QgsRectangle, QgsVectorLayerUtils, Qgis, QgsAction, QgsApplication, QgsMapLayer, QgsCoordinateTransform, QgsExpressionContextScope, QgsSettings, QgsMarkerSymbol, QgsTextAnnotation, QgsMessageLog
23 | from qgis.gui import QgsAttributeEditorContext, QgsMapTool, QgsAttributeDialog, QgsRubberBand, QgsAttributeForm, QgsVertexMarker, QgsHighlight, QgsMapCanvasAnnotationItem
24 | from .BezierGeometry import *
25 | from .BezierMarker import *
26 | import math
27 | import numpy as np
28 | from typing import Dict, Any, List
29 |
30 |
31 | class BezierEditingTool(QgsMapTool):
32 |
33 | sLastUsedValues: Dict[str, Dict[int, Any]] = dict()
34 |
35 | def __init__(self, canvas, iface):
36 | QgsMapTool.__init__(self, canvas)
37 |
38 | # qgis interface
39 | self.iface = iface
40 | self.canvas = canvas
41 | self.canvas.destinationCrsChanged.connect(self.crsChanged)
42 | # freehand tool line
43 | self.freehand_rbl = QgsRubberBand(
44 | self.canvas, QgsWkbTypes.LineGeometry)
45 | self.freehand_rbl.setColor(QColor(255, 0, 0, 150))
46 | self.freehand_rbl.setWidth(2)
47 | # snap marker
48 | self.snap_mark = QgsVertexMarker(self.canvas)
49 | self.snap_mark.setColor(QColor(0, 0, 255))
50 | self.snap_mark.setPenWidth(2)
51 | self.snap_mark.setIconType(QgsVertexMarker.ICON_BOX)
52 | self.snap_mark.setIconSize(10)
53 | self.snap_mark.hide()
54 | # snap guide line
55 | self.guide_rbl = QgsRubberBand(self.canvas, QgsWkbTypes.LineGeometry)
56 | self.guide_rbl.setColor(QColor(0, 0, 255, 150))
57 | self.guide_rbl.setWidth(1)
58 | # rectangle selection for unsplit
59 | self.rubberBand = QgsRubberBand(
60 | self.canvas, QgsWkbTypes.PolygonGeometry)
61 | self.rubberBand.setColor(QColor(255, 0, 0, 100))
62 | self.rubberBand.setWidth(1)
63 |
64 | # cursor icon
65 | self.addanchor_cursor = QCursor(
66 | QPixmap(':/plugins/BezierEditing/icon/anchor.svg'), 1, 1)
67 | self.insertanchor_cursor = QCursor(
68 | QPixmap(':/plugins/BezierEditing/icon/anchor_add.svg'), 1, 1)
69 | self.deleteanchor_cursor = QCursor(
70 | QPixmap(':/plugins/BezierEditing/icon/anchor_del.svg'), 1, 1)
71 | self.movehandle_cursor = QCursor(
72 | QPixmap(':/plugins/BezierEditing/icon/handle.svg'), 1, 1)
73 | self.addhandle_cursor = QCursor(
74 | QPixmap(':/plugins/BezierEditing/icon/handle_add.svg'), 1, 1)
75 | self.deletehandle_cursor = QCursor(
76 | QPixmap(':/plugins/BezierEditing/icon/handle_del.svg'), 1, 1)
77 | self.drawline_cursor = QCursor(
78 | QPixmap(':/plugins/BezierEditing/icon/drawline.svg'), 1, 1)
79 | self.split_cursor = QCursor(
80 | QPixmap(':/plugins/BezierEditing/icon/mCrossHair.svg'), -1, -1)
81 | self.unsplit_cursor = QCursor(Qt.ArrowCursor)
82 |
83 | # initialize variable
84 | self.mode = "bezier" # [bezier, freehand , split, unsplit]
85 | # [free, add_anchor,move_anchor,move_handle,insert_anchor,draw_line,drawing_freehand]
86 | self.mouse_state = "free"
87 | self.editing = False # in bezier editing or not
88 | self.freehand_drawing = False # Track if we're in freehand drawing mode
89 | self.freehand_streaming = False # Streaming mode for freehand tool (will be loaded from settings)
90 | self.snapping = None # in snap setting or not
91 | self.show_handle = True # show handle or not
92 | self.editing_feature_id = None # bezier editing feature id
93 | self.editing_geom_type = None # bezier editing geom type
94 | self.clicked_idx = None # clicked anchor or handle idx
95 | self.bg = None # BezierGeometry
96 | self.bm = None # BezierMarker
97 |
98 | # smart guide
99 | self.guideLabelGroup = None
100 | self.smartGuideOn = False
101 | self.snapToLengthUnit = 0
102 | self.snapToAngleUnit = 0
103 | self.generate_menu()
104 |
105 | # interpolation number
106 | s = QgsSettings()
107 | BezierGeometry.INTERPOLATION = int(
108 | s.value("BezierEditing/INTERPOLATION", 10))
109 | # Load streaming mode setting
110 | streaming_val = s.value("BezierEditing/freehand_streaming", False)
111 | if streaming_val is None:
112 | self.freehand_streaming = False
113 | elif isinstance(streaming_val, str):
114 | self.freehand_streaming = streaming_val.lower() == "true"
115 | else:
116 | self.freehand_streaming = bool(streaming_val)
117 |
118 | def tr(self, message):
119 | return QCoreApplication.translate('BezierEditingTool', message)
120 |
121 | def crsChanged(self):
122 | if self.bg is not None:
123 | self.iface.messageBar().pushMessage(
124 | self.tr("Warning"), self.tr("Reset editing data"), level=Qgis.Warning)
125 | self.resetEditing()
126 | self.checkCRS()
127 |
128 | def canvasPressEvent(self, event):
129 | modifiers = QApplication.keyboardModifiers()
130 | layer = self.canvas.currentLayer()
131 | if not layer or layer.type() != QgsMapLayer.VectorLayer:
132 | return
133 | self.checkSnapSetting()
134 | mouse_point, snapped, snap_point, snap_idx = self.getSnapPoint(event)
135 | # bezier tool
136 | if self.mode == "bezier":
137 | # right click
138 | if event.button() == Qt.RightButton:
139 | if bool(modifiers & Qt.ControlModifier):
140 | self.menu.exec_(QCursor.pos())
141 | # left click
142 | elif event.button() == Qt.LeftButton:
143 | # with ctrl
144 | if bool(modifiers & Qt.ControlModifier):
145 | # if click on anchor with ctrl, force to add anchor not moving anchor
146 | if snapped[1]:
147 | if self.editing_geom_type == QgsWkbTypes.PolygonGeometry:
148 | return
149 | self.mouse_state = "add_anchor"
150 | self.clicked_idx = self.bg.anchorCount()
151 | self.bg.add_anchor(self.clicked_idx, snap_point[1])
152 | self.bm.add_anchor(self.clicked_idx, snap_point[1])
153 | # add the anchor snapped by guide. guide is on by ctrl
154 | else:
155 | if self.editing_geom_type == QgsWkbTypes.PolygonGeometry:
156 | return
157 | if not self.editing:
158 | self.bg = BezierGeometry(self.projectCRS)
159 | self.bm = BezierMarker(self.canvas, self.bg)
160 | self.editing = True
161 | self.mouse_state = "add_anchor"
162 | self.clicked_idx = self.bg.anchorCount()
163 | self.bg.add_anchor(self.clicked_idx, snap_point[0])
164 | self.bm.add_anchor(self.clicked_idx, snap_point[0])
165 | # with alt
166 | elif bool(modifiers & Qt.AltModifier):
167 | # if click on anchor with alt, move out a handle from anchor
168 | if snapped[2] and snapped[1]:
169 | self.mouse_state = "move_handle"
170 | self.clicked_idx = snap_idx[2]
171 | # if click on bezier line with alt, insert anchor in bezier line
172 | elif snapped[3] and not snapped[1]:
173 | self.mouse_state = "insert_anchor"
174 | self.bg.insert_anchor(snap_idx[3], snap_point[3])
175 | self.bm.show()
176 | # if click on handle, move handle
177 | elif snapped[2]:
178 | self.mouse_state = "move_handle"
179 | self.clicked_idx = snap_idx[2]
180 | self.bg.move_handle(snap_idx[2], snap_point[2])
181 | self.bm.move_handle(snap_idx[2], snap_point[2])
182 |
183 | # with shift
184 | elif bool(modifiers & Qt.ShiftModifier):
185 | # if click on anchor with shift, delete anchor from bezier line
186 | if snapped[1]:
187 | # polygon's first anchor
188 | if self.editing_geom_type == QgsWkbTypes.PolygonGeometry and snap_idx[1] == self.bg.anchorCount()-1:
189 | self.bg.delete_anchor2(snap_idx[1], snap_point[1])
190 | self.bm.delete_anchor(snap_idx[1])
191 | self.bm.delete_anchor(0)
192 | self.bm.add_anchor(
193 | self.bg.anchorCount(), self.bg.getAnchor(0, revert=True))
194 | else:
195 | self.bg.delete_anchor(snap_idx[1], snap_point[1])
196 | self.bm.delete_anchor(snap_idx[1])
197 |
198 | # if click on handle with shift, move handle to anchor
199 | elif snapped[2]:
200 | self.bg.delete_handle(snap_idx[2], snap_point[2])
201 | point = self.bg.getAnchor(
202 | int(snap_idx[2] / 2), revert=True)
203 | self.bm.move_handle(snap_idx[2], point)
204 | # click with no key
205 | else:
206 | # if click on anchor, move anchor
207 | if snapped[1]:
208 | self.mouse_state = "move_anchor"
209 | self.clicked_idx = snap_idx[1]
210 | if self.editing_geom_type == QgsWkbTypes.PolygonGeometry and snap_idx[1] == (self.bg.anchorCount() - 1):
211 | self.bg.move_anchor2(snap_idx[1], snap_point[1])
212 | self.bm.move_anchor(snap_idx[1], snap_point[1])
213 | self.bm.move_anchor(0, snap_point[1])
214 | else:
215 | self.bg.move_anchor(snap_idx[1], snap_point[1])
216 | self.bm.move_anchor(snap_idx[1], snap_point[1])
217 |
218 | # if click on handle, move handle
219 | elif snapped[2]:
220 | self.mouse_state = "move_handle"
221 | self.clicked_idx = snap_idx[2]
222 | self.bg.move_handle(snap_idx[2], snap_point[2])
223 | self.bm.move_handle(snap_idx[2], snap_point[2])
224 | # if click on canvas, add anchor
225 | else:
226 | if self.editing_geom_type == QgsWkbTypes.PolygonGeometry:
227 | return
228 | if not self.editing:
229 | self.bg = BezierGeometry(self.projectCRS)
230 | self.bm = BezierMarker(self.canvas, self.bg)
231 | self.editing = True
232 | self.mouse_state = "add_anchor"
233 | self.clicked_idx = self.bg.anchorCount()
234 | self.bg.add_anchor(self.clicked_idx, snap_point[0])
235 | self.bm.add_anchor(self.clicked_idx, snap_point[0])
236 | # freehand tool
237 | elif self.mode == "freehand":
238 | # right click with Ctrl - show context menu
239 | if event.button() == Qt.RightButton and bool(modifiers & Qt.ControlModifier):
240 | self.showFreehandContextMenu(event)
241 | return
242 | # left click
243 | elif event.button() == Qt.LeftButton:
244 | # Streaming mode behavior (click-move-click)
245 | if self.freehand_streaming:
246 | # If we're already drawing, finish the line
247 | if self.freehand_drawing:
248 | self.drawlineToBezier(snapped[4])
249 | self.freehand_drawing = False
250 | self.mouse_state = "free"
251 | return
252 |
253 | # Start new drawing
254 | # if click on canvas, freehand drawing start
255 | if not self.editing:
256 | self.bg = BezierGeometry(self.projectCRS)
257 | self.bm = BezierMarker(self.canvas, self.bg)
258 | point = mouse_point
259 | self.bg.add_anchor(0, point, undo=False)
260 | self.editing = True
261 | # if click on bezier line, modified by freehand drawing
262 | elif self.editing and (snapped[1] or snapped[3]):
263 | if snapped[1]:
264 | point = snap_point[1]
265 | elif snapped[3]:
266 | point = snap_point[3]
267 | else:
268 | return
269 | self.freehand_drawing = True
270 | self.mouse_state = "drawing_freehand"
271 | self.freehand_rbl.reset(QgsWkbTypes.LineGeometry)
272 | self.freehand_rbl.addPoint(point)
273 | # Original drag mode behavior
274 | else:
275 | # if click on canvas, freehand drawing start
276 | if not self.editing:
277 | self.bg = BezierGeometry(self.projectCRS)
278 | self.bm = BezierMarker(self.canvas, self.bg)
279 | point = mouse_point
280 | self.bg.add_anchor(0, point, undo=False)
281 | self.editing = True
282 | # if click on bezier line, modified by freehand drawing
283 | elif self.editing and (snapped[1] or snapped[3]):
284 | if snapped[1]:
285 | point = snap_point[1]
286 | elif snapped[3]:
287 | point = snap_point[3]
288 | else:
289 | return
290 | self.mouse_state = "draw_line"
291 | self.freehand_rbl.reset(QgsWkbTypes.LineGeometry)
292 | self.freehand_rbl.addPoint(point)
293 | # split tool
294 | elif self.mode == "split":
295 | # right click
296 | if event.button() == Qt.RightButton:
297 | # if right click in editing, bezier editing finish
298 | if self.editing:
299 | self.finishEditing(layer)
300 | # if right click on feature, bezier editing start
301 | else:
302 | ok = self.startEditing(layer, mouse_point)
303 | if ok:
304 | self.editing = True
305 | # left click
306 | elif event.button() == Qt.LeftButton:
307 | # if click on bezier line, split bezier feature is created
308 | if self.editing and self.editing_feature_id is not None:
309 | type = layer.geometryType()
310 | if type == QgsWkbTypes.LineGeometry:
311 | # split on anchor
312 | if snapped[1]:
313 | lineA, lineB = self.bg.split_line(
314 | snap_idx[1], snap_point[1], isAnchor=True)
315 | # split on line
316 | elif snapped[3]:
317 | lineA, lineB = self.bg.split_line(
318 | snap_idx[3], snap_point[3], isAnchor=False)
319 | else:
320 | return
321 |
322 | if layer.wkbType() == QgsWkbTypes.LineString:
323 | geomA = QgsGeometry.fromPolylineXY(lineA)
324 | geomB = QgsGeometry.fromPolylineXY(lineB)
325 | elif layer.wkbType() == QgsWkbTypes.MultiLineString:
326 | geomA = QgsGeometry.fromMultiPolylineXY([lineA])
327 | geomB = QgsGeometry.fromMultiPolylineXY([lineB])
328 |
329 | feature = self.getFeatureById(
330 | layer, self.editing_feature_id)
331 | _, _ = self.createFeature(
332 | geomB, feature, editmode=False, showdlg=False)
333 | f, _ = self.createFeature(
334 | geomA, feature, editmode=True, showdlg=False)
335 | layer.removeSelection()
336 | layer.select(f.id())
337 | self.resetEditing()
338 |
339 | else:
340 | QMessageBox.warning(None, self.tr("Geometry type is different"), self.tr(
341 | "Only line geometry can be split."))
342 | else:
343 | QMessageBox.warning(
344 | None, self.tr("No feature"), self.tr("No feature to split."))
345 | # unsplit tool
346 | elif self.mode == "unsplit":
347 | # if left click, feature selection
348 | if event.button() == Qt.LeftButton:
349 | self.endPoint = self.startPoint = mouse_point
350 | self.isEmittingPoint = True
351 | self.showRect(self.startPoint, self.endPoint)
352 |
353 | def canvasMoveEvent(self, event):
354 | modifiers = QApplication.keyboardModifiers()
355 | if bool(modifiers & Qt.ControlModifier):
356 | self.smartGuideOn = True
357 | else:
358 | self.smartGuideOn = False
359 | #self.smartGuideOn = self.guideAction.isChecked()
360 | layer = self.canvas.currentLayer()
361 | if not layer or layer.type() != QgsMapLayer.VectorLayer:
362 | return
363 | mouse_point, snapped, snap_point, snap_idx = self.getSnapPoint(event)
364 | # bezier tool
365 | if self.mode == "bezier":
366 | # add anchor and dragging
367 | if self.mouse_state == "add_anchor":
368 | self.canvas.setCursor(self.movehandle_cursor)
369 | withAlt = bool(modifiers & Qt.AltModifier)
370 | withShift = bool(modifiers & Qt.ShiftModifier)
371 | other_handle_idx, other_handle_point, anchor_point = self.bg.move_handle2(
372 | self.clicked_idx, mouse_point, withAlt, withShift)
373 | if withShift:
374 | self.bm.move_handle(other_handle_idx, other_handle_point)
375 | self.bm.move_handle(other_handle_idx + 1, anchor_point)
376 | elif withAlt:
377 | self.bm.move_handle(other_handle_idx + 1, mouse_point)
378 | else:
379 | self.bm.move_handle(other_handle_idx, other_handle_point)
380 | self.bm.move_handle(other_handle_idx + 1, mouse_point)
381 |
382 | # insert anchor
383 | elif self.mouse_state == "insert_anchor":
384 | pass
385 | # move handle
386 | elif self.mouse_state == "move_handle":
387 | self.canvas.setCursor(self.movehandle_cursor)
388 | point = snap_point[0]
389 | withAlt = bool(modifiers & Qt.AltModifier)
390 | if withAlt:
391 | self.bg.move_handle(self.clicked_idx, point, undo=False)
392 | self.bm.move_handle(self.clicked_idx, point)
393 | other_handle_idx, other_point = self.bg.other_handle(
394 | self.clicked_idx, point)
395 | self.bg.move_handle(
396 | other_handle_idx, other_point, undo=False)
397 | self.bm.move_handle(other_handle_idx, other_point)
398 | else:
399 |
400 | self.bg.move_handle(self.clicked_idx, point, undo=False)
401 | self.bm.move_handle(self.clicked_idx, point)
402 | # add handle
403 | elif bool(modifiers & Qt.AltModifier) and snapped[1] and snapped[2]:
404 | self.canvas.setCursor(self.addhandle_cursor)
405 | # insert anchor
406 | elif bool(modifiers & Qt.AltModifier) and snapped[3] and not snapped[1]:
407 | self.canvas.setCursor(self.insertanchor_cursor)
408 | # force to add anchor
409 | elif bool(modifiers & Qt.ControlModifier) and snapped[1]:
410 | self.canvas.setCursor(self.insertanchor_cursor)
411 | # delete anchor
412 | elif bool(modifiers & Qt.ShiftModifier) and snapped[1]:
413 | self.canvas.setCursor(self.deleteanchor_cursor)
414 | # delete handle
415 | elif bool(modifiers & Qt.ShiftModifier) and snapped[2]:
416 | self.canvas.setCursor(self.deletehandle_cursor)
417 |
418 | # move anchor
419 | elif self.mouse_state == "move_anchor":
420 | point = snap_point[0]
421 | if snapped[1]:
422 | point = snap_point[1]
423 | self.bg.move_anchor(self.clicked_idx, point, undo=False)
424 | self.bm.move_anchor(self.clicked_idx, point)
425 | if self.editing_geom_type == QgsWkbTypes.PolygonGeometry and self.clicked_idx == (self.bg.anchorCount() - 1):
426 | self.bg.move_anchor(0, point, undo=False)
427 | self.bm.move_anchor(0, point)
428 | # free moving
429 | else:
430 | # on anchor
431 | if snapped[1]:
432 | self.canvas.setCursor(self.movehandle_cursor)
433 | # on handle
434 | elif snapped[2]:
435 | self.canvas.setCursor(self.movehandle_cursor)
436 | # on canvas
437 | else:
438 | self.canvas.setCursor(self.addanchor_cursor)
439 | # freehand tool
440 | elif self.mode == "freehand":
441 | self.canvas.setCursor(self.drawline_cursor)
442 | # Streaming mode - continuously add points following mouse
443 | if self.freehand_streaming and self.freehand_drawing and self.mouse_state == "drawing_freehand":
444 | point = mouse_point
445 | # on start anchor
446 | if snapped[4]:
447 | point = snap_point[4]
448 | self.freehand_rbl.addPoint(point)
449 | # Original drag mode - add points while dragging
450 | elif not self.freehand_streaming and self.mouse_state == "draw_line":
451 | point = mouse_point
452 | # on start anchor
453 | if snapped[4]:
454 | point = snap_point[4]
455 | self.freehand_rbl.addPoint(point)
456 | # split tool
457 | elif self.mode == "split":
458 | self.canvas.setCursor(self.split_cursor)
459 | # unsplit tool
460 | elif self.mode == "unsplit":
461 | # if dragging, draw rectangle area
462 | self.canvas.setCursor(self.unsplit_cursor)
463 | if not self.isEmittingPoint:
464 | return
465 | self.endPoint = mouse_point
466 | self.showRect(self.startPoint, self.endPoint)
467 |
468 | def canvasReleaseEvent(self, event):
469 | modifiers = QApplication.keyboardModifiers()
470 | layer = self.canvas.currentLayer()
471 | if not layer or layer.type() != QgsMapLayer.VectorLayer:
472 | return
473 | mouse_point, snapped, snap_point, _ = self.getSnapPoint(event)
474 | if event.button() == Qt.LeftButton:
475 | # bezier tool
476 | if self.mode == "bezier":
477 | self.clicked_idx = None
478 | self.mouse_state = "free"
479 | # freehand tool
480 | elif self.mode == "freehand":
481 | # Streaming mode - don't process release event
482 | if self.freehand_streaming:
483 | pass
484 | # Original drag mode - convert drawing line to bezier line
485 | else:
486 | if self.mouse_state != "free":
487 | self.drawlineToBezier(snapped[4])
488 | self.mouse_state = "free"
489 | # split tool
490 | elif self.mode == "split":
491 | self.clicked_idx = None
492 | self.mouse_state = "free"
493 | # unsplit tool
494 | elif self.mode == "unsplit":
495 | # if feature in selection, select feature
496 | self.isEmittingPoint = False
497 | r = self.rectangleArea()
498 | if r is not None:
499 | self.resetUnsplit()
500 | self.selectFeatures(mouse_point, r)
501 | else:
502 | self.selectFeatures(mouse_point)
503 | if self.bm is not None:
504 | self.bm.show_handle(self.show_handle)
505 | elif event.button() == Qt.RightButton:
506 | if self.mode == "bezier":
507 | if bool(modifiers & Qt.ControlModifier):
508 | return
509 | elif self.editing:
510 | # if right click on first anchor in editing, flip bezier line
511 | if snapped[4] and self.bg.anchorCount() > 1:
512 | self.bg.flip_line()
513 | self.bm.show(self.show_handle)
514 | # if right click in editing, bezier editing finish
515 | else:
516 | self.finishEditing(layer)
517 | # if right click on feature, bezier editing start
518 | else:
519 | ok = self.startEditing(layer, mouse_point)
520 | if ok:
521 | self.editing = True
522 | # freehand tool
523 | elif self.mode == "freehand":
524 | # Ctrl+right click is handled in canvasPressEvent
525 | if bool(modifiers & Qt.ControlModifier):
526 | return
527 | # if right click in editing, bezier editing finish
528 | elif self.editing:
529 | self.finishEditing(layer)
530 | # if right click on feature, bezier editing start
531 | else:
532 | ok = self.startEditing(layer, mouse_point)
533 | if ok:
534 | self.editing = True
535 | # if right click, selected bezier feature are unsplit
536 | elif self.mode == "unsplit":
537 | self.unsplit()
538 |
539 | def startEditing(self, layer, mouse_point):
540 | """
541 | convert feature to bezier line and start editing
542 | """
543 | ok = False
544 | near, feat = self.getNearFeatures(layer, mouse_point)
545 | if near:
546 | # First try to edit the selected feature. If not, edit the last feature of the table.
547 | edit_feature = feat[-1]
548 | feat_ids = [f.id() for f in feat]
549 | for selected_id in layer.selectedFeatureIds():
550 | if selected_id in feat_ids:
551 | edit_feature = feat[feat_ids.index(selected_id)]
552 | geom_type = self.convertFeatureToBezier(edit_feature)
553 | if geom_type is not None:
554 | self.editing_feature_id = edit_feature.id()
555 | self.editing_geom_type = geom_type
556 | ok = True
557 | return ok
558 |
559 | def finishEditing(self, layer):
560 | """
561 | convert bezier line to feature and finish editing
562 | """
563 | layer_type = layer.geometryType()
564 | layer_wkbtype = layer.wkbType()
565 | result, geom = self.bg.asGeometry(layer_type, layer_wkbtype)
566 | # no geometry to convert
567 | if result is None:
568 | continueFlag = False
569 | # the layer geometry type is different
570 | elif result is False:
571 | reply = QMessageBox.question(None, self.tr("Continue editing?"), self.tr(
572 | "Geometry type of the layer is different, or polygon isn't closed. Do you want to continue editing?"), QMessageBox.Yes,
573 | QMessageBox.No)
574 | if reply == QMessageBox.Yes:
575 | continueFlag = True
576 | else:
577 | continueFlag = False
578 | else:
579 | # create new feature
580 | if self.editing_feature_id is None:
581 | f, continueFlag = self.createFeature(
582 | geom, None, editmode=False)
583 | # modify the feature
584 | else:
585 | feature = self.getFeatureById(layer, self.editing_feature_id)
586 | if feature is None:
587 | reply = QMessageBox.question(None, self.tr("No feature"),
588 | self.tr(
589 | "No feature found. Do you want to continue editing?"),
590 | QMessageBox.Yes,
591 | QMessageBox.No)
592 | if reply == QMessageBox.Yes:
593 | continueFlag = True
594 | else:
595 | continueFlag = False
596 | else:
597 | f, continueFlag = self.createFeature(
598 | geom, feature, editmode=True)
599 | if continueFlag is False:
600 | self.resetEditing()
601 | self.canvas.refresh()
602 |
603 | def drawlineToBezier(self, snap_to_start):
604 | """
605 | convert drawing line to bezier line
606 | """
607 | geom = self.freehand_rbl.asGeometry()
608 | scale = self.canvas.scale()
609 | layer = self.canvas.currentLayer()
610 | layer_type = layer.geometryType()
611 | self.bg.modified_by_geometry(geom, layer_type, scale, snap_to_start)
612 | self.bm.show()
613 | self.freehand_rbl.reset()
614 |
615 | def resetEditing(self):
616 | """
617 | reset bezier setting
618 | """
619 | self.bm.reset()
620 | self.bg.reset()
621 | self.bg = None
622 | self.bm = None
623 | self.editing_feature_id = None
624 | self.editing_geom_type = None
625 | self.editing = False
626 | self.freehand_drawing = False
627 | self.mouse_state = "free"
628 |
629 | def convertFeatureToBezier(self, feature):
630 | """
631 | convert feature to bezier line
632 | """
633 | geom_type = None
634 | geom = QgsGeometry(feature.geometry())
635 | self.checkCRS()
636 | if self.layerCRS.srsid() != self.projectCRS.srsid():
637 | geom.transform(QgsCoordinateTransform(
638 | self.layerCRS, self.projectCRS, QgsProject.instance()))
639 |
640 | button_line = QPushButton(
641 | QIcon(QgsApplication.getThemeIcon("/mIconLineLayer.svg")), self.tr("Line"))
642 | button_curve = QPushButton(QIcon(QgsApplication.getThemeIcon(
643 | "/mActionDigitizeWithCurve.svg")), self.tr("Curve"))
644 | msgbox_convert = QMessageBox()
645 | msgbox_convert.setWindowTitle(self.tr("Convert to Bezier"))
646 | msgbox_convert.setIcon(QMessageBox.Question)
647 | msgbox_convert.setText(self.tr(
648 | "The feature isn't created by Bezier Tool or ver 1.3 higher.\n\n" +
649 | "Do you want to convert to Bezier?\n\n" +
650 | "Conversion can be done either to line segments or to fitting curve.\nPlease select conversion mode."))
651 | msgbox_convert.addButton(button_line, QMessageBox.ApplyRole)
652 | msgbox_convert.addButton(button_curve, QMessageBox.ApplyRole)
653 | msgbox_button_cancel = msgbox_convert.addButton(QMessageBox.Cancel)
654 |
655 | if geom.type() == QgsWkbTypes.PointGeometry:
656 | point = geom.asPoint()
657 | self.bg = BezierGeometry.convertPointToBezier(
658 | self.projectCRS, point)
659 | self.bm = BezierMarker(self.canvas, self.bg)
660 | self.bm.add_anchor(0, point)
661 | geom_type = geom.type()
662 | elif geom.type() == QgsWkbTypes.LineGeometry:
663 | geom.convertToSingleType()
664 | polyline = geom.asPolyline()
665 | is_bezier = BezierGeometry.checkIsBezier(self.projectCRS, polyline)
666 | if is_bezier:
667 | self.bg = BezierGeometry.convertLineToBezier(
668 | self.projectCRS, polyline)
669 | self.bm = BezierMarker(self.canvas, self.bg)
670 | self.bm.show(self.show_handle)
671 | geom_type = geom.type()
672 | else:
673 | msgbox_convert.exec()
674 | if msgbox_convert.clickedButton() != msgbox_button_cancel:
675 | if msgbox_convert.clickedButton() == button_line:
676 | linetype = "line"
677 | elif msgbox_convert.clickedButton() == button_curve:
678 | linetype = "curve"
679 | self.bg = BezierGeometry.convertLineToBezier(
680 | self.projectCRS, polyline, linetype)
681 | self.bm = BezierMarker(self.canvas, self.bg)
682 | self.bm.show(self.show_handle)
683 | geom_type = geom.type()
684 |
685 | elif geom.type() == QgsWkbTypes.PolygonGeometry:
686 | geom.convertToSingleType()
687 | polygon = geom.asPolygon()
688 | is_bezier = BezierGeometry.checkIsBezier(
689 | self.projectCRS, polygon[0])
690 | if is_bezier:
691 | self.bg = BezierGeometry.convertLineToBezier(
692 | self.projectCRS, polygon[0])
693 | self.bm = BezierMarker(self.canvas, self.bg)
694 | self.bm.show(self.show_handle)
695 | geom_type = geom.type()
696 | else:
697 | msgbox_convert.exec()
698 | if msgbox_convert.clickedButton() != msgbox_button_cancel:
699 | if msgbox_convert.clickedButton() == button_line:
700 | linetype = "line"
701 | elif msgbox_convert.clickedButton() == button_curve:
702 | linetype = "curve"
703 | self.bg = BezierGeometry.convertLineToBezier(
704 | self.projectCRS, polygon[0], linetype)
705 | self.bm = BezierMarker(self.canvas, self.bg)
706 | self.bm.show(self.show_handle)
707 | geom_type = geom.type()
708 |
709 | else:
710 | QMessageBox.warning(None, self.tr("Not supported type"), self.tr(
711 | "Geometry type of the layer is not supported."))
712 |
713 | return geom_type
714 |
715 | def createFeature(self, geom, feature, editmode=True, showdlg=True):
716 | """
717 | create or edit feature
718 | Referred to
719 | https://github.com/EnMAP-Box/qgispluginsupport/blob/master/qps/maptools.py#L717
720 | """
721 | continueFlag = False
722 | layer = self.canvas.currentLayer()
723 | fields = layer.fields()
724 |
725 | self.checkCRS()
726 | if self.layerCRS.srsid() != self.projectCRS.srsid():
727 | geom.transform(QgsCoordinateTransform(
728 | self.projectCRS, self.layerCRS, QgsProject.instance()))
729 |
730 |
731 | initialAttributeValues = dict()
732 | settings = QgsSettings()
733 | val = settings.value("qgis/digitizing/reuseLastValues", False)
734 | if val is None:
735 | reuseLastValues = False
736 | elif isinstance(val, str):
737 | reuseLastValues = val.lower() == "true"
738 | else:
739 | reuseLastValues = bool(val)
740 |
741 | lyr: QgsVectorLayer = layer
742 | for idx in range(lyr.fields().count()):
743 | if feature is not None:
744 | initialAttributeValues[idx] = feature.attributes()[idx]
745 | elif lyr.dataProvider().defaultValueClause(idx)!="":
746 | initialAttributeValues[idx] = lyr.dataProvider().defaultValueClause(idx)
747 | elif (reuseLastValues or lyr.editFormConfig().reuseLastValue(idx)) and layer.id() in self.sLastUsedValues.keys() and idx in self.sLastUsedValues[lyr.id()].keys():
748 | lastUsed = self.sLastUsedValues[lyr.id()][idx]
749 | initialAttributeValues[idx] = lastUsed
750 |
751 | context = layer.createExpressionContext()
752 | f = QgsVectorLayerUtils.createFeature(
753 | layer, geom, initialAttributeValues, context)
754 | newFeature = QgsFeature(f)
755 |
756 | settings = QgsSettings()
757 | val = settings.value(
758 | "qgis/digitizing/disable_enter_attribute_values_dialog", False
759 | )
760 | if val is None:
761 | disable_attributes = False
762 | elif isinstance(val, str):
763 | disable_attributes = val.lower() == "true"
764 | else:
765 | disable_attributes = bool(val)
766 |
767 | if disable_attributes or showdlg is False or fields.count() == 0:
768 | if not editmode:
769 | layer.beginEditCommand(self.tr("Bezier added"))
770 | layer.addFeature(newFeature)
771 | else:
772 | # if using changeGeometry function, crashed... it's bug? So using add and delete
773 | layer.beginEditCommand(self.tr("Bezier edited"))
774 | layer.addFeature(newFeature)
775 | layer.deleteFeature(feature.id())
776 | layer.endEditCommand()
777 | else:
778 | if not editmode:
779 | dlg = QgsAttributeDialog(layer, newFeature, True)
780 | dlg.setAttribute(Qt.WA_DeleteOnClose)
781 | dlg.setMode(QgsAttributeEditorContext.AddFeatureMode)
782 | dlg.setEditCommandMessage(self.tr("Bezier added"))
783 | dlg.attributeForm().featureSaved.connect(
784 | lambda f, form=dlg.attributeForm(): self.onFeatureSaved(f, form))
785 | ok = dlg.exec_()
786 | if not ok:
787 | reply = QMessageBox.question(None, self.tr("Continue editing?"), self.tr("Do you want to continue editing?"),
788 | QMessageBox.Yes,
789 | QMessageBox.No)
790 | if reply == QMessageBox.Yes:
791 | continueFlag = True
792 | else:
793 | layer.beginEditCommand("Bezier edited")
794 | dlg = self.iface.getFeatureForm(layer, feature)
795 | ok = dlg.exec_()
796 | if ok:
797 | layer.changeGeometry(feature.id(), geom)
798 | layer.endEditCommand()
799 | else:
800 | layer.destroyEditCommand()
801 | reply = QMessageBox.question(None, self.tr("Continue editing?"), self.tr("Do you want to continue editing?"),
802 | QMessageBox.Yes,
803 | QMessageBox.No)
804 | if reply == QMessageBox.Yes:
805 | continueFlag = True
806 |
807 | return newFeature, continueFlag
808 |
809 | def onFeatureSaved(self, feature: QgsFeature, form: QgsAttributeForm):
810 | form = self.sender()
811 | if not isinstance(form, QgsAttributeForm):
812 | return
813 |
814 | settings = QgsSettings()
815 | val = settings.value("qgis/digitizing/reuseLastValues", False)
816 | if val is None:
817 | reuseLastValues = False
818 | elif isinstance(val, str):
819 | reuseLastValues = val.lower() == "true"
820 | else:
821 | reuseLastValues = bool(val)
822 | lyr = self.canvas.currentLayer()
823 | fields = lyr.fields()
824 | origValues: Dict[int, Any] = self.sLastUsedValues.get(
825 | lyr.id(), dict())
826 | newValues: List = feature.attributes()
827 | for idx in range(fields.count()):
828 | if(reuseLastValues or lyr.editFormConfig().reuseLastValue(idx)):
829 | origValues[idx] = newValues[idx]
830 | self.sLastUsedValues[lyr.id()] = origValues
831 |
832 | def undo(self):
833 | """
834 | undo bezier editing (add, move, delete , draw) for anchor and handle
835 | """
836 | if self.bg is not None:
837 | history_length = self.bg.undo()
838 | self.bm.show(self.show_handle)
839 | if history_length == 0:
840 | self.resetEditing()
841 |
842 | def showHandle(self, checked):
843 | """
844 | change bezier handle visibility
845 | """
846 | self.show_handle = checked
847 | if self.bm is not None:
848 | self.bm.show_handle(checked)
849 |
850 | def showFreehandContextMenu(self, event):
851 | """
852 | Show context menu for freehand tool settings
853 | """
854 | menu = QMenu()
855 |
856 | # Add streaming mode action
857 | streamingAction = QAction(self.tr("Streaming"), menu)
858 | streamingAction.setCheckable(True)
859 | streamingAction.setChecked(self.freehand_streaming)
860 | streamingAction.triggered.connect(self.toggleFreehandStreaming)
861 | menu.addAction(streamingAction)
862 |
863 | # Show menu at cursor position
864 | menu.exec_(self.canvas.mapToGlobal(event.pos()))
865 |
866 | def toggleFreehandStreaming(self):
867 | """
868 | Toggle streaming mode for freehand tool
869 | """
870 | self.freehand_streaming = not self.freehand_streaming
871 | # Save the setting
872 | s = QgsSettings()
873 | s.setValue("BezierEditing/freehand_streaming", self.freehand_streaming)
874 | # Reset any ongoing drawing when switching modes
875 | if self.freehand_drawing:
876 | self.freehand_drawing = False
877 | self.mouse_state = "free"
878 | self.freehand_rbl.reset()
879 |
880 | def getSnapPoint(self, event):
881 | """
882 | return mouse point and snapped point list.
883 | snapped point list is 0:map, 1:anchor, 2:handle, 3:bezier line, 4:start anchor
884 | """
885 | snap_idx = ["", "", "", "", "", ""]
886 | snapped = [False, False, False, False, False, False]
887 | snap_point = [None, None, None, None, None, None]
888 |
889 | self.snap_mark.hide()
890 | self.guide_rbl.reset(QgsWkbTypes.LineGeometry)
891 | if self.guideLabelGroup is not None:
892 | self.canvas.scene().removeItem(self.guideLabelGroup)
893 | self.guideLabelGroup = None
894 | self.guideLabelGroup = QGraphicsItemGroup()
895 | self.canvas.scene().addItem(self.guideLabelGroup)
896 |
897 | mouse_point = self.toMapCoordinates(event.pos())
898 | snapped[0], snap_point[0] = self.checkSnapToPoint(event.pos())
899 |
900 | if self.bg is not None:
901 | point = snap_point[0]
902 | snap_distance = self.canvas.scale() / 500
903 | #d = self.canvas.mapUnitsPerPixel() * 4
904 | snapped[1], snap_point[1], snap_idx[1] = self.bg.checkSnapToAnchor(
905 | point, self.clicked_idx, snap_distance)
906 | if self.show_handle and self.mode == "bezier":
907 | snapped[2], snap_point[2], snap_idx[2] = self.bg.checkSnapToHandle(
908 | point, snap_distance)
909 | snapped[3], snap_point[3], snap_idx[3] = self.bg.checkSnapToLine(
910 | point, snap_distance)
911 | snapped[4], snap_point[4], snap_idx[4] = self.bg.checkSnapToStart(
912 | point, snap_distance)
913 |
914 | if self.smartGuideOn and self.mode == "bezier" and self.bg.anchorCount() > 0 and not snapped[1]:
915 | # calc the angle from line made by point0 and point1
916 | if self.bg.anchorCount() >= 2:
917 | origin_point0 = self.bg.getAnchor(-2, revert=True)
918 | origin_point1 = self.bg.getAnchor(-1, revert=True)
919 | # calc the angle from horizontal line
920 | elif self.bg.anchorCount() == 1:
921 | origin_point0 = None
922 | origin_point1 = self.bg.getAnchor(0, revert=True)
923 | guide_point = self.smartGuide(
924 | origin_point0, origin_point1, snap_point[0], doSnap=True)
925 | snap_point[0] = guide_point
926 | snap_point[1] = guide_point
927 | # show snap marker, but didn't show to line snap
928 | for i in [0, 1, 2, 4]:
929 | if snapped[i]:
930 | self.snap_mark.setCenter(snap_point[i])
931 | self.snap_mark.show()
932 | break
933 |
934 | return mouse_point, snapped, snap_point, snap_idx
935 |
936 | def generate_menu(self):
937 | self.menu = QMenu()
938 | self.menu.addAction(self.tr("Guide settings...")
939 | ).triggered.connect(self.guide_snap_setting)
940 | self.menu.addAction(self.tr("Reset guide")
941 | ).triggered.connect(self.clear_guide)
942 | self.menu.addSeparator()
943 | self.menu.addAction(self.tr("Advanced settings...")
944 | ).triggered.connect(self.interpolate_setting)
945 | self.menu.addSeparator()
946 | self.closeAction = self.menu.addAction(self.tr("Close"))
947 |
948 | def guide_snap_setting(self):
949 | num, ok = QInputDialog.getInt(QInputDialog(), self.tr("Set snap to angle"), self.tr(
950 | "Enter snap angle (degree)"), self.snapToAngleUnit, 0, 90)
951 | if ok:
952 | self.snapToAngleUnit = num
953 | num, ok = QInputDialog.getInt(QInputDialog(), self.tr("Set snap to length"), self.tr(
954 | "Enter snap length (in case of LatLon in seconds)"), self.snapToLengthUnit, 0)
955 | if ok:
956 | if self.projectCRS.projectionAcronym() == "longlat":
957 | self.snapToLengthUnit = num/3600
958 | else:
959 | self.snapToLengthUnit = num
960 |
961 | def clear_guide(self):
962 | self.snapToAngleUnit = 0
963 | self.snapToLengthUnit = 0
964 |
965 | def interpolate_setting(self):
966 | if self.bg is not None:
967 | QMessageBox.warning(
968 | None, self.tr("Warning"), self.tr("Can't be set while editing."))
969 | return
970 |
971 | QMessageBox.warning(
972 | None, self.tr("Warning"), self.tr("Be careful when changing values, as Bezier curves with different numbers of interpolants will not be converted accurately."))
973 | num, ok = QInputDialog.getInt(QInputDialog(), self.tr("Count"), self.tr(
974 | "Enter Interpolate Point Count (default is 10)"), BezierGeometry.INTERPOLATION, 5, 99)
975 | if ok:
976 | BezierGeometry.INTERPOLATION = num
977 | s = QgsSettings()
978 | s.setValue("BezierEditing/INTERPOLATION", num)
979 |
980 | def lengthSnapPoint(self, origin_point, point):
981 | v = point - origin_point
982 | theta = math.atan2(v.y(), v.x())
983 | org_length = origin_point.distance(point)
984 | if self.snapToLengthUnit == 0:
985 | snap_length = org_length
986 | else:
987 | snap_length = ((org_length + self.snapToLengthUnit / 2.0) //
988 | self.snapToLengthUnit) * self.snapToLengthUnit
989 | snap_point = QgsPointXY(origin_point.x() + snap_length * math.cos(theta),
990 | origin_point.y() + snap_length * math.sin(theta))
991 | return snap_point, snap_length, org_length
992 |
993 | def angleSnapPoint(self, origin_point0, origin_point1, point):
994 |
995 | if (origin_point0 is None or (origin_point1.x() == point.x() and origin_point1.y() == point.y())):
996 | v = point - origin_point1
997 | theta = math.atan2(v.y(), v.x())
998 | org_deg = math.degrees(theta)
999 | if self.snapToAngleUnit == 0:
1000 | snap_deg = org_deg
1001 | snap_point = point
1002 | else:
1003 | snap_deg = ((org_deg + self.snapToAngleUnit / 2.0) //
1004 | self.snapToAngleUnit) * self.snapToAngleUnit
1005 | snap_theta = math.radians(snap_deg)
1006 | if snap_deg == 90 or snap_deg == -90:
1007 | snap_point = QgsPointXY(
1008 | origin_point1.x(), origin_point1.y() + v.y())
1009 | else:
1010 | snap_point = QgsPointXY(
1011 | origin_point1.x() + v.x(), origin_point1.y() + math.tan(snap_theta) * v.x())
1012 |
1013 | else:
1014 | v_a = origin_point1 - origin_point0
1015 | v_b = point - origin_point1
1016 |
1017 | rot_theta = math.atan2(v_a.y(), v_a.x())
1018 | v_b_rot = QgsPointXY(math.cos(-rot_theta) * v_b.x() - math.sin(-rot_theta) * v_b.y(),
1019 | math.sin(-rot_theta) * v_b.x() + math.cos(-rot_theta) * v_b.y())
1020 | theta = math.atan2(v_b_rot.y(), v_b_rot.x())
1021 | org_deg = math.degrees(theta)
1022 | if self.snapToAngleUnit == 0:
1023 | snap_deg = org_deg
1024 | snap_point = point
1025 | else:
1026 | snap_deg = ((org_deg + self.snapToAngleUnit / 2.0) //
1027 | self.snapToAngleUnit) * self.snapToAngleUnit
1028 | snap_theta = math.radians(snap_deg)
1029 | if snap_deg == 90 or snap_deg == -90:
1030 | v_b_rot_snap = QgsPointXY(0, v_b_rot.y())
1031 | else:
1032 | v_b_rot_snap = QgsPointXY(
1033 | v_b_rot.x(), math.tan(snap_theta)*v_b_rot.x())
1034 | v_b_rot2 = QgsPointXY(math.cos(rot_theta) * v_b_rot_snap.x() - math.sin(rot_theta) * v_b_rot_snap.y(),
1035 | math.sin(rot_theta) * v_b_rot_snap.x() + math.cos(rot_theta) * v_b_rot_snap.y())
1036 | snap_point = QgsPointXY(
1037 | v_b_rot2.x() + origin_point1.x(), v_b_rot2.y() + origin_point1.y())
1038 |
1039 | return snap_point, snap_deg, org_deg
1040 |
1041 | def smartGuide(self, origin_point0, origin_point1, point, doSnap=False):
1042 | snapped_angle = False
1043 | snapped_length = False
1044 |
1045 | if doSnap:
1046 | snap_point, snap_deg, org_deg = self.angleSnapPoint(
1047 | origin_point0, origin_point1, point)
1048 | if self.snapToAngleUnit > 0:
1049 | guide_point = snap_point
1050 | guide_deg = snap_deg
1051 | snapped_angle = True
1052 | else:
1053 | guide_point = point
1054 | guide_deg = org_deg
1055 |
1056 | snap_point, snap_length, org_length = self.lengthSnapPoint(
1057 | origin_point1, guide_point)
1058 | snap_distance = self.canvas.scale() / 500
1059 | if self.snapToLengthUnit > 0 and abs(snap_length-org_length) < snap_distance:
1060 | guide_point = snap_point
1061 | guide_length = snap_length
1062 | snapped_length = True
1063 | else:
1064 | guide_point = guide_point
1065 | guide_length = org_length
1066 |
1067 | angle_text = "{:.1f}°".format(guide_deg)
1068 | gl = self.guideLabel(angle_text, origin_point1, snapped_angle)
1069 | self.guideLabelGroup.addToGroup(gl)
1070 | if self.projectCRS.projectionAcronym() == "longlat":
1071 | length_text = "{:.1f}″".format(guide_length*3600)
1072 | else:
1073 | length_text = "{:.1f}m".format(guide_length)
1074 | gl = self.guideLabel(length_text, origin_point1 +
1075 | (guide_point - origin_point1) / 2, snapped_length)
1076 | self.guideLabelGroup.addToGroup(gl)
1077 |
1078 | self.snap_mark.setCenter(guide_point)
1079 | self.snap_mark.show()
1080 |
1081 | v = guide_point - origin_point1
1082 | self.guide_rbl.addPoint(origin_point1 - v*(10000.0))
1083 | self.guide_rbl.addPoint(guide_point + v*(10000.0))
1084 | self.guide_rbl.show()
1085 |
1086 | return guide_point
1087 |
1088 | def guideLabel(self, text, position, snapped=False):
1089 | symbol = QgsMarkerSymbol()
1090 | symbol.setSize(0)
1091 | font = QFont()
1092 | font.setPointSize(12)
1093 | lbltext = QTextDocument()
1094 | lbltext.setDefaultFont(font)
1095 | if snapped:
1096 | lbltext.setHtml("" + text + "")
1097 | self.guide_rbl.setColor(QColor(255, 0, 0, 150))
1098 | else:
1099 | lbltext.setHtml("" + text + "")
1100 | self.guide_rbl.setColor(QColor(0, 0, 255, 150))
1101 | label = QgsTextAnnotation()
1102 | label.setMapPosition(position)
1103 | label.setFrameOffsetFromReferencePoint(QPointF(15, -30))
1104 | label.setDocument(lbltext)
1105 | label.setFrameSize(lbltext.size())
1106 | fs = label.fillSymbol()
1107 | fs.setOpacity(0)
1108 | label.setMarkerSymbol(symbol)
1109 | return QgsMapCanvasAnnotationItem(label, self.canvas)
1110 |
1111 | def checkSnapToPoint(self, point):
1112 | snapped = False
1113 | snap_point = self.toMapCoordinates(point)
1114 | if self.snapping:
1115 | snapper = self.canvas.snappingUtils()
1116 | snapMatch = snapper.snapToMap(point)
1117 | if snapMatch.hasVertex():
1118 | snap_point = snapMatch.point()
1119 | snapped = True
1120 | elif snapMatch.hasEdge():
1121 | snap_point = snapMatch.point()
1122 | snapped = True
1123 | return snapped, snap_point
1124 |
1125 | def getFeatureById(self, layer, featid):
1126 | features = [f for f in layer.getFeatures(
1127 | QgsFeatureRequest().setFilterFids([featid]))]
1128 | if len(features) != 1:
1129 | return None
1130 | else:
1131 | return features[0]
1132 |
1133 | def getNearFeatures(self, layer, point, rect=None):
1134 | if rect is None:
1135 | dist = self.canvas.mapUnitsPerPixel() * 4
1136 | rect = QgsRectangle(
1137 | (point.x() - dist), (point.y() - dist), (point.x() + dist), (point.y() + dist))
1138 | self.checkCRS()
1139 | if self.layerCRS.srsid() != self.projectCRS.srsid():
1140 | rectGeom = QgsGeometry.fromRect(rect)
1141 | rectGeom.transform(QgsCoordinateTransform(
1142 | self.projectCRS, self.layerCRS, QgsProject.instance()))
1143 | rect = rectGeom.boundingBox()
1144 | request = QgsFeatureRequest()
1145 | request.setFilterRect(rect)
1146 | f = [feat for feat in layer.getFeatures(request)]
1147 | if len(f) == 0:
1148 | return False, None
1149 | else:
1150 | return True, f
1151 |
1152 | def checkSnapSetting(self):
1153 | snap_cfg = self.iface.mapCanvas().snappingUtils().config()
1154 | if snap_cfg.enabled():
1155 | self.snapping = True
1156 |
1157 | else:
1158 | self.snapping = False
1159 |
1160 | def checkCRS(self):
1161 | self.projectCRS = self.canvas.mapSettings().destinationCrs()
1162 | if self.canvas.currentLayer() is not None:
1163 | self.layerCRS = self.canvas.currentLayer().crs()
1164 |
1165 | def selectFeatures(self, point, rect=None):
1166 | # layers = QgsMapLayerRegistry.instance().mapLayers().values()
1167 | layers = QgsProject.instance().layerTreeRoot().findLayers()
1168 | for layer in layers:
1169 | if layer.layer().type() != QgsMapLayer.VectorLayer:
1170 | continue
1171 | near = self.selectNearFeature(layer.layer(), point, rect)
1172 | if near and rect is None:
1173 | break
1174 | elif not near:
1175 | layer.layer().removeSelection()
1176 |
1177 | def showRect(self, startPoint, endPoint):
1178 | self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
1179 | if startPoint.x() == endPoint.x() or startPoint.y() == endPoint.y():
1180 | return
1181 |
1182 | point1 = QgsPointXY(startPoint.x(), startPoint.y())
1183 | point2 = QgsPointXY(startPoint.x(), endPoint.y())
1184 | point3 = QgsPointXY(endPoint.x(), endPoint.y())
1185 | point4 = QgsPointXY(endPoint.x(), startPoint.y())
1186 |
1187 | self.rubberBand.addPoint(point1, False)
1188 | self.rubberBand.addPoint(point2, False)
1189 | self.rubberBand.addPoint(point3, False)
1190 | self.rubberBand.addPoint(point4, True) # true to update canvas
1191 | self.rubberBand.show()
1192 |
1193 | def rectangleArea(self):
1194 | if self.startPoint is None or self.endPoint is None:
1195 | return None
1196 | elif self.startPoint.x() == self.endPoint.x() or self.startPoint.y() == self.endPoint.y():
1197 | return None
1198 |
1199 | return QgsRectangle(self.startPoint, self.endPoint)
1200 |
1201 | def selectNearFeature(self, layer, point, rect=None):
1202 | if rect is not None:
1203 | layer.removeSelection()
1204 | near, features = self.getNearFeatures(layer, point, rect)
1205 | if near:
1206 | fids = [f.id() for f in features]
1207 | if rect is not None:
1208 | layer.selectByIds(fids)
1209 | else:
1210 | for fid in fids:
1211 | if self.isSelected(layer, fid):
1212 | layer.deselect(fid)
1213 | else:
1214 | layer.select(fid)
1215 | return near
1216 |
1217 | def isSelected(self, layer, fid):
1218 | for sid in layer.selectedFeatureIds():
1219 | if sid == fid:
1220 | return True
1221 | return False
1222 |
1223 | def resetUnsplit(self):
1224 | self.startPoint = self.endPoint = None
1225 | self.isEmittingPoint = False
1226 | self.rubberBand.reset(QgsWkbTypes.PolygonGeometry)
1227 |
1228 | def distance(self, p1, p2):
1229 | dx = p1[0] - p2[0]
1230 | dy = p1[1] - p2[1]
1231 | return math.sqrt(dx * dx + dy * dy)
1232 |
1233 | def unsplit(self):
1234 | """
1235 | unsplit selected two feature.it needs the feature can convert to bezier line.
1236 | """
1237 | layer = self.canvas.currentLayer()
1238 | fields = layer.fields()
1239 | if layer.geometryType() == QgsWkbTypes.LineGeometry:
1240 | selected_features = layer.selectedFeatures()
1241 | if len(selected_features) == 2:
1242 | f0 = selected_features[0]
1243 | f1 = selected_features[1]
1244 | geom0 = f0.geometry()
1245 | geom0.convertToSingleType()
1246 | geom1 = f1.geometry()
1247 | geom1.convertToSingleType()
1248 | line0 = geom0.asPolyline()
1249 | line1 = geom1.asPolyline()
1250 |
1251 | # Connect points with the smallest distance from all combinations of endpoints
1252 | dist = [self.distance(li0, li1) for li0, li1 in
1253 | [(line0[-1], line1[0]), (line0[0], line1[-1]), (line0[0], line1[0]), (line0[-1], line1[-1])]]
1254 | type = dist.index(min(dist))
1255 | if type == 0:
1256 | pass
1257 | elif type == 1:
1258 | line0.reverse()
1259 | line1.reverse()
1260 | elif type == 2:
1261 | line0.reverse()
1262 | elif type == 3:
1263 | line1.reverse()
1264 | # if endpoints are same position
1265 | if line0[-1] == line1[0]:
1266 | line = line0 + line1[1:]
1267 | # If the end points are separated, the are interpolated using Bezier line
1268 | else:
1269 | b = BezierGeometry(self.projectCRS)
1270 | b.add_anchor(0, line0[-1], undo=False)
1271 | b.add_anchor(1, line1[0], undo=False)
1272 | interporate_line = b.asPolyline()
1273 | line = line0 + interporate_line[1:] + line1[1:]
1274 |
1275 | if layer.wkbType() == QgsWkbTypes.LineString:
1276 | geom = QgsGeometry.fromPolylineXY(line)
1277 | elif layer.wkbType() == QgsWkbTypes.MultiLineString:
1278 | geom = QgsGeometry.fromMultiPolylineXY([line])
1279 |
1280 | layer.beginEditCommand(self.tr("Bezier unsplit"))
1281 | settings = QgsSettings()
1282 | disable_val = settings.value("/qgis/digitizing/disable_enter_attribute_values_dialog", False)
1283 | if disable_val is None:
1284 | disable_attributes = False
1285 | elif isinstance(disable_val, str):
1286 | disable_attributes = disable_val.lower() == "true"
1287 | else:
1288 | disable_attributes = bool(disable_val)
1289 | if disable_attributes or fields.count() == 0:
1290 | layer.changeGeometry(f0.id(), geom)
1291 | layer.deleteFeature(f1.id())
1292 | layer.endEditCommand()
1293 | else:
1294 | dlg = self.iface.getFeatureForm(layer, f0)
1295 | if dlg.exec_():
1296 | layer.changeGeometry(f0.id(), geom)
1297 | layer.deleteFeature(f1.id())
1298 | layer.endEditCommand()
1299 | else:
1300 | layer.destroyEditCommand()
1301 | self.canvas.refresh()
1302 | else:
1303 | QMessageBox.warning(
1304 | None, self.tr("Warning"), self.tr("Select exactly two feature."))
1305 | else:
1306 | QMessageBox.warning(
1307 | None, self.tr("Warning"), self.tr("Select a line Layer."))
1308 |
1309 | def activate(self):
1310 | self.canvas.setCursor(self.addanchor_cursor)
1311 | self.checkSnapSetting()
1312 | self.checkCRS()
1313 | self.snap_mark.hide()
1314 | self.resetUnsplit()
1315 |
1316 | def deactivate(self):
1317 | # self.canvas.unsetMapTool(self)
1318 | # QgsMapTool.deactivate(self)
1319 | # self.log("deactivate")
1320 | pass
1321 |
1322 | def isZoomTool(self):
1323 | return False
1324 |
1325 | def isTransient(self):
1326 | return False
1327 |
1328 | def isEditTool(self):
1329 | return True
1330 |
1331 | def showSettingsWarning(self):
1332 | pass
1333 |
1334 | def log(self, msg):
1335 | QgsMessageLog.logMessage(msg, 'BezierEditing', Qgis.Info)
1336 |
--------------------------------------------------------------------------------