feat: add atomic undo redo (#2570)
Todo: - [ ] undo/sync a version restore - [x] add test for scenario where two peers create a layer and one click on save (both should be saved) - [x] undo/sync of conditional rules - [x] undo/sync of filter management from table editor
|
@ -14,11 +14,12 @@
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
}
|
}
|
||||||
.leaflet-container .edit-save,
|
.leaflet-container .edit-save,
|
||||||
.leaflet-container .edit-cancel,
|
.leaflet-container .edit-undo,
|
||||||
|
.leaflet-container .edit-redo,
|
||||||
.leaflet-container .edit-disable,
|
.leaflet-container .edit-disable,
|
||||||
.leaflet-container .connected-peers
|
.leaflet-container .connected-peers
|
||||||
{
|
{
|
||||||
display: block;
|
display: inline-block;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
|
@ -39,7 +40,8 @@
|
||||||
color: var(--color-darkGray);
|
color: var(--color-darkGray);
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaflet-container .edit-cancel:hover,
|
.leaflet-container .edit-undo:hover,
|
||||||
|
.leaflet-container .edit-redo:hover,
|
||||||
.leaflet-container .edit-disable:hover {
|
.leaflet-container .edit-disable:hover {
|
||||||
border: 0.5px solid rgba(153, 153, 153, 0.80);
|
border: 0.5px solid rgba(153, 153, 153, 0.80);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -76,19 +78,13 @@
|
||||||
background: rgba(66, 236, 230, 0.10);
|
background: rgba(66, 236, 230, 0.10);
|
||||||
}
|
}
|
||||||
.leaflet-container .edit-save,
|
.leaflet-container .edit-save,
|
||||||
.leaflet-container .edit-cancel,
|
|
||||||
.leaflet-container .edit-disable,
|
|
||||||
.umap-edit-enabled .edit-enable {
|
.umap-edit-enabled .edit-enable {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.umap-edit-enabled .edit-save,
|
.umap-edit-enabled .edit-save,
|
||||||
.umap-edit-enabled .edit-disable,
|
.umap-edit-enabled .edit-disable {
|
||||||
.umap-edit-enabled.umap-is-dirty .edit-cancel {
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.umap-is-dirty .edit-disable {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.umap-caption-bar {
|
.umap-caption-bar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -115,8 +111,6 @@
|
||||||
.umap-right-edit-toolbox {
|
.umap-right-edit-toolbox {
|
||||||
display: flex;
|
display: flex;
|
||||||
column-gap: 10px;
|
column-gap: 10px;
|
||||||
}
|
|
||||||
.umap-right-edit-toolbox {
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,17 +129,20 @@
|
||||||
text-indent: -9999px;
|
text-indent: -9999px;
|
||||||
}
|
}
|
||||||
.umap-main-edit-toolbox .map-name {
|
.umap-main-edit-toolbox .map-name {
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
}
|
}
|
||||||
|
.truncate {
|
||||||
|
display: inline-flex;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.umap-main-edit-toolbox .username {
|
||||||
|
max-width: 100px;
|
||||||
|
}
|
||||||
.umap-main-edit-toolbox .share-status {
|
.umap-main-edit-toolbox .share-status {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
}
|
||||||
.map-name:after {
|
.map-name:after {
|
||||||
content: '\00a0';
|
content: '\00a0';
|
||||||
|
@ -242,3 +239,14 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@media all and (max-width: 980px) {
|
||||||
|
.umap-main-edit-toolbox button span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media all and (max-width: 770px) {
|
||||||
|
.umap-main-edit-toolbox .umap-help-link,
|
||||||
|
.umap-main-edit-toolbox .share-status {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -167,11 +167,15 @@ html[dir="rtl"] .icon {
|
||||||
.icon-profile {
|
.icon-profile {
|
||||||
background-position: 0 calc(var(--tile) * 4);
|
background-position: 0 calc(var(--tile) * 4);
|
||||||
}
|
}
|
||||||
|
.icon-redo {
|
||||||
|
background-position: calc(var(--tile) * 3) calc(var(--tile) * 7);
|
||||||
|
}
|
||||||
.icon-resize {
|
.icon-resize {
|
||||||
background-position: calc(var(--tile) * 3) calc(var(--tile) * 6);
|
background-position: calc(var(--tile) * 3) calc(var(--tile) * 6);
|
||||||
}
|
}
|
||||||
|
.icon-undo,
|
||||||
.icon-restore {
|
.icon-restore {
|
||||||
background-position: calc(var(--tile) * 5) calc(var(--tile) * 3);
|
background-position: calc(var(--tile) * 2) calc(var(--tile) * 7);
|
||||||
}
|
}
|
||||||
.expanded .icon-resize {
|
.expanded .icon-resize {
|
||||||
background-position: calc(var(--tile) * 2) calc(var(--tile) * 6);
|
background-position: calc(var(--tile) * 2) calc(var(--tile) * 6);
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
<clipPath id="clip0_3071_861">
|
<clipPath id="clip0_3071_861">
|
||||||
<rect id="rect3" width="18" height="20" fill="#fff"/>
|
<rect id="rect3" width="18" height="20" fill="#fff"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
|
<clipPath id="clip0_241_10857-3">
|
||||||
|
<rect id="rect586-6" width="18.05" height="19.01" fill="#fff"/>
|
||||||
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<metadata id="metadata7">
|
<metadata id="metadata7">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
|
@ -67,9 +70,12 @@
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<path id="path4873" d="m108.15 816.36v3.8267h3.8544v-3.8267zm0.51755 4.3517-1.2459 2.3132 1.1669 0.61848 1.244-2.3151zm-1.8689 3.4717-0.27666 0.51571h-2.426v2.2441l-4.0916 4.064 1.3626 1.3528 3.862-3.8342h2.7214v-3.6959l0.015-0.028-0.015-8e-3v-0.0953h-0.17879l-0.97303-0.51571z" color="#000000" color-rendering="auto" fill="#f2f2f2" fill-rule="evenodd" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
<path id="path4873" d="m108.15 816.36v3.8267h3.8544v-3.8267zm0.51755 4.3517-1.2459 2.3132 1.1669 0.61848 1.244-2.3151zm-1.8689 3.4717-0.27666 0.51571h-2.426v2.2441l-4.0916 4.064 1.3626 1.3528 3.862-3.8342h2.7214v-3.6959l0.015-0.028-0.015-8e-3v-0.0953h-0.17879l-0.97303-0.51571z" color="#000000" color-rendering="auto" fill="#f2f2f2" fill-rule="evenodd" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||||
<g id="g4244" transform="matrix(.51357 -.54309 .54309 .51357 -518.02 506.23)">
|
<g id="g4244" transform="matrix(.51357 -.54309 .54309 .51357 -590.02 601.73)">
|
||||||
<path id="path4240" transform="matrix(.91922 .97205 -.97205 .91922 152.14 647.93)" d="m220.49 133.52c-0.33017 0.01-0.66239 0.0456-0.99414 0.10742-2.2249 0.41425-4.0267 1.9575-4.832 4.0098l-0.87696-1.8164a0.50005 0.50005 0 0 0-0.83398-0.11328 0.50005 0.50005 0 0 0-0.0664 0.54883l1.459 3.0195a0.50005 0.50005 0 0 0 0.60742 0.25586l2.8438-0.94532a0.50028 0.50028 0 1 0-0.31641-0.94922l-2.002 0.66797c0.61312-1.8902 2.2073-3.3241 4.2012-3.6953 2.2474-0.41845 4.5146 0.59912 5.6953 2.5566 1.1807 1.9575 1.022 4.4377-0.39648 6.2305-1.4185 1.7928-3.7961 2.5154-5.9727 1.8164a0.50005 0.50005 0 1 0-0.30469 0.95117c2.5704 0.82539 5.3874-0.0294 7.0625-2.1465 0.4188-0.52924 0.74532-1.1114 0.97657-1.7207 0.69373-1.828 0.53599-3.9147-0.50977-5.6484-1.22-2.0227-3.4291-3.1977-5.7402-3.1289z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
<path id="path4240" transform="matrix(.91922 .97205 -.97205 .91922 152.14 647.93)" d="m220.49 133.52c-0.33017 0.01-0.66239 0.0456-0.99414 0.10742-2.2249 0.41425-4.0267 1.9575-4.832 4.0098l-0.87696-1.8164a0.50005 0.50005 0 0 0-0.83398-0.11328 0.50005 0.50005 0 0 0-0.0664 0.54883l1.459 3.0195a0.50005 0.50005 0 0 0 0.60742 0.25586l2.8438-0.94532a0.50028 0.50028 0 1 0-0.31641-0.94922l-2.002 0.66797c0.61312-1.8902 2.2073-3.3241 4.2012-3.6953 2.2474-0.41845 4.5146 0.59912 5.6953 2.5566 1.1807 1.9575 1.022 4.4377-0.39648 6.2305-1.4185 1.7928-3.7961 2.5154-5.9727 1.8164a0.50005 0.50005 0 1 0-0.30469 0.95117c2.5704 0.82539 5.3874-0.0294 7.0625-2.1465 0.4188-0.52924 0.74532-1.1114 0.97657-1.7207 0.69373-1.828 0.53599-3.9147-0.50977-5.6484-1.22-2.0227-3.4291-3.1977-5.7402-3.1289z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||||
</g>
|
</g>
|
||||||
|
<g id="g4244-6" transform="matrix(-.51357 -.54309 -.54309 .51357 734.02 601.73)">
|
||||||
|
<path id="path4240-7" transform="matrix(.91922 .97205 -.97205 .91922 152.14 647.93)" d="m220.49 133.52c-0.33017 0.01-0.66239 0.0456-0.99414 0.10742-2.2249 0.41425-4.0267 1.9575-4.832 4.0098l-0.87696-1.8164a0.50005 0.50005 0 0 0-0.83398-0.11328 0.50005 0.50005 0 0 0-0.0664 0.54883l1.459 3.0195a0.50005 0.50005 0 0 0 0.60742 0.25586l2.8438-0.94532a0.50028 0.50028 0 1 0-0.31641-0.94922l-2.002 0.66797c0.61312-1.8902 2.2073-3.3241 4.2012-3.6953 2.2474-0.41845 4.5146 0.59912 5.6953 2.5566 1.1807 1.9575 1.022 4.4377-0.39648 6.2305-1.4185 1.7928-3.7961 2.5154-5.9727 1.8164a0.50005 0.50005 0 1 0-0.30469 0.95117c2.5704 0.82539 5.3874-0.0294 7.0625-2.1465 0.4188-0.52924 0.74532-1.1114 0.97657-1.7207 0.69373-1.828 0.53599-3.9147-0.50977-5.6484-1.22-2.0227-3.4291-3.1977-5.7402-3.1289z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-linecap="round" stroke-linejoin="round" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||||
|
</g>
|
||||||
<g id="delete-vertex" transform="translate(-90,-64)">
|
<g id="delete-vertex" transform="translate(-90,-64)">
|
||||||
<path id="path4349-2-2-9-3-8-3-5" transform="translate(0 852.36)" d="m227.55 53.105-6.0547 3.0273h-3.4414v3.5176l-2.9512 5.9023 1.7891 0.89453 3.1562-6.3145h2.0059v-2.043l6.3906-3.1953z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
<path id="path4349-2-2-9-3-8-3-5" transform="translate(0 852.36)" d="m227.55 53.105-6.0547 3.0273h-3.4414v3.5176l-2.9512 5.9023 1.7891 0.89453 3.1562-6.3145h2.0059v-2.043l6.3906-3.1953z" color="#000000" color-rendering="auto" fill="#f2f2f2" image-rendering="auto" shape-rendering="auto" solid-color="#000000" stroke="#999" stroke-width=".25" style="isolation:auto;mix-blend-mode:normal;text-decoration-color:#000000;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none;white-space:normal"/>
|
||||||
<path id="path4353-1-6-1-3-5-1-9" d="m217 907.36 6 6" fill="none" stroke="#fff" stroke-width="2.482"/>
|
<path id="path4353-1-6-1-3-5-1-9" d="m217 907.36 6 6" fill="none" stroke="#fff" stroke-width="2.482"/>
|
||||||
|
@ -143,6 +149,10 @@
|
||||||
<path id="path580" d="m1.07 4.41h9.42c0.9234-0.0066 1.8391 0.16957 2.6941 0.5184 0.8551 0.34883 1.6327 0.8634 2.288 1.5141s1.1754 1.4246 1.5303 2.2771c0.3549 0.85256 0.5376 1.767 0.5376 2.6904 0.0067 0.9277-0.1712 1.8474-0.5231 2.7058-0.3519 0.8583-0.871 1.6382-1.527 2.2941-0.656 0.656-1.4358 1.1751-2.2941 1.527-0.8584 0.352-1.7781 0.5298-2.7058 0.5231h-9.42"/>
|
<path id="path580" d="m1.07 4.41h9.42c0.9234-0.0066 1.8391 0.16957 2.6941 0.5184 0.8551 0.34883 1.6327 0.8634 2.288 1.5141s1.1754 1.4246 1.5303 2.2771c0.3549 0.85256 0.5376 1.767 0.5376 2.6904 0.0067 0.9277-0.1712 1.8474-0.5231 2.7058-0.3519 0.8583-0.871 1.6382-1.527 2.2941-0.656 0.656-1.4358 1.1751-2.2941 1.527-0.8584 0.352-1.7781 0.5298-2.7058 0.5231h-9.42"/>
|
||||||
<path id="path582" d="m4.75 8.45-4.04-4.05 4.04-4.05"/>
|
<path id="path582" d="m4.75 8.45-4.04-4.05 4.04-4.05"/>
|
||||||
</g>
|
</g>
|
||||||
|
<g id="undo-5" transform="matrix(-.71301 0 0 .66261 90.012 938.13)" clip-path="url(#clip0_241_10857-3)" fill="none" stroke="#f2f2f2" stroke-miterlimit="10" stroke-width="1.4549">
|
||||||
|
<path id="path580-3" d="m1.07 4.41h9.42c0.9234-0.0066 1.8391 0.16957 2.6941 0.5184 0.8551 0.34883 1.6327 0.8634 2.288 1.5141s1.1754 1.4246 1.5303 2.2771c0.3549 0.85256 0.5376 1.767 0.5376 2.6904 0.0067 0.9277-0.1712 1.8474-0.5231 2.7058-0.3519 0.8583-0.871 1.6382-1.527 2.2941-0.656 0.656-1.4358 1.1751-2.2941 1.527-0.8584 0.352-1.7781 0.5298-2.7058 0.5231h-9.42"/>
|
||||||
|
<path id="path582-5" d="m4.75 8.45-4.04-4.05 4.04-4.05"/>
|
||||||
|
</g>
|
||||||
<g id="g1" transform="translate(144 -24)" fill="none" stroke="#999">
|
<g id="g1" transform="translate(144 -24)" fill="none" stroke="#999">
|
||||||
<path id="path438" d="m9 849.94v4.05c0 0.20708 0.1679 0.375 0.375 0.375h5.25c0.20708 0 0.375-0.16792 0.375-0.375v-4.05c0-0.20708-0.16792-0.375-0.375-0.375h-5.25c-0.2071 0-0.375 0.16792-0.375 0.375z"/>
|
<path id="path438" d="m9 849.94v4.05c0 0.20708 0.1679 0.375 0.375 0.375h5.25c0.20708 0 0.375-0.16792 0.375-0.375v-4.05c0-0.20708-0.16792-0.375-0.375-0.375h-5.25c-0.2071 0-0.375 0.16792-0.375 0.375z"/>
|
||||||
<path id="save" d="m15.213 842.36h-8.8376c-0.2071 0-0.375 0.1679-0.375 0.37499v11.25c0 0.20708 0.1679 0.375 0.375 0.375h11.25c0.20708 0 0.375-0.16792 0.375-0.375v-8.6766c0-0.0953-0.0363-0.18697-0.1014-0.25648l-2.4124-2.5733c-0.07095-0.0756-0.16995-0.11853-0.2736-0.11853z"/>
|
<path id="save" d="m15.213 842.36h-8.8376c-0.2071 0-0.375 0.1679-0.375 0.37499v11.25c0 0.20708 0.1679 0.375 0.375 0.375h11.25c0.20708 0 0.375-0.16792 0.375-0.375v-8.6766c0-0.0953-0.0363-0.18697-0.1014-0.25648l-2.4124-2.5733c-0.07095-0.0756-0.16995-0.11853-0.2736-0.11853z"/>
|
||||||
|
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 28 KiB |
|
@ -21,8 +21,11 @@
|
||||||
<clipPath id="clip0_3071_861">
|
<clipPath id="clip0_3071_861">
|
||||||
<rect width="18" height="20" fill="#ffffff" id="rect3" x="0" y="0" />
|
<rect width="18" height="20" fill="#ffffff" id="rect3" x="0" y="0" />
|
||||||
</clipPath>
|
</clipPath>
|
||||||
|
<clipPath id="clip0_241_10857-3">
|
||||||
|
<rect width="18.049999" height="19.01" fill="#ffffff" id="rect586-6" x="0" y="0" />
|
||||||
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="22.311152" inkscape:cx="196.69536" inkscape:cy="36.932203" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
<sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="4.4320814" inkscape:cx="116.08541" inkscape:cy="109.65503" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="true" inkscape:window-width="1920" inkscape:window-height="1011" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" showguides="true" inkscape:guide-bbox="true" inkscape:snap-grids="true" inkscape:snap-to-guides="true" inkscape:showpageshadow="2" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1">
|
||||||
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" spacingy="1" spacingx="1" units="px" />
|
<inkscape:grid type="xygrid" id="grid3004" empspacing="4" visible="true" enabled="true" snapvisiblegridlinesonly="true" originx="0" originy="0" spacingy="1" spacingx="1" units="px" />
|
||||||
<inkscape:grid id="grid1" units="px" originx="0" originy="0" spacingx="24" spacingy="24" empcolor="#203fff" empopacity="0.85490196" color="#3f3fff" opacity="0.1254902" empspacing="1" enabled="true" visible="true" />
|
<inkscape:grid id="grid1" units="px" originx="0" originy="0" spacingx="24" spacingy="24" empcolor="#203fff" empopacity="0.85490196" color="#3f3fff" opacity="0.1254902" empspacing="1" enabled="true" visible="true" />
|
||||||
</sodipodi:namedview>
|
</sodipodi:namedview>
|
||||||
|
@ -78,9 +81,12 @@
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:evenodd;stroke:#999999;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 108.14555,816.36218 v 3.8267 h 3.85445 v -3.8267 z m 0.51755,4.35174 -1.24591,2.31321 1.16687,0.61848 1.24404,-2.31507 z m -1.86888,3.47168 -0.27666,0.51571 h -2.42597 v 2.24408 l -4.09159,4.06399 1.36261,1.3528 3.86198,-3.83417 h 2.72145 v -3.6959 l 0.015,-0.028 -0.015,-0.008 v -0.0953 h -0.17879 l -0.97303,-0.51571 z" id="path4873" inkscape:connector-curvature="0" />
|
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:evenodd;stroke:#999999;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 108.14555,816.36218 v 3.8267 h 3.85445 v -3.8267 z m 0.51755,4.35174 -1.24591,2.31321 1.16687,0.61848 1.24404,-2.31507 z m -1.86888,3.47168 -0.27666,0.51571 h -2.42597 v 2.24408 l -4.09159,4.06399 1.36261,1.3528 3.86198,-3.83417 h 2.72145 v -3.6959 l 0.015,-0.028 -0.015,-0.008 v -0.0953 h -0.17879 l -0.97303,-0.51571 z" id="path4873" inkscape:connector-curvature="0" />
|
||||||
<g id="g4244" transform="matrix(0.51357238,-0.54309229,0.54309229,0.51357238,-518.0199,506.22551)">
|
<g id="g4244" transform="matrix(0.51357238,-0.54309229,0.54309229,0.51357238,-590.0195,601.72586)">
|
||||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 220.49219,133.52344 c -0.33017,0.01 -0.66239,0.0456 -0.99414,0.10742 -2.22487,0.41425 -4.02666,1.95747 -4.83203,4.00976 l -0.87696,-1.8164 a 0.50004998,0.50004998 0 0 0 -0.83398,-0.11328 0.50004998,0.50004998 0 0 0 -0.0664,0.54883 l 1.45899,3.01953 a 0.50004998,0.50004998 0 0 0 0.60742,0.25586 l 2.84375,-0.94532 a 0.50028339,0.50028339 0 1 0 -0.31641,-0.94922 l -2.00195,0.66797 c 0.61312,-1.89015 2.20733,-3.32407 4.20117,-3.69531 2.24744,-0.41845 4.51458,0.59912 5.69531,2.55664 1.18073,1.95754 1.02202,4.43774 -0.39648,6.23047 -1.41851,1.79275 -3.79606,2.51535 -5.97266,1.81641 a 0.50004998,0.50004998 0 1 0 -0.30469,0.95117 c 2.57038,0.82539 5.38736,-0.0294 7.0625,-2.14649 0.4188,-0.52924 0.74532,-1.11137 0.97657,-1.7207 0.69373,-1.828 0.53599,-3.91467 -0.50977,-5.64844 -1.22005,-2.02271 -3.42908,-3.19767 -5.74023,-3.1289 z" transform="matrix(0.91921787,0.9720541,-0.9720541,0.91921787,152.1356,647.93271)" id="path4240" inkscape:connector-curvature="0" />
|
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 220.49219,133.52344 c -0.33017,0.01 -0.66239,0.0456 -0.99414,0.10742 -2.22487,0.41425 -4.02666,1.95747 -4.83203,4.00976 l -0.87696,-1.8164 a 0.50004998,0.50004998 0 0 0 -0.83398,-0.11328 0.50004998,0.50004998 0 0 0 -0.0664,0.54883 l 1.45899,3.01953 a 0.50004998,0.50004998 0 0 0 0.60742,0.25586 l 2.84375,-0.94532 a 0.50028339,0.50028339 0 1 0 -0.31641,-0.94922 l -2.00195,0.66797 c 0.61312,-1.89015 2.20733,-3.32407 4.20117,-3.69531 2.24744,-0.41845 4.51458,0.59912 5.69531,2.55664 1.18073,1.95754 1.02202,4.43774 -0.39648,6.23047 -1.41851,1.79275 -3.79606,2.51535 -5.97266,1.81641 a 0.50004998,0.50004998 0 1 0 -0.30469,0.95117 c 2.57038,0.82539 5.38736,-0.0294 7.0625,-2.14649 0.4188,-0.52924 0.74532,-1.11137 0.97657,-1.7207 0.69373,-1.828 0.53599,-3.91467 -0.50977,-5.64844 -1.22005,-2.02271 -3.42908,-3.19767 -5.74023,-3.1289 z" transform="matrix(0.91921787,0.9720541,-0.9720541,0.91921787,152.1356,647.93271)" id="path4240" inkscape:connector-curvature="0" />
|
||||||
</g>
|
</g>
|
||||||
|
<g id="g4244-6" transform="matrix(-0.51357238,-0.54309229,-0.54309229,0.51357238,734.0195,601.72586)">
|
||||||
|
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 220.49219,133.52344 c -0.33017,0.01 -0.66239,0.0456 -0.99414,0.10742 -2.22487,0.41425 -4.02666,1.95747 -4.83203,4.00976 l -0.87696,-1.8164 a 0.50004998,0.50004998 0 0 0 -0.83398,-0.11328 0.50004998,0.50004998 0 0 0 -0.0664,0.54883 l 1.45899,3.01953 a 0.50004998,0.50004998 0 0 0 0.60742,0.25586 l 2.84375,-0.94532 a 0.50028339,0.50028339 0 1 0 -0.31641,-0.94922 l -2.00195,0.66797 c 0.61312,-1.89015 2.20733,-3.32407 4.20117,-3.69531 2.24744,-0.41845 4.51458,0.59912 5.69531,2.55664 1.18073,1.95754 1.02202,4.43774 -0.39648,6.23047 -1.41851,1.79275 -3.79606,2.51535 -5.97266,1.81641 a 0.50004998,0.50004998 0 1 0 -0.30469,0.95117 c 2.57038,0.82539 5.38736,-0.0294 7.0625,-2.14649 0.4188,-0.52924 0.74532,-1.11137 0.97657,-1.7207 0.69373,-1.828 0.53599,-3.91467 -0.50977,-5.64844 -1.22005,-2.02271 -3.42908,-3.19767 -5.74023,-3.1289 z" transform="matrix(0.91921787,0.9720541,-0.9720541,0.91921787,152.1356,647.93271)" id="path4240-7" inkscape:connector-curvature="0" />
|
||||||
|
</g>
|
||||||
<g id="delete-vertex" transform="translate(-90,-64)">
|
<g id="delete-vertex" transform="translate(-90,-64)">
|
||||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 227.55273,53.105469 -6.05468,3.027343 h -3.44141 v 3.517579 l -2.95117,5.902343 1.78906,0.894532 3.15625,-6.314454 h 2.00586 v -2.042968 l 6.39063,-3.195313 z" transform="translate(0,852.36218)" id="path4349-2-2-9-3-8-3-5" inkscape:connector-curvature="0" />
|
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#f2f2f2;fill-opacity:1;fill-rule:nonzero;stroke:#999999;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="m 227.55273,53.105469 -6.05468,3.027343 h -3.44141 v 3.517579 l -2.95117,5.902343 1.78906,0.894532 3.15625,-6.314454 h 2.00586 v -2.042968 l 6.39063,-3.195313 z" transform="translate(0,852.36218)" id="path4349-2-2-9-3-8-3-5" inkscape:connector-curvature="0" />
|
||||||
<path sodipodi:nodetypes="cc" style="fill:none;stroke:#ffffff;stroke-width:2.482;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 217,907.36218 6,6" id="path4353-1-6-1-3-5-1-9" inkscape:connector-curvature="0" />
|
<path sodipodi:nodetypes="cc" style="fill:none;stroke:#ffffff;stroke-width:2.482;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" d="m 217,907.36218 6,6" id="path4353-1-6-1-3-5-1-9" inkscape:connector-curvature="0" />
|
||||||
|
@ -154,6 +160,10 @@
|
||||||
<path d="m 1.07001,4.41003 h 9.41999 c 0.9234,-0.0066 1.8391,0.16957 2.6941,0.5184 0.8551,0.34883 1.6327,0.8634 2.288,1.51407 0.6553,0.65067 1.1754,1.42458 1.5303,2.27713 0.3549,0.85256 0.5376,1.76697 0.5376,2.69037 0.0067,0.9277 -0.1712,1.8474 -0.5231,2.7058 -0.3519,0.8583 -0.871,1.6382 -1.527,2.2941 -0.656,0.656 -1.4358,1.1751 -2.2941,1.527 -0.8584,0.352 -1.7781,0.5298 -2.7058,0.5231 h -9.41999" stroke="#f2f2f2" stroke-miterlimit="10" id="path580" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
<path d="m 1.07001,4.41003 h 9.41999 c 0.9234,-0.0066 1.8391,0.16957 2.6941,0.5184 0.8551,0.34883 1.6327,0.8634 2.288,1.51407 0.6553,0.65067 1.1754,1.42458 1.5303,2.27713 0.3549,0.85256 0.5376,1.76697 0.5376,2.69037 0.0067,0.9277 -0.1712,1.8474 -0.5231,2.7058 -0.3519,0.8583 -0.871,1.6382 -1.527,2.2941 -0.656,0.656 -1.4358,1.1751 -2.2941,1.527 -0.8584,0.352 -1.7781,0.5298 -2.7058,0.5231 h -9.41999" stroke="#f2f2f2" stroke-miterlimit="10" id="path580" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
<path d="m 4.75002,8.44998 -4.039998,-4.05 4.039998,-4.050004" stroke="#f2f2f2" stroke-miterlimit="10" id="path582" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
<path d="m 4.75002,8.44998 -4.039998,-4.05 4.039998,-4.050004" stroke="#f2f2f2" stroke-miterlimit="10" id="path582" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
</g>
|
</g>
|
||||||
|
<g clip-path="url(#clip0_241_10857-3)" id="undo-5" transform="matrix(-0.71300568,0,0,0.66260978,90.012499,938.13028)" style="fill:none;stroke:#f2f2f2;stroke-width:1.45488;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1">
|
||||||
|
<path d="m 1.07001,4.41003 h 9.41999 c 0.9234,-0.0066 1.8391,0.16957 2.6941,0.5184 0.8551,0.34883 1.6327,0.8634 2.288,1.51407 0.6553,0.65067 1.1754,1.42458 1.5303,2.27713 0.3549,0.85256 0.5376,1.76697 0.5376,2.69037 0.0067,0.9277 -0.1712,1.8474 -0.5231,2.7058 -0.3519,0.8583 -0.871,1.6382 -1.527,2.2941 -0.656,0.656 -1.4358,1.1751 -2.2941,1.527 -0.8584,0.352 -1.7781,0.5298 -2.7058,0.5231 h -9.41999" stroke="#f2f2f2" stroke-miterlimit="10" id="path580-3" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
<path d="m 4.75002,8.44998 -4.039998,-4.05 4.039998,-4.050004" stroke="#f2f2f2" stroke-miterlimit="10" id="path582-5" style="fill:none;stroke:#f2f2f2;stroke-width:1.45486;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1" />
|
||||||
|
</g>
|
||||||
<g id="g1" transform="translate(144,-24.00004)">
|
<g id="g1" transform="translate(144,-24.00004)">
|
||||||
<path d="m 9,849.93721 v 4.04997 c 0,0.20708 0.167895,0.375 0.375,0.375 h 5.25 c 0.207075,0 0.375,-0.16792 0.375,-0.375 v -4.04997 c 0,-0.20708 -0.167925,-0.375 -0.375,-0.375 h -5.25 c -0.207105,0 -0.375,0.16792 -0.375,0.375 z" stroke="#f2f2f2" id="path438" style="fill:none;stroke:#999999;stroke-width:0.999997;stroke-opacity:1" />
|
<path d="m 9,849.93721 v 4.04997 c 0,0.20708 0.167895,0.375 0.375,0.375 h 5.25 c 0.207075,0 0.375,-0.16792 0.375,-0.375 v -4.04997 c 0,-0.20708 -0.167925,-0.375 -0.375,-0.375 h -5.25 c -0.207105,0 -0.375,0.16792 -0.375,0.375 z" stroke="#f2f2f2" id="path438" style="fill:none;stroke:#999999;stroke-width:0.999997;stroke-opacity:1" />
|
||||||
<path d="m 15.21255,842.36218 h -8.83755 c -0.207105,0 -0.375,0.1679 -0.375,0.37499 v 11.24993 c 0,0.20708 0.167895,0.375 0.375,0.375 h 11.25 c 0.207075,0 0.375,-0.16792 0.375,-0.375 v -8.67664 c 0,-0.0953 -0.0363,-0.18697 -0.1014,-0.25648 l -2.41245,-2.57327 c -0.07095,-0.0756 -0.16995,-0.11853 -0.2736,-0.11853 z" stroke="#f2f2f2" id="save" style="fill:none;stroke:#999999;stroke-width:0.999997;stroke-opacity:1" />
|
<path d="m 15.21255,842.36218 h -8.83755 c -0.207105,0 -0.375,0.1679 -0.375,0.37499 v 11.24993 c 0,0.20708 0.167895,0.375 0.375,0.375 h 11.25 c 0.207075,0 0.375,-0.16792 0.375,-0.375 v -8.67664 c 0,-0.0953 -0.0363,-0.18697 -0.1014,-0.25648 l -2.41245,-2.57327 c -0.07095,-0.0756 -0.16995,-0.11853 -0.2736,-0.11853 z" stroke="#f2f2f2" id="save" style="fill:none;stroke:#999999;stroke-width:0.999997;stroke-opacity:1" />
|
||||||
|
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 79 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |
|
@ -91,6 +91,7 @@ class Feature {
|
||||||
}
|
}
|
||||||
|
|
||||||
set geometry(value) {
|
set geometry(value) {
|
||||||
|
this._geometry_bk = Utils.CopyJSON(this._geometry)
|
||||||
this._geometry = value
|
this._geometry = value
|
||||||
this.pushGeometry()
|
this.pushGeometry()
|
||||||
}
|
}
|
||||||
|
@ -104,13 +105,15 @@ class Feature {
|
||||||
}
|
}
|
||||||
|
|
||||||
pullGeometry(sync = true) {
|
pullGeometry(sync = true) {
|
||||||
|
const oldGeometry = Utils.CopyJSON(this._geometry)
|
||||||
this.fromLatLngs(this._getLatLngs())
|
this.fromLatLngs(this._getLatLngs())
|
||||||
if (sync) {
|
if (sync) {
|
||||||
this.sync.update('geometry', this.geometry)
|
this.sync.update('geometry', this.geometry, oldGeometry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fromLatLngs(latlngs) {
|
fromLatLngs(latlngs) {
|
||||||
|
this._geometry_bk = Utils.CopyJSON(this._geometry)
|
||||||
this._geometry = this.convertLatLngs(latlngs)
|
this._geometry = this.convertLatLngs(latlngs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,8 +148,15 @@ class Feature {
|
||||||
onCommit() {
|
onCommit() {
|
||||||
// When the layer is a remote layer, we don't want to sync the creation of the
|
// When the layer is a remote layer, we don't want to sync the creation of the
|
||||||
// points via the websocket, as the other peers will get them themselves.
|
// points via the websocket, as the other peers will get them themselves.
|
||||||
|
const oldGeoJSON = this._just_married ? null : Utils.CopyJSON(this.toGeoJSON())
|
||||||
|
this.pullGeometry(false)
|
||||||
if (this.datalayer?.isRemoteLayer()) return
|
if (this.datalayer?.isRemoteLayer()) return
|
||||||
this.sync.upsert(this.toGeoJSON())
|
if (this._just_married) {
|
||||||
|
this.sync.upsert(this.toGeoJSON(), null)
|
||||||
|
this._just_married = false
|
||||||
|
} else {
|
||||||
|
this.sync.update('geometry', this.geometry, this._geometry_bk)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isReadOnly() {
|
isReadOnly() {
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { Default as DefaultLayer } from '../rendering/layers/base.js'
|
||||||
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
|
import { Categorized, Choropleth, Circles } from '../rendering/layers/classified.js'
|
||||||
import { Cluster } from '../rendering/layers/cluster.js'
|
import { Cluster } from '../rendering/layers/cluster.js'
|
||||||
import { Heat } from '../rendering/layers/heat.js'
|
import { Heat } from '../rendering/layers/heat.js'
|
||||||
import { ServerStored } from '../saving.js'
|
|
||||||
import * as Schema from '../schema.js'
|
import * as Schema from '../schema.js'
|
||||||
import TableEditor from '../tableeditor.js'
|
import TableEditor from '../tableeditor.js'
|
||||||
import * as Utils from '../utils.js'
|
import * as Utils from '../utils.js'
|
||||||
|
@ -36,9 +35,8 @@ const LAYER_MAP = LAYER_TYPES.reduce((acc, klass) => {
|
||||||
return acc
|
return acc
|
||||||
}, {})
|
}, {})
|
||||||
|
|
||||||
export class DataLayer extends ServerStored {
|
export class DataLayer {
|
||||||
constructor(umap, leafletMap, data = {}) {
|
constructor(umap, leafletMap, data = {}) {
|
||||||
super()
|
|
||||||
this._umap = umap
|
this._umap = umap
|
||||||
this.sync = umap.syncEngine.proxy(this)
|
this.sync = umap.syncEngine.proxy(this)
|
||||||
this._index = Array()
|
this._index = Array()
|
||||||
|
@ -49,7 +47,6 @@ export class DataLayer extends ServerStored {
|
||||||
this._leafletMap = leafletMap
|
this._leafletMap = leafletMap
|
||||||
this.parentPane = this._leafletMap.getPane('overlayPane')
|
this.parentPane = this._leafletMap.getPane('overlayPane')
|
||||||
this.pane = this._leafletMap.createPane(`datalayer${stamp(this)}`, this.parentPane)
|
this.pane = this._leafletMap.createPane(`datalayer${stamp(this)}`, this.parentPane)
|
||||||
this.pane.dataset.id = stamp(this)
|
|
||||||
// FIXME: should be on layer
|
// FIXME: should be on layer
|
||||||
this.renderer = L.svg({ pane: this.pane })
|
this.renderer = L.svg({ pane: this.pane })
|
||||||
this.defaultOptions = {
|
this.defaultOptions = {
|
||||||
|
@ -66,6 +63,7 @@ export class DataLayer extends ServerStored {
|
||||||
data.id = data.id || crypto.randomUUID()
|
data.id = data.id || crypto.randomUUID()
|
||||||
|
|
||||||
this.setOptions(data)
|
this.setOptions(data)
|
||||||
|
this.pane.dataset.id = this.id
|
||||||
|
|
||||||
if (!Utils.isObject(this.options.remoteData)) {
|
if (!Utils.isObject(this.options.remoteData)) {
|
||||||
this.options.remoteData = {}
|
this.options.remoteData = {}
|
||||||
|
@ -114,7 +112,6 @@ export class DataLayer extends ServerStored {
|
||||||
|
|
||||||
set isDeleted(status) {
|
set isDeleted(status) {
|
||||||
this._isDeleted = status
|
this._isDeleted = status
|
||||||
if (status) this.isDirty = status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDeleted() {
|
get isDeleted() {
|
||||||
|
@ -269,13 +266,11 @@ export class DataLayer extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.layer.clearLayers()
|
this.sync.startBatch()
|
||||||
this._features = {}
|
for (const feature of Object.values(this._features)) {
|
||||||
this._index = Array()
|
feature.del()
|
||||||
if (this._geojson) {
|
|
||||||
this.backupData()
|
|
||||||
this._geojson = null
|
|
||||||
}
|
}
|
||||||
|
this.sync.commitBatch()
|
||||||
this.dataChanged()
|
this.dataChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,9 +361,8 @@ export class DataLayer extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
connectToMap() {
|
connectToMap() {
|
||||||
const id = stamp(this)
|
if (!this._umap.datalayers[this.id]) {
|
||||||
if (!this._umap.datalayers[id]) {
|
this._umap.datalayers[this.id] = this
|
||||||
this._umap.datalayers[id] = this
|
|
||||||
}
|
}
|
||||||
if (!this._umap.datalayersIndex.includes(this)) {
|
if (!this._umap.datalayersIndex.includes(this)) {
|
||||||
this._umap.datalayersIndex.push(this)
|
this._umap.datalayersIndex.push(this)
|
||||||
|
@ -417,7 +411,10 @@ export class DataLayer extends ServerStored {
|
||||||
|
|
||||||
removeFeature(feature, sync) {
|
removeFeature(feature, sync) {
|
||||||
const id = stamp(feature)
|
const id = stamp(feature)
|
||||||
if (sync !== false) feature.sync.delete()
|
if (sync !== false) {
|
||||||
|
const oldValue = feature.toGeoJSON()
|
||||||
|
feature.sync.delete(oldValue)
|
||||||
|
}
|
||||||
this.hideFeature(feature)
|
this.hideFeature(feature)
|
||||||
delete this._umap.featuresIndex[feature.getSlug()]
|
delete this._umap.featuresIndex[feature.getSlug()]
|
||||||
feature.disconnectFromDataLayer(this)
|
feature.disconnectFromDataLayer(this)
|
||||||
|
@ -460,7 +457,10 @@ export class DataLayer extends ServerStored {
|
||||||
try {
|
try {
|
||||||
// Do not fail if remote data is somehow invalid,
|
// Do not fail if remote data is somehow invalid,
|
||||||
// otherwise the layer becomes uneditable.
|
// otherwise the layer becomes uneditable.
|
||||||
return this.makeFeatures(geojson, sync)
|
this.sync.startBatch()
|
||||||
|
const features = this.makeFeatures(geojson, sync)
|
||||||
|
this.sync.commitBatch()
|
||||||
|
return features
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.debug('Error with DataLayer', this.id)
|
console.debug('Error with DataLayer', this.id)
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
@ -518,7 +518,7 @@ export class DataLayer extends ServerStored {
|
||||||
}
|
}
|
||||||
if (feature && !feature.isEmpty()) {
|
if (feature && !feature.isEmpty()) {
|
||||||
this.addFeature(feature)
|
this.addFeature(feature)
|
||||||
if (sync) feature.onCommit()
|
if (sync) feature.sync.upsert(feature.toGeoJSON(), null)
|
||||||
return feature
|
return feature
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -527,10 +527,6 @@ export class DataLayer extends ServerStored {
|
||||||
return this._umap.formatter
|
return this._umap.formatter
|
||||||
.parse(raw, format)
|
.parse(raw, format)
|
||||||
.then((geojson) => this.addData(geojson))
|
.then((geojson) => this.addData(geojson))
|
||||||
.then((data) => {
|
|
||||||
if (data?.length) this.isDirty = true
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.debug(error)
|
console.debug(error)
|
||||||
Alert.error(translate('Import failed: invalid data'))
|
Alert.error(translate('Import failed: invalid data'))
|
||||||
|
@ -596,17 +592,17 @@ export class DataLayer extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
del(sync = true) {
|
del(sync = true) {
|
||||||
|
const oldValue = Utils.CopyJSON(this.umapGeoJSON())
|
||||||
this.erase()
|
this.erase()
|
||||||
if (sync) {
|
if (sync) {
|
||||||
this.isDeleted = true
|
this.isDeleted = true
|
||||||
this.sync.delete()
|
this.sync.delete(oldValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
empty() {
|
empty() {
|
||||||
if (this.isRemoteLayer()) return
|
if (this.isRemoteLayer()) return
|
||||||
this.clear()
|
this.clear()
|
||||||
this.isDirty = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
|
@ -630,25 +626,6 @@ export class DataLayer extends ServerStored {
|
||||||
this.clear()
|
this.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (!this.createdOnServer) {
|
|
||||||
this.erase()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetOptions()
|
|
||||||
this.parentPane.appendChild(this.pane)
|
|
||||||
if (this._leaflet_events_bk && !this._leaflet_events) {
|
|
||||||
this._leaflet_events = this._leaflet_events_bk
|
|
||||||
}
|
|
||||||
this.clear()
|
|
||||||
this.hide()
|
|
||||||
if (this.isRemoteLayer()) this.fetchRemoteData()
|
|
||||||
else if (this._geojson_bk) this.fromGeoJSON(this._geojson_bk)
|
|
||||||
this.show()
|
|
||||||
this.isDirty = false
|
|
||||||
}
|
|
||||||
|
|
||||||
redraw() {
|
redraw() {
|
||||||
if (!this.isVisible()) return
|
if (!this.isVisible()) return
|
||||||
this.eachFeature((feature) => feature.redraw())
|
this.eachFeature((feature) => feature.redraw())
|
||||||
|
@ -940,11 +917,14 @@ export class DataLayer extends ServerStored {
|
||||||
)
|
)
|
||||||
if (!error) {
|
if (!error) {
|
||||||
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
|
if (geojson._storage) geojson._umap_options = geojson._storage // Retrocompat.
|
||||||
if (geojson._umap_options) this.setOptions(geojson._umap_options)
|
if (geojson._umap_options) {
|
||||||
|
const oldOptions = Utils.CopyJSON(this.options)
|
||||||
|
this.setOptions(geojson._umap_options)
|
||||||
|
this.sync.update('options', this.options, oldOptions)
|
||||||
|
}
|
||||||
this.empty()
|
this.empty()
|
||||||
if (this.isRemoteLayer()) this.fetchRemoteData()
|
if (this.isRemoteLayer()) this.fetchRemoteData()
|
||||||
else this.addData(geojson)
|
else this.addData(geojson)
|
||||||
this.isDirty = true
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1098,7 +1078,11 @@ export class DataLayer extends ServerStored {
|
||||||
|
|
||||||
setReferenceVersion({ response, sync }) {
|
setReferenceVersion({ response, sync }) {
|
||||||
this._referenceVersion = response.headers.get('X-Datalayer-Version')
|
this._referenceVersion = response.headers.get('X-Datalayer-Version')
|
||||||
if (sync) this.sync.update('_referenceVersion', this._referenceVersion)
|
if (sync) {
|
||||||
|
this.sync.update('_referenceVersion', this._referenceVersion, null, {
|
||||||
|
undo: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
|
@ -1127,6 +1111,10 @@ export class DataLayer extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _trySave(url, headers, formData) {
|
async _trySave(url, headers, formData) {
|
||||||
|
if (this._forceSave) {
|
||||||
|
headers = {}
|
||||||
|
this._forceSave = false
|
||||||
|
}
|
||||||
const [data, response, error] = await this._umap.server.post(url, headers, formData)
|
const [data, response, error] = await this._umap.server.post(url, headers, formData)
|
||||||
if (error) {
|
if (error) {
|
||||||
if (response && response.status === 412) {
|
if (response && response.status === 412) {
|
||||||
|
@ -1136,16 +1124,9 @@ export class DataLayer extends ServerStored {
|
||||||
'This situation is tricky, you have to choose carefully which version is pertinent.'
|
'This situation is tricky, you have to choose carefully which version is pertinent.'
|
||||||
),
|
),
|
||||||
async () => {
|
async () => {
|
||||||
// Save again this layer
|
this._forceSave = true
|
||||||
const status = await this._trySave(url, {}, formData)
|
|
||||||
if (status) {
|
|
||||||
this.isDirty = false
|
|
||||||
|
|
||||||
// Call the main save, in case something else needs to be saved
|
|
||||||
// as the conflict stopped the saving flow
|
|
||||||
await this._umap.saveAll()
|
await this._umap.saveAll()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -1179,7 +1160,7 @@ export class DataLayer extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
commitDelete() {
|
commitDelete() {
|
||||||
delete this._umap.datalayers[stamp(this)]
|
delete this._umap.datalayers[this.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
|
|
|
@ -135,7 +135,13 @@ export default class Facets {
|
||||||
for (const [property, { label, type }] of parsed) {
|
for (const [property, { label, type }] of parsed) {
|
||||||
dumped.push([property, label, type].filter(Boolean).join('|'))
|
dumped.push([property, label, type].filter(Boolean).join('|'))
|
||||||
}
|
}
|
||||||
return dumped.join(',')
|
const oldValue = this._umap.properties.facetKey
|
||||||
|
this._umap.properties.facetKey = dumped.join(',')
|
||||||
|
this._umap.sync.update(
|
||||||
|
'properties.facetKey',
|
||||||
|
this._umap.properties.facetKey,
|
||||||
|
oldValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
has(property) {
|
has(property) {
|
||||||
|
@ -146,15 +152,13 @@ export default class Facets {
|
||||||
const defined = this.getDefined()
|
const defined = this.getDefined()
|
||||||
if (!defined.has(property)) {
|
if (!defined.has(property)) {
|
||||||
defined.set(property, { label, type })
|
defined.set(property, { label, type })
|
||||||
this._umap.properties.facetKey = this.dumps(defined)
|
this.dumps(defined)
|
||||||
this._umap.isDirty = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(property) {
|
remove(property) {
|
||||||
const defined = this.getDefined()
|
const defined = this.getDefined()
|
||||||
defined.delete(property)
|
defined.delete(property)
|
||||||
this._umap.properties.facetKey = this.dumps(defined)
|
this.dumps(defined)
|
||||||
this._umap.isDirty = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,21 +70,7 @@ export class Form extends Utils.WithEvents {
|
||||||
}
|
}
|
||||||
|
|
||||||
setter(field, value) {
|
setter(field, value) {
|
||||||
const path = field.split('.')
|
Utils.setObjectValue(this.obj, field, value)
|
||||||
let obj = this.obj
|
|
||||||
let what
|
|
||||||
for (let i = 0, l = path.length; i < l; i++) {
|
|
||||||
what = path[i]
|
|
||||||
if (what === path[l - 1]) {
|
|
||||||
if (typeof value === 'undefined') {
|
|
||||||
delete obj[what]
|
|
||||||
} else {
|
|
||||||
obj[what] = value
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
obj = obj[what]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreField(field) {
|
restoreField(field) {
|
||||||
|
@ -190,13 +176,17 @@ export class MutatingForm extends Form {
|
||||||
}
|
}
|
||||||
|
|
||||||
setter(field, value) {
|
setter(field, value) {
|
||||||
|
const oldValue = this.getter(field)
|
||||||
|
if ('setter' in this.obj) {
|
||||||
|
this.obj.setter(field, value)
|
||||||
|
} else {
|
||||||
super.setter(field, value)
|
super.setter(field, value)
|
||||||
this.obj.isDirty = true
|
}
|
||||||
if ('render' in this.obj) {
|
if ('render' in this.obj) {
|
||||||
this.obj.render([field], this)
|
this.obj.render([field], this)
|
||||||
}
|
}
|
||||||
if ('sync' in this.obj) {
|
if ('sync' in this.obj) {
|
||||||
this.obj.sync.update(field, value)
|
this.obj.sync.update(field, value, oldValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -541,14 +541,14 @@ Fields.DataLayerSwitcher = class extends Fields.Select {
|
||||||
!datalayer.isDataReadOnly() &&
|
!datalayer.isDataReadOnly() &&
|
||||||
datalayer.isBrowsable()
|
datalayer.isBrowsable()
|
||||||
) {
|
) {
|
||||||
options.push([L.stamp(datalayer), datalayer.getName()])
|
options.push([datalayer.id, datalayer.getName()])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML() {
|
toHTML() {
|
||||||
return L.stamp(this.obj.datalayer)
|
return this.obj.datalayer.id
|
||||||
}
|
}
|
||||||
|
|
||||||
toJS() {
|
toJS() {
|
||||||
|
|
|
@ -249,7 +249,7 @@ export default class Importer extends Utils.WithTemplate {
|
||||||
tagName: 'option',
|
tagName: 'option',
|
||||||
parent: layerSelect,
|
parent: layerSelect,
|
||||||
textContent: datalayer.options.name,
|
textContent: datalayer.options.name,
|
||||||
value: L.stamp(datalayer),
|
value: datalayer.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,17 +2,15 @@ import { DomUtil } from '../../vendors/leaflet/leaflet-src.esm.js'
|
||||||
import { uMapAlert as Alert } from '../components/alerts/alert.js'
|
import { uMapAlert as Alert } from '../components/alerts/alert.js'
|
||||||
import { MutatingForm } from './form/builder.js'
|
import { MutatingForm } from './form/builder.js'
|
||||||
import { translate } from './i18n.js'
|
import { translate } from './i18n.js'
|
||||||
import { ServerStored } from './saving.js'
|
|
||||||
import * as Utils from './utils.js'
|
import * as Utils from './utils.js'
|
||||||
|
|
||||||
// Dedicated object so we can deal with a separate dirty status, and thus
|
// Dedicated object so we can deal with a separate dirty status, and thus
|
||||||
// call the endpoint only when needed, saving one call at each save.
|
// call the endpoint only when needed, saving one call at each save.
|
||||||
export class MapPermissions extends ServerStored {
|
export class MapPermissions {
|
||||||
constructor(umap) {
|
constructor(umap) {
|
||||||
super()
|
|
||||||
this.setProperties(umap.properties.permissions)
|
this.setProperties(umap.properties.permissions)
|
||||||
this._umap = umap
|
this._umap = umap
|
||||||
this._isDirty = false
|
this.sync = umap.syncEngine.proxy(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
setProperties(properties) {
|
setProperties(properties) {
|
||||||
|
@ -28,6 +26,13 @@ export class MapPermissions extends ServerStored {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSyncMetadata() {
|
||||||
|
return {
|
||||||
|
subject: 'mappermissions',
|
||||||
|
metadata: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this._umap.render(['properties.permissions'])
|
this._umap.render(['properties.permissions'])
|
||||||
}
|
}
|
||||||
|
@ -188,7 +193,6 @@ export class MapPermissions extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
if (!this.isDirty) return
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
if (!this.isAnonymousMap() && this.properties.editors) {
|
if (!this.isAnonymousMap() && this.properties.editors) {
|
||||||
const editors = this.properties.editors.map((u) => u.id)
|
const editors = this.properties.editors.map((u) => u.id)
|
||||||
|
@ -247,9 +251,8 @@ export class MapPermissions extends ServerStored {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataLayerPermissions extends ServerStored {
|
export class DataLayerPermissions {
|
||||||
constructor(umap, datalayer) {
|
constructor(umap, datalayer) {
|
||||||
super()
|
|
||||||
this._umap = umap
|
this._umap = umap
|
||||||
this.properties = Object.assign(
|
this.properties = Object.assign(
|
||||||
{
|
{
|
||||||
|
@ -259,6 +262,14 @@ export class DataLayerPermissions extends ServerStored {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.datalayer = datalayer
|
this.datalayer = datalayer
|
||||||
|
this.sync = umap.syncEngine.proxy(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSyncMetadata() {
|
||||||
|
return {
|
||||||
|
subject: 'datalayerpermissions',
|
||||||
|
metadata: { id: this.datalayer.id },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
edit(container) {
|
edit(container) {
|
||||||
|
@ -289,7 +300,6 @@ export class DataLayerPermissions extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
if (!this.isDirty) return
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('edit_status', this.properties.edit_status)
|
formData.append('edit_status', this.properties.edit_status)
|
||||||
const [data, response, error] = await this._umap.server.post(
|
const [data, response, error] = await this._umap.server.post(
|
||||||
|
|
|
@ -117,7 +117,7 @@ export const Choropleth = FeatureGroup.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
_getValue: function (feature) {
|
_getValue: function (feature) {
|
||||||
const key = this.datalayer.options.choropleth.property || 'value'
|
const key = this.datalayer.options.choropleth?.property || 'value'
|
||||||
const value = +feature.properties[key]
|
const value = +feature.properties[key]
|
||||||
if (!Number.isNaN(value)) return value
|
if (!Number.isNaN(value)) return value
|
||||||
},
|
},
|
||||||
|
@ -130,12 +130,12 @@ export const Choropleth = FeatureGroup.extend({
|
||||||
this.options.colors = []
|
this.options.colors = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const mode = this.datalayer.options.choropleth.mode
|
const mode = this.datalayer.options.choropleth?.mode
|
||||||
let classes = +this.datalayer.options.choropleth.classes || 5
|
let classes = +this.datalayer.options.choropleth?.classes || 5
|
||||||
let breaks
|
let breaks
|
||||||
classes = Math.min(classes, values.length)
|
classes = Math.min(classes, values.length)
|
||||||
if (mode === 'manual') {
|
if (mode === 'manual') {
|
||||||
const manualBreaks = this.datalayer.options.choropleth.breaks
|
const manualBreaks = this.datalayer.options.choropleth?.breaks
|
||||||
if (manualBreaks) {
|
if (manualBreaks) {
|
||||||
breaks = manualBreaks
|
breaks = manualBreaks
|
||||||
.split(',')
|
.split(',')
|
||||||
|
|
|
@ -97,7 +97,6 @@ const FeatureMixin = {
|
||||||
},
|
},
|
||||||
|
|
||||||
onCommit: function () {
|
onCommit: function () {
|
||||||
this.feature.pullGeometry(false)
|
|
||||||
this.feature.onCommit()
|
this.feature.onCommit()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -112,7 +111,7 @@ const PointMixin = {
|
||||||
this.on('dragend', (event) => {
|
this.on('dragend', (event) => {
|
||||||
this.isDirty = true
|
this.isDirty = true
|
||||||
this.feature.edit(event)
|
this.feature.edit(event)
|
||||||
this.feature.pullGeometry(false)
|
// this.feature.pullGeometry(false)
|
||||||
})
|
})
|
||||||
if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging)
|
if (!this.feature.isReadOnly()) this.on('mouseover', this._enableDragging)
|
||||||
this.on('mouseout', this._onMouseOut)
|
this.on('mouseout', this._onMouseOut)
|
||||||
|
@ -303,13 +302,13 @@ const PathMixin = {
|
||||||
this._container = null
|
this._container = null
|
||||||
FeatureMixin.onAdd.call(this, map)
|
FeatureMixin.onAdd.call(this, map)
|
||||||
this.setStyle()
|
this.setStyle()
|
||||||
if (this.editing?.enabled()) this.editing.addHooks()
|
if (this.editor?.enabled()) this.editor.addHooks()
|
||||||
this.resetTooltip()
|
this.resetTooltip()
|
||||||
this._path.dataset.feature = this.feature.id
|
this._path.dataset.feature = this.feature.id
|
||||||
},
|
},
|
||||||
|
|
||||||
onRemove: function (map) {
|
onRemove: function (map) {
|
||||||
if (this.editing?.enabled()) this.editing.removeHooks()
|
if (this.editor?.enabled()) this.editor.removeHooks()
|
||||||
FeatureMixin.onRemove.call(this, map)
|
FeatureMixin.onRemove.call(this, map)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -362,6 +361,13 @@ const PathMixin = {
|
||||||
isOnScreen: function (bounds) {
|
isOnScreen: function (bounds) {
|
||||||
return bounds.overlaps(this.getBounds())
|
return bounds.overlaps(this.getBounds())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_setLatLngs: function (latlngs) {
|
||||||
|
this.parentClass.prototype._setLatLngs.call(this, latlngs)
|
||||||
|
if (this.editor?.enabled()) {
|
||||||
|
this.editor.reset()
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LeafletPolyline = Polyline.extend({
|
export const LeafletPolyline = Polyline.extend({
|
||||||
|
|
|
@ -17,20 +17,10 @@ class Rule {
|
||||||
this.parse()
|
this.parse()
|
||||||
}
|
}
|
||||||
|
|
||||||
get isDirty() {
|
|
||||||
return this._isDirty
|
|
||||||
}
|
|
||||||
|
|
||||||
set isDirty(status) {
|
|
||||||
this._isDirty = status
|
|
||||||
if (status) this._umap.isDirty = status
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(umap, condition = '', options = {}) {
|
constructor(umap, condition = '', options = {}) {
|
||||||
// TODO make this public properties when browser coverage is ok
|
// TODO make this public properties when browser coverage is ok
|
||||||
// cf https://caniuse.com/?search=public%20class%20field
|
// cf https://caniuse.com/?search=public%20class%20field
|
||||||
this._condition = null
|
this._condition = null
|
||||||
this._isDirty = false
|
|
||||||
this.OPERATORS = [
|
this.OPERATORS = [
|
||||||
['>', this.gt],
|
['>', this.gt],
|
||||||
['<', this.lt],
|
['<', this.lt],
|
||||||
|
@ -190,17 +180,25 @@ class Rule {
|
||||||
|
|
||||||
_delete() {
|
_delete() {
|
||||||
this._umap.rules.rules = this._umap.rules.rules.filter((rule) => rule !== this)
|
this._umap.rules.rules = this._umap.rules.rules.filter((rule) => rule !== this)
|
||||||
|
this._umap.rules.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
setter(key, value) {
|
||||||
|
const oldRules = Utils.CopyJSON(this._umap.properties.rules || {})
|
||||||
|
Utils.setObjectValue(this, key, value)
|
||||||
|
this._umap.rules.commit()
|
||||||
|
this._umap.sync.update('properties.rules', this._umap.properties.rules, oldRules)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Rules {
|
export default class Rules {
|
||||||
constructor(umap) {
|
constructor(umap) {
|
||||||
this._umap = umap
|
this._umap = umap
|
||||||
this.rules = []
|
|
||||||
this.load()
|
this.load()
|
||||||
}
|
}
|
||||||
|
|
||||||
load() {
|
load() {
|
||||||
|
this.rules = []
|
||||||
if (!this._umap.properties.rules?.length) return
|
if (!this._umap.properties.rules?.length) return
|
||||||
for (const { condition, options } of this._umap.properties.rules) {
|
for (const { condition, options } of this._umap.properties.rules) {
|
||||||
if (!condition) continue
|
if (!condition) continue
|
||||||
|
@ -222,8 +220,8 @@ export default class Rules {
|
||||||
else if (finalIndex > initialIndex) newIdx = referenceIdx
|
else if (finalIndex > initialIndex) newIdx = referenceIdx
|
||||||
else newIdx = referenceIdx + 1
|
else newIdx = referenceIdx + 1
|
||||||
this.rules.splice(newIdx, 0, moved)
|
this.rules.splice(newIdx, 0, moved)
|
||||||
moved.isDirty = true
|
|
||||||
this._umap.render(['rules'])
|
this._umap.render(['rules'])
|
||||||
|
this.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
edit(container) {
|
edit(container) {
|
||||||
|
@ -242,7 +240,6 @@ export default class Rules {
|
||||||
|
|
||||||
addRule() {
|
addRule() {
|
||||||
const rule = new Rule(this._umap)
|
const rule = new Rule(this._umap)
|
||||||
rule.isDirty = true
|
|
||||||
this.rules.push(rule)
|
this.rules.push(rule)
|
||||||
rule.edit(map)
|
rule.edit(map)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
const _queue = new Set()
|
|
||||||
|
|
||||||
export let isDirty = false
|
|
||||||
|
|
||||||
export async function save() {
|
|
||||||
for (const obj of _queue) {
|
|
||||||
const ok = await obj.save()
|
|
||||||
if (!ok) break
|
|
||||||
remove(obj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clear() {
|
|
||||||
_queue.clear()
|
|
||||||
onUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function add(obj) {
|
|
||||||
_queue.add(obj)
|
|
||||||
onUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove(obj) {
|
|
||||||
_queue.delete(obj)
|
|
||||||
onUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
function has(obj) {
|
|
||||||
return _queue.has(obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onUpdate() {
|
|
||||||
isDirty = Boolean(_queue.size)
|
|
||||||
document.body.classList.toggle('umap-is-dirty', isDirty)
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ServerStored {
|
|
||||||
set isDirty(status) {
|
|
||||||
if (status) {
|
|
||||||
add(this)
|
|
||||||
} else {
|
|
||||||
remove(this)
|
|
||||||
}
|
|
||||||
this.onDirty(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
get isDirty() {
|
|
||||||
return has(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
onDirty(status) {}
|
|
||||||
}
|
|
|
@ -44,6 +44,10 @@ export const SCHEMA = {
|
||||||
type: Object,
|
type: Object,
|
||||||
impacts: ['data'],
|
impacts: ['data'],
|
||||||
},
|
},
|
||||||
|
center: {
|
||||||
|
type: Object,
|
||||||
|
impacts: [], // default center, doesn't need any update of the map
|
||||||
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
impacts: ['data'],
|
impacts: ['data'],
|
||||||
|
@ -118,6 +122,9 @@ export const SCHEMA = {
|
||||||
default: false,
|
default: false,
|
||||||
label: translate('Animated transitions'),
|
label: translate('Animated transitions'),
|
||||||
},
|
},
|
||||||
|
edit_status: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
editinosmControl: {
|
editinosmControl: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
impacts: ['ui'],
|
impacts: ['ui'],
|
||||||
|
@ -125,6 +132,9 @@ export const SCHEMA = {
|
||||||
label: translate('Display the control to open OpenStreetMap editor'),
|
label: translate('Display the control to open OpenStreetMap editor'),
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
editors: {
|
||||||
|
type: Array,
|
||||||
|
},
|
||||||
embedControl: {
|
embedControl: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
impacts: ['ui'],
|
impacts: ['ui'],
|
||||||
|
@ -362,6 +372,9 @@ export const SCHEMA = {
|
||||||
type: Object,
|
type: Object,
|
||||||
impacts: ['background'],
|
impacts: ['background'],
|
||||||
},
|
},
|
||||||
|
owner: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
permanentCredit: {
|
permanentCredit: {
|
||||||
type: 'Text',
|
type: 'Text',
|
||||||
impacts: ['ui'],
|
impacts: ['ui'],
|
||||||
|
@ -436,6 +449,9 @@ export const SCHEMA = {
|
||||||
label: translate('Display the search control'),
|
label: translate('Display the search control'),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
share_status: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
shortCredit: {
|
shortCredit: {
|
||||||
type: String,
|
type: String,
|
||||||
impacts: ['ui'],
|
impacts: ['ui'],
|
||||||
|
@ -500,6 +516,9 @@ export const SCHEMA = {
|
||||||
helpEntries: ['sync'],
|
helpEntries: ['sync'],
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
team: {
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
tilelayer: {
|
tilelayer: {
|
||||||
type: Object,
|
type: Object,
|
||||||
impacts: ['background'],
|
impacts: ['background'],
|
||||||
|
@ -566,7 +585,6 @@ export const SCHEMA = {
|
||||||
type: Object,
|
type: Object,
|
||||||
impacts: ['data'],
|
impacts: ['data'],
|
||||||
},
|
},
|
||||||
|
|
||||||
_referenceVersion: {
|
_referenceVersion: {
|
||||||
type: Number,
|
type: Number,
|
||||||
impacts: ['data'],
|
impacts: ['data'],
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import * as SaveManager from '../saving.js'
|
|
||||||
import * as Utils from '../utils.js'
|
import * as Utils from '../utils.js'
|
||||||
import { HybridLogicalClock } from './hlc.js'
|
import { HybridLogicalClock } from './hlc.js'
|
||||||
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
|
import { UndoManager } from './undo.js'
|
||||||
|
import {
|
||||||
|
DataLayerUpdater,
|
||||||
|
FeatureUpdater,
|
||||||
|
MapUpdater,
|
||||||
|
MapPermissionsUpdater,
|
||||||
|
DataLayerPermissionsUpdater,
|
||||||
|
} from './updaters.js'
|
||||||
import { WebSocketTransport } from './websocket.js'
|
import { WebSocketTransport } from './websocket.js'
|
||||||
|
|
||||||
// Start reconnecting after 2 seconds, then double the delay each time
|
// Start reconnecting after 2 seconds, then double the delay each time
|
||||||
|
@ -55,6 +61,8 @@ export class SyncEngine {
|
||||||
map: new MapUpdater(umap),
|
map: new MapUpdater(umap),
|
||||||
feature: new FeatureUpdater(umap),
|
feature: new FeatureUpdater(umap),
|
||||||
datalayer: new DataLayerUpdater(umap),
|
datalayer: new DataLayerUpdater(umap),
|
||||||
|
mappermissions: new MapPermissionsUpdater(umap),
|
||||||
|
datalayerpermissions: new DataLayerPermissionsUpdater(umap),
|
||||||
}
|
}
|
||||||
this.transport = undefined
|
this.transport = undefined
|
||||||
this._operations = new Operations()
|
this._operations = new Operations()
|
||||||
|
@ -64,6 +72,7 @@ export class SyncEngine {
|
||||||
this.websocketConnected = false
|
this.websocketConnected = false
|
||||||
this.closeRequested = false
|
this.closeRequested = false
|
||||||
this.peerId = Utils.generateId()
|
this.peerId = Utils.generateId()
|
||||||
|
this._undoManager = new UndoManager(umap, this.updaters, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOpen() {
|
get isOpen() {
|
||||||
|
@ -122,16 +131,107 @@ export class SyncEngine {
|
||||||
await this.authenticate()
|
await this.authenticate()
|
||||||
}, this._reconnectDelay)
|
}, this._reconnectDelay)
|
||||||
}
|
}
|
||||||
upsert(subject, metadata, value) {
|
|
||||||
this._send({ verb: 'upsert', subject, metadata, value })
|
startBatch() {
|
||||||
|
this._batch = []
|
||||||
}
|
}
|
||||||
|
|
||||||
update(subject, metadata, key, value) {
|
commitBatch(subject, metadata) {
|
||||||
this._send({ verb: 'update', subject, metadata, key, value })
|
if (!this._batch.length) {
|
||||||
|
this._batch = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const operations = this._batch.map((stage) => stage.operation)
|
||||||
|
const operation = { verb: 'batch', operations, subject, metadata }
|
||||||
|
this._undoManager.add({ operation, stages: this._batch })
|
||||||
|
this._send(operation)
|
||||||
|
this._batch = null
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(subject, metadata, key) {
|
upsert(subject, metadata, value, oldValue) {
|
||||||
this._send({ verb: 'delete', subject, metadata, key })
|
const operation = {
|
||||||
|
verb: 'upsert',
|
||||||
|
subject,
|
||||||
|
metadata,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
const stage = {
|
||||||
|
operation,
|
||||||
|
newValue: value,
|
||||||
|
oldValue: oldValue,
|
||||||
|
}
|
||||||
|
if (this._batch) {
|
||||||
|
this._batch.push(stage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._undoManager.add(stage)
|
||||||
|
this._send(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(subject, metadata, key, value, oldValue, { undo } = { undo: true }) {
|
||||||
|
const operation = {
|
||||||
|
verb: 'update',
|
||||||
|
subject,
|
||||||
|
metadata,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
const stage = {
|
||||||
|
operation,
|
||||||
|
oldValue: oldValue,
|
||||||
|
newValue: value,
|
||||||
|
}
|
||||||
|
if (this._batch) {
|
||||||
|
this._batch.push(stage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (undo) this._undoManager.add(stage)
|
||||||
|
this._send(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(subject, metadata, oldValue) {
|
||||||
|
const operation = {
|
||||||
|
verb: 'delete',
|
||||||
|
subject,
|
||||||
|
metadata,
|
||||||
|
}
|
||||||
|
const stage = {
|
||||||
|
operation,
|
||||||
|
oldValue: oldValue,
|
||||||
|
}
|
||||||
|
if (this._batch) {
|
||||||
|
this._batch.push(stage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this._undoManager.add(stage)
|
||||||
|
this._send(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const needSave = new Map()
|
||||||
|
if (!this._umap.id) {
|
||||||
|
// There is no operation for first map save
|
||||||
|
needSave.set(this._umap, [])
|
||||||
|
}
|
||||||
|
for (const operation of this._operations.sorted()) {
|
||||||
|
if (operation.dirty) {
|
||||||
|
const updater = this._getUpdater(operation.subject)
|
||||||
|
const obj = updater.getStoredObject(operation.metadata)
|
||||||
|
if (!needSave.has(obj)) {
|
||||||
|
needSave.set(obj, [])
|
||||||
|
}
|
||||||
|
needSave.get(obj).push(operation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const [obj, operations] of needSave.entries()) {
|
||||||
|
const ok = await obj.save()
|
||||||
|
if (!ok) break
|
||||||
|
for (const operation of operations) {
|
||||||
|
operation.dirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.saved()
|
||||||
|
this._undoManager.toggleState()
|
||||||
}
|
}
|
||||||
|
|
||||||
saved() {
|
saved() {
|
||||||
|
@ -144,8 +244,8 @@ export class SyncEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_send(inputMessage) {
|
_send(operation) {
|
||||||
const message = this._operations.addLocal(inputMessage)
|
const message = this._operations.addLocal(operation)
|
||||||
|
|
||||||
if (this.offline) return
|
if (this.offline) return
|
||||||
if (this.transport) {
|
if (this.transport) {
|
||||||
|
@ -153,7 +253,11 @@ export class SyncEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_getUpdater(subject, metadata) {
|
_getUpdater(subject, metadata, sync) {
|
||||||
|
// For now, prevent permissions to be synced, for security reasons
|
||||||
|
if (sync && (subject === 'mappermissions' || subject === 'datalayerpermissions')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (Object.keys(this.updaters).includes(subject)) {
|
if (Object.keys(this.updaters).includes(subject)) {
|
||||||
return this.updaters[subject]
|
return this.updaters[subject]
|
||||||
}
|
}
|
||||||
|
@ -161,7 +265,15 @@ export class SyncEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
_applyOperation(operation) {
|
_applyOperation(operation) {
|
||||||
|
if (operation.verb === 'batch') {
|
||||||
|
operation.operations.map((op) => this._applyOperation(op))
|
||||||
|
return
|
||||||
|
}
|
||||||
const updater = this._getUpdater(operation.subject, operation.metadata)
|
const updater = this._getUpdater(operation.subject, operation.metadata)
|
||||||
|
if (!updater) {
|
||||||
|
debug('No updater for', operation)
|
||||||
|
return
|
||||||
|
}
|
||||||
updater.applyMessage(operation)
|
updater.applyMessage(operation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -304,9 +416,8 @@ export class SyncEngine {
|
||||||
|
|
||||||
onSavedMessage({ sender, lastKnownHLC }) {
|
onSavedMessage({ sender, lastKnownHLC }) {
|
||||||
debug(`received saved message from peer ${sender}`, lastKnownHLC)
|
debug(`received saved message from peer ${sender}`, lastKnownHLC)
|
||||||
if (lastKnownHLC === this._operations.getLastKnownHLC() && SaveManager.isDirty) {
|
this._operations.saved(lastKnownHLC)
|
||||||
SaveManager.clear()
|
this._undoManager.toggleState()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -356,7 +467,7 @@ export class SyncEngine {
|
||||||
const handler = {
|
const handler = {
|
||||||
get(target, prop) {
|
get(target, prop) {
|
||||||
// Only proxy these methods
|
// Only proxy these methods
|
||||||
if (['upsert', 'update', 'delete'].includes(prop)) {
|
if (['upsert', 'update', 'delete', 'commitBatch'].includes(prop)) {
|
||||||
const { subject, metadata } = object.getSyncMetadata()
|
const { subject, metadata } = object.getSyncMetadata()
|
||||||
// Reflect.get is calling the original method.
|
// Reflect.get is calling the original method.
|
||||||
// .bind is adding the parameters automatically
|
// .bind is adding the parameters automatically
|
||||||
|
@ -378,16 +489,22 @@ export class Operations {
|
||||||
this._operations = new Array()
|
this._operations = new Array()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saved(hlc) {
|
||||||
|
for (const operation of this.getOperationsBefore(hlc)) {
|
||||||
|
operation.dirty = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tick the clock and store the passed message in the operations list.
|
* Tick the clock and store the passed message in the operations list.
|
||||||
*
|
*
|
||||||
* @param {*} inputMessage
|
* @param {*} inputMessage
|
||||||
* @returns {*} clock-aware message
|
* @returns {*} clock-aware message
|
||||||
*/
|
*/
|
||||||
addLocal(inputMessage) {
|
addLocal(operation) {
|
||||||
const message = { ...inputMessage, hlc: this._hlc.tick() }
|
operation.hlc = this._hlc.tick()
|
||||||
this._operations.push(message)
|
this._operations.push(operation)
|
||||||
return message
|
return operation
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -445,6 +562,11 @@ export class Operations {
|
||||||
return this._operations.filter((op) => op.hlc > hlc)
|
return this._operations.filter((op) => op.hlc > hlc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOperationsBefore(hlc) {
|
||||||
|
if (!hlc) return this._operations
|
||||||
|
return this._operations.filter((op) => op.hlc <= hlc)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the last known HLC value.
|
* Returns the last known HLC value.
|
||||||
*/
|
*/
|
||||||
|
|
101
umap/static/umap/js/modules/sync/undo.js
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import * as Utils from '../utils.js'
|
||||||
|
import { DataLayerUpdater, FeatureUpdater, MapUpdater } from './updaters.js'
|
||||||
|
|
||||||
|
export class UndoManager {
|
||||||
|
constructor(umap, updaters, syncEngine) {
|
||||||
|
this._umap = umap
|
||||||
|
this._syncEngine = syncEngine
|
||||||
|
this.updaters = updaters
|
||||||
|
this._undoStack = []
|
||||||
|
this._redoStack = []
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleState() {
|
||||||
|
// document is undefined during unittests
|
||||||
|
if (typeof document === 'undefined') return
|
||||||
|
const undoButton = document.querySelector('.edit-undo')
|
||||||
|
const redoButton = document.querySelector('.edit-redo')
|
||||||
|
if (undoButton) undoButton.disabled = !this._undoStack.length
|
||||||
|
if (redoButton) redoButton.disabled = !this._redoStack.length
|
||||||
|
const dirty = this.isDirty()
|
||||||
|
document.body.classList.toggle('umap-is-dirty', dirty)
|
||||||
|
for (const button of document.querySelectorAll('.disabled-on-dirty')) {
|
||||||
|
button.disabled = dirty
|
||||||
|
}
|
||||||
|
for (const button of document.querySelectorAll('.enabled-on-dirty')) {
|
||||||
|
button.disabled = !dirty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isDirty() {
|
||||||
|
if (!this._umap.id) return true
|
||||||
|
for (const stage of this._undoStack) {
|
||||||
|
if (stage.operation.dirty) return true
|
||||||
|
}
|
||||||
|
for (const stage of this._redoStack) {
|
||||||
|
if (stage.operation.dirty) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
add(stage) {
|
||||||
|
stage.operation.dirty = true
|
||||||
|
this._redoStack = []
|
||||||
|
this._undoStack.push(stage)
|
||||||
|
this.toggleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
copyOperation(stage, redo) {
|
||||||
|
const operation = Utils.CopyJSON(stage.operation)
|
||||||
|
const value = redo ? stage.newValue : stage.oldValue
|
||||||
|
operation.value = value
|
||||||
|
if (['delete', 'upsert'].includes(operation.verb)) {
|
||||||
|
operation.verb = value === null || value === undefined ? 'delete' : 'upsert'
|
||||||
|
}
|
||||||
|
return operation
|
||||||
|
}
|
||||||
|
|
||||||
|
undo(redo = false) {
|
||||||
|
const fromStack = redo ? this._redoStack : this._undoStack
|
||||||
|
const toStack = redo ? this._undoStack : this._redoStack
|
||||||
|
const stage = fromStack.pop()
|
||||||
|
if (!stage) return
|
||||||
|
stage.operation.dirty = !stage.operation.dirty
|
||||||
|
if (stage.operation.verb === 'batch') {
|
||||||
|
for (const st of stage.stages) {
|
||||||
|
this.applyOperation(this.copyOperation(st, redo))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.applyOperation(this.copyOperation(stage, redo))
|
||||||
|
}
|
||||||
|
toStack.push(stage)
|
||||||
|
this.toggleState()
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
this.undo(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
applyOperation(operation) {
|
||||||
|
const updater = this._getUpdater(operation.subject, operation.metadata)
|
||||||
|
switch (operation.verb) {
|
||||||
|
case 'update':
|
||||||
|
updater.update(operation)
|
||||||
|
break
|
||||||
|
case 'delete':
|
||||||
|
updater.delete(operation)
|
||||||
|
break
|
||||||
|
case 'upsert':
|
||||||
|
updater.upsert(operation)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
this._syncEngine._send(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
_getUpdater(subject, metadata) {
|
||||||
|
if (Object.keys(this.updaters).includes(subject)) {
|
||||||
|
return this.updaters[subject]
|
||||||
|
}
|
||||||
|
throw new Error(`Unknown updater ${subject}, ${metadata}`)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { fieldInSchema } from '../utils.js'
|
import * as Utils from '../utils.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updaters are classes able to convert messages
|
* Updaters are classes able to convert messages
|
||||||
|
@ -10,27 +10,6 @@ class BaseUpdater {
|
||||||
this._umap = umap
|
this._umap = umap
|
||||||
}
|
}
|
||||||
|
|
||||||
updateObjectValue(obj, key, value) {
|
|
||||||
const parts = key.split('.')
|
|
||||||
const lastKey = parts.pop()
|
|
||||||
|
|
||||||
// Reduce the current list of attributes,
|
|
||||||
// to find the object to set the property onto
|
|
||||||
const objectToSet = parts.reduce((currentObj, part) => {
|
|
||||||
if (currentObj !== undefined && part in currentObj) return currentObj[part]
|
|
||||||
}, obj)
|
|
||||||
|
|
||||||
// In case the given path doesn't exist, stop here
|
|
||||||
if (objectToSet === undefined) return
|
|
||||||
|
|
||||||
// Set the value (or delete it)
|
|
||||||
if (typeof value === 'undefined') {
|
|
||||||
delete objectToSet[lastKey]
|
|
||||||
} else {
|
|
||||||
objectToSet[lastKey] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getDataLayerFromID(layerId) {
|
getDataLayerFromID(layerId) {
|
||||||
return this._umap.getDataLayerByUmapId(layerId)
|
return this._umap.getDataLayerByUmapId(layerId)
|
||||||
}
|
}
|
||||||
|
@ -43,13 +22,17 @@ class BaseUpdater {
|
||||||
|
|
||||||
export class MapUpdater extends BaseUpdater {
|
export class MapUpdater extends BaseUpdater {
|
||||||
update({ key, value }) {
|
update({ key, value }) {
|
||||||
if (fieldInSchema(key)) {
|
if (Utils.fieldInSchema(key)) {
|
||||||
this.updateObjectValue(this._umap, key, value)
|
Utils.setObjectValue(this._umap, key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
this._umap.onPropertiesUpdated([key])
|
this._umap.onPropertiesUpdated([key])
|
||||||
this._umap.render([key])
|
this._umap.render([key])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStoredObject() {
|
||||||
|
return this._umap
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataLayerUpdater extends BaseUpdater {
|
export class DataLayerUpdater extends BaseUpdater {
|
||||||
|
@ -58,14 +41,21 @@ export class DataLayerUpdater extends BaseUpdater {
|
||||||
try {
|
try {
|
||||||
this.getDataLayerFromID(value.id)
|
this.getDataLayerFromID(value.id)
|
||||||
} catch {
|
} catch {
|
||||||
this._umap.createDataLayer(value, false)
|
const datalayer = this._umap.createDataLayer(value._umap_options || value, false)
|
||||||
|
if (value.features) {
|
||||||
|
// FIXME: this will create new stages in the undoStack, thus this will empty
|
||||||
|
// the redoStack
|
||||||
|
datalayer.addData(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update({ key, metadata, value }) {
|
update({ key, metadata, value }) {
|
||||||
const datalayer = this.getDataLayerFromID(metadata.id)
|
const datalayer = this.getDataLayerFromID(metadata.id)
|
||||||
if (fieldInSchema(key)) {
|
if (key === 'options') {
|
||||||
this.updateObjectValue(datalayer, key, value)
|
datalayer.setOptions(value)
|
||||||
|
} else if (Utils.fieldInSchema(key)) {
|
||||||
|
Utils.setObjectValue(datalayer, key, value)
|
||||||
} else {
|
} else {
|
||||||
console.debug(
|
console.debug(
|
||||||
'Not applying update for datalayer because key is not in the schema',
|
'Not applying update for datalayer because key is not in the schema',
|
||||||
|
@ -82,6 +72,10 @@ export class DataLayerUpdater extends BaseUpdater {
|
||||||
datalayer.commitDelete()
|
datalayer.commitDelete()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStoredObject(metadata) {
|
||||||
|
return this.getDataLayerFromID(metadata.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FeatureUpdater extends BaseUpdater {
|
export class FeatureUpdater extends BaseUpdater {
|
||||||
|
@ -114,7 +108,7 @@ export class FeatureUpdater extends BaseUpdater {
|
||||||
const feature = this.getFeatureFromMetadata(metadata)
|
const feature = this.getFeatureFromMetadata(metadata)
|
||||||
feature.geometry = value
|
feature.geometry = value
|
||||||
} else {
|
} else {
|
||||||
this.updateObjectValue(feature, key, value)
|
Utils.setObjectValue(feature, key, value)
|
||||||
feature.datalayer.indexProperties(feature)
|
feature.datalayer.indexProperties(feature)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,4 +121,32 @@ export class FeatureUpdater extends BaseUpdater {
|
||||||
const feature = this.getFeatureFromMetadata(metadata)
|
const feature = this.getFeatureFromMetadata(metadata)
|
||||||
if (feature) feature.del(false)
|
if (feature) feature.del(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStoredObject(metadata) {
|
||||||
|
return this.getDataLayerFromID(metadata.layerId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapPermissionsUpdater extends BaseUpdater {
|
||||||
|
update({ key, value }) {
|
||||||
|
if (Utils.fieldInSchema(key)) {
|
||||||
|
Utils.setObjectValue(this._umap.permissions, key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStoredObject(metadata) {
|
||||||
|
return this._umap.permissions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataLayerPermissionsUpdater extends BaseUpdater {
|
||||||
|
update({ key, value, metadata }) {
|
||||||
|
if (Utils.fieldInSchema(key)) {
|
||||||
|
Utils.setObjectValue(this.getDataLayerFromID(metadata.id), key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getStoredObject(metadata) {
|
||||||
|
return this.getDataLayerFromID(metadata.id).permissions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,16 @@ const TOP_BAR_TEMPLATE = `
|
||||||
<div class="umap-main-edit-toolbox with-transition dark">
|
<div class="umap-main-edit-toolbox with-transition dark">
|
||||||
<div class="umap-left-edit-toolbox" data-ref="left">
|
<div class="umap-left-edit-toolbox" data-ref="left">
|
||||||
<div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div>
|
<div class="logo"><a class="" href="/" title="${translate('Go to the homepage')}">uMap</a></div>
|
||||||
<button class="map-name flat" type="button" data-ref="name"></button>
|
<button class="map-name flat truncate" type="button" data-ref="name"></button>
|
||||||
<button class="share-status flat" type="button" data-ref="share"></button>
|
<button class="share-status flat truncate" type="button" data-ref="share"></button>
|
||||||
|
<button class="edit-undo round" type="button" data-ref="undo" disabled>
|
||||||
|
<i class="icon icon-16 icon-undo"></i>
|
||||||
|
<span>${translate('Undo')}</span>
|
||||||
|
</button>
|
||||||
|
<button class="edit-redo round" type="button" data-ref="redo" disabled>
|
||||||
|
<i class="icon icon-16 icon-redo"></i>
|
||||||
|
<span>${translate('Redo')}</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="umap-right-edit-toolbox" data-ref="right">
|
<div class="umap-right-edit-toolbox" data-ref="right">
|
||||||
<button class="connected-peers round" type="button" data-ref="peers">
|
<button class="connected-peers round" type="button" data-ref="peers">
|
||||||
|
@ -19,18 +27,14 @@ const TOP_BAR_TEMPLATE = `
|
||||||
</button>
|
</button>
|
||||||
<button class="umap-user flat" type="button" data-ref="user">
|
<button class="umap-user flat" type="button" data-ref="user">
|
||||||
<i class="icon icon-16 icon-profile"></i>
|
<i class="icon icon-16 icon-profile"></i>
|
||||||
<span class="username" data-ref="username"></span>
|
<span class="username truncate" data-ref="username"></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
|
<button class="umap-help-link flat" type="button" title="${translate('Help')}" data-ref="help">${translate('Help')}</button>
|
||||||
<button class="edit-cancel round" type="button" data-ref="cancel">
|
<button class="edit-disable round disabled-on-dirty" type="button" data-ref="view">
|
||||||
<i class="icon icon-16 icon-restore"></i>
|
|
||||||
<span class="">${translate('Cancel edits')}</span>
|
|
||||||
</button>
|
|
||||||
<button class="edit-disable round" type="button" data-ref="view">
|
|
||||||
<i class="icon icon-16 icon-eye"></i>
|
<i class="icon icon-16 icon-eye"></i>
|
||||||
<span class="">${translate('View')}</span>
|
<span>${translate('View')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="edit-save button round" type="button" data-ref="save">
|
<button class="edit-save button round enabled-on-dirty" type="button" data-ref="save">
|
||||||
<i class="icon icon-16 icon-save"></i>
|
<i class="icon icon-16 icon-save"></i>
|
||||||
<i class="icon icon-16 icon-save-disabled"></i>
|
<i class="icon icon-16 icon-save-disabled"></i>
|
||||||
<span hidden data-ref="saveLabel">${translate('Save')}</span>
|
<span hidden data-ref="saveLabel">${translate('Save')}</span>
|
||||||
|
@ -118,11 +122,12 @@ export class TopBar extends WithTemplate {
|
||||||
})
|
})
|
||||||
|
|
||||||
this.elements.help.addEventListener('click', () => this._umap.help.showGetStarted())
|
this.elements.help.addEventListener('click', () => this._umap.help.showGetStarted())
|
||||||
this.elements.cancel.addEventListener('click', () => this._umap.askForReset())
|
this.elements.redo.addEventListener('click', () => this._umap.redo())
|
||||||
this.elements.cancel.addEventListener('mouseover', () => {
|
this.elements.undo.addEventListener('click', () => this._umap.undo())
|
||||||
|
this.elements.undo.addEventListener('mouseover', () => {
|
||||||
this._umap.tooltip.open({
|
this._umap.tooltip.open({
|
||||||
content: this._umap.help.displayLabel('CANCEL'),
|
content: this._umap.help.displayLabel('CANCEL'),
|
||||||
anchor: this.elements.cancel,
|
anchor: this.elements.undo,
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
delay: 500,
|
delay: 500,
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
|
@ -154,9 +159,10 @@ export class TopBar extends WithTemplate {
|
||||||
redraw() {
|
redraw() {
|
||||||
const syncEnabled = this._umap.getProperty('syncEnabled')
|
const syncEnabled = this._umap.getProperty('syncEnabled')
|
||||||
this.elements.peers.hidden = !syncEnabled
|
this.elements.peers.hidden = !syncEnabled
|
||||||
this.elements.cancel.hidden = syncEnabled
|
this.elements.view.disabled = this._umap.sync._undoManager.isDirty()
|
||||||
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
this.elements.saveLabel.hidden = this._umap.permissions.isDraft()
|
||||||
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
this.elements.saveDraftLabel.hidden = !this._umap.permissions.isDraft()
|
||||||
|
this._umap.sync._undoManager.toggleState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,10 +276,7 @@ export class EditBar extends WithTemplate {
|
||||||
DomEvent.disableClickPropagation(this.element)
|
DomEvent.disableClickPropagation(this.element)
|
||||||
this._onClick('marker', () => this._leafletMap.editTools.startMarker())
|
this._onClick('marker', () => this._leafletMap.editTools.startMarker())
|
||||||
this._onClick('polyline', () => this._leafletMap.editTools.startPolyline())
|
this._onClick('polyline', () => this._leafletMap.editTools.startPolyline())
|
||||||
this._onClick('multiline', () => {
|
this._onClick('multiline', () => this._umap.editedFeature.ui.editor.newShape())
|
||||||
console.log('click click')
|
|
||||||
this._umap.editedFeature.ui.editor.newShape()
|
|
||||||
})
|
|
||||||
this._onClick('polygon', () => this._leafletMap.editTools.startPolygon())
|
this._onClick('polygon', () => this._leafletMap.editTools.startPolygon())
|
||||||
this._onClick('multipolygon', () => this._umap.editedFeature.ui.editor.newShape())
|
this._onClick('multipolygon', () => this._umap.editedFeature.ui.editor.newShape())
|
||||||
this._onClick('caption', () => this._umap.editCaption())
|
this._onClick('caption', () => this._umap.editCaption())
|
||||||
|
|
|
@ -22,8 +22,6 @@ import { MapPermissions } from './permissions.js'
|
||||||
import { LeafletMap } from './rendering/map.js'
|
import { LeafletMap } from './rendering/map.js'
|
||||||
import { Request, ServerRequest } from './request.js'
|
import { Request, ServerRequest } from './request.js'
|
||||||
import Rules from './rules.js'
|
import Rules from './rules.js'
|
||||||
import { ServerStored } from './saving.js'
|
|
||||||
import * as SAVEMANAGER from './saving.js'
|
|
||||||
import { SCHEMA } from './schema.js'
|
import { SCHEMA } from './schema.js'
|
||||||
import Share from './share.js'
|
import Share from './share.js'
|
||||||
import Slideshow from './slideshow.js'
|
import Slideshow from './slideshow.js'
|
||||||
|
@ -36,9 +34,8 @@ import Tooltip from './ui/tooltip.js'
|
||||||
import URLs from './urls.js'
|
import URLs from './urls.js'
|
||||||
import * as Utils from './utils.js'
|
import * as Utils from './utils.js'
|
||||||
|
|
||||||
export default class Umap extends ServerStored {
|
export default class Umap {
|
||||||
constructor(element, geojson) {
|
constructor(element, geojson) {
|
||||||
super()
|
|
||||||
// We need to call async function in the init process,
|
// We need to call async function in the init process,
|
||||||
// the init itself does not need to be awaited, but some calls
|
// the init itself does not need to be awaited, but some calls
|
||||||
// in the process must be blocker
|
// in the process must be blocker
|
||||||
|
@ -96,6 +93,10 @@ export default class Umap extends ServerStored {
|
||||||
this._leafletMap.latLng(center)
|
this._leafletMap.latLng(center)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needed for permissions
|
||||||
|
this.syncEngine = new SyncEngine(this)
|
||||||
|
this.sync = this.syncEngine.proxy(this)
|
||||||
|
|
||||||
// Needed to render controls
|
// Needed to render controls
|
||||||
this.permissions = new MapPermissions(this)
|
this.permissions = new MapPermissions(this)
|
||||||
this.urls = new URLs(this.properties.urls)
|
this.urls = new URLs(this.properties.urls)
|
||||||
|
@ -130,9 +131,6 @@ export default class Umap extends ServerStored {
|
||||||
this.share = new Share(this)
|
this.share = new Share(this)
|
||||||
this.rules = new Rules(this)
|
this.rules = new Rules(this)
|
||||||
|
|
||||||
this.syncEngine = new SyncEngine(this)
|
|
||||||
this.sync = this.syncEngine.proxy(this)
|
|
||||||
|
|
||||||
if (this.hasEditMode()) {
|
if (this.hasEditMode()) {
|
||||||
this.editPanel = new EditPanel(this, this._leafletMap)
|
this.editPanel = new EditPanel(this, this._leafletMap)
|
||||||
this.fullPanel = new FullPanel(this, this._leafletMap)
|
this.fullPanel = new FullPanel(this, this._leafletMap)
|
||||||
|
@ -196,7 +194,6 @@ export default class Umap extends ServerStored {
|
||||||
// Creation mode
|
// Creation mode
|
||||||
if (!this.id) {
|
if (!this.id) {
|
||||||
if (!this.properties.preview) {
|
if (!this.properties.preview) {
|
||||||
this.isDirty = true
|
|
||||||
this.enableEdit()
|
this.enableEdit()
|
||||||
}
|
}
|
||||||
this._defaultExtent = true
|
this._defaultExtent = true
|
||||||
|
@ -212,10 +209,14 @@ export default class Umap extends ServerStored {
|
||||||
this.propagate()
|
this.propagate()
|
||||||
}
|
}
|
||||||
|
|
||||||
window.onbeforeunload = () => (this.editEnabled && SAVEMANAGER.isDirty) || null
|
window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null
|
||||||
this.backup()
|
this.backup()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isDirty() {
|
||||||
|
return this.sync._undoManager.isDirty()
|
||||||
|
}
|
||||||
|
|
||||||
get editedFeature() {
|
get editedFeature() {
|
||||||
return this._editedFeature
|
return this._editedFeature
|
||||||
}
|
}
|
||||||
|
@ -349,7 +350,7 @@ export default class Umap extends ServerStored {
|
||||||
const items = []
|
const items = []
|
||||||
if (this.hasEditMode()) {
|
if (this.hasEditMode()) {
|
||||||
if (this.editEnabled) {
|
if (this.editEnabled) {
|
||||||
if (!SAVEMANAGER.isDirty) {
|
if (!this.isDirty) {
|
||||||
items.push({
|
items.push({
|
||||||
label: this.help.displayLabel('STOP_EDIT'),
|
label: this.help.displayLabel('STOP_EDIT'),
|
||||||
action: () => this.disableEdit(),
|
action: () => this.disableEdit(),
|
||||||
|
@ -543,19 +544,17 @@ export default class Umap extends ServerStored {
|
||||||
let used = true
|
let used = true
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
case 'e':
|
case 'e':
|
||||||
if (!SAVEMANAGER.isDirty) this.disableEdit()
|
if (!this.isDirty) this.disableEdit()
|
||||||
break
|
break
|
||||||
case 's':
|
case 's':
|
||||||
if (SAVEMANAGER.isDirty) this.saveAll()
|
if (this.isDirty) this.saveAll()
|
||||||
break
|
break
|
||||||
case 'z':
|
case 'z':
|
||||||
if (Utils.isWritable(event.target)) {
|
if (Utils.isWritable(event.target)) {
|
||||||
used = false
|
used = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if (SAVEMANAGER.isDirty) {
|
this.sync._undoManager.undo()
|
||||||
this.askForReset()
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
case 'm':
|
case 'm':
|
||||||
this._leafletMap.editTools.startMarker()
|
this._leafletMap.editTools.startMarker()
|
||||||
|
@ -671,10 +670,10 @@ export default class Umap extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveAll() {
|
async saveAll() {
|
||||||
if (!SAVEMANAGER.isDirty) return
|
if (!this.isDirty) return
|
||||||
if (this._defaultExtent) this._setCenterAndZoom()
|
if (this._defaultExtent) this._setCenterAndZoom()
|
||||||
this.backup()
|
this.backup()
|
||||||
await SAVEMANAGER.save()
|
await this.sync.save()
|
||||||
// Do a blind render for now, as we are not sure what could
|
// Do a blind render for now, as we are not sure what could
|
||||||
// have changed, we'll be more subtil when we'll remove the
|
// have changed, we'll be more subtil when we'll remove the
|
||||||
// save action
|
// save action
|
||||||
|
@ -685,7 +684,6 @@ export default class Umap extends ServerStored {
|
||||||
Alert.success(translate('Map has been saved!'))
|
Alert.success(translate('Map has been saved!'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.sync.saved()
|
|
||||||
this.fire('saved')
|
this.fire('saved')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1019,35 +1017,36 @@ export default class Umap extends ServerStored {
|
||||||
'button',
|
'button',
|
||||||
boundsButtons,
|
boundsButtons,
|
||||||
translate('Use current bounds'),
|
translate('Use current bounds'),
|
||||||
function () {
|
() => {
|
||||||
const bounds = this._leafletMap.getBounds()
|
const bounds = this._leafletMap.getBounds()
|
||||||
|
const oldLimitBounds = { ...this.properties.limitBounds }
|
||||||
this.properties.limitBounds.south = LeafletUtil.formatNum(bounds.getSouth())
|
this.properties.limitBounds.south = LeafletUtil.formatNum(bounds.getSouth())
|
||||||
this.properties.limitBounds.west = LeafletUtil.formatNum(bounds.getWest())
|
this.properties.limitBounds.west = LeafletUtil.formatNum(bounds.getWest())
|
||||||
this.properties.limitBounds.north = LeafletUtil.formatNum(bounds.getNorth())
|
this.properties.limitBounds.north = LeafletUtil.formatNum(bounds.getNorth())
|
||||||
this.properties.limitBounds.east = LeafletUtil.formatNum(bounds.getEast())
|
this.properties.limitBounds.east = LeafletUtil.formatNum(bounds.getEast())
|
||||||
boundsBuilder.fetchAll()
|
boundsBuilder.fetchAll()
|
||||||
|
this.sync.update(
|
||||||
this.sync.update(this, 'properties.limitBounds', this.properties.limitBounds)
|
'properties.limitBounds',
|
||||||
this.isDirty = true
|
this.properties.limitBounds,
|
||||||
this._leafletMap.handleLimitBounds()
|
oldLimitBounds
|
||||||
},
|
|
||||||
this
|
|
||||||
)
|
)
|
||||||
DomUtil.createButton(
|
this._leafletMap.handleLimitBounds()
|
||||||
'button',
|
}
|
||||||
boundsButtons,
|
)
|
||||||
translate('Empty'),
|
DomUtil.createButton('button', boundsButtons, translate('Empty'), () => {
|
||||||
function () {
|
const oldLimitBounds = { ...this.properties.limitBounds }
|
||||||
this.properties.limitBounds.south = null
|
this.properties.limitBounds.south = null
|
||||||
this.properties.limitBounds.west = null
|
this.properties.limitBounds.west = null
|
||||||
this.properties.limitBounds.north = null
|
this.properties.limitBounds.north = null
|
||||||
this.properties.limitBounds.east = null
|
this.properties.limitBounds.east = null
|
||||||
boundsBuilder.fetchAll()
|
boundsBuilder.fetchAll()
|
||||||
this.isDirty = true
|
|
||||||
this._leafletMap.handleLimitBounds()
|
this._leafletMap.handleLimitBounds()
|
||||||
},
|
this.sync.update(
|
||||||
this
|
'properties.limitBounds',
|
||||||
|
this.properties.limitBounds,
|
||||||
|
oldLimitBounds
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
_editSlideshow(container) {
|
_editSlideshow(container) {
|
||||||
|
@ -1160,23 +1159,7 @@ export default class Umap extends ServerStored {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (this._leafletMap.editTools) this._leafletMap.editTools.stopDrawing()
|
|
||||||
this.resetProperties()
|
|
||||||
this.datalayersIndex = [].concat(this._datalayersIndex_bk)
|
|
||||||
// Iter over all datalayers, including deleted if any.
|
|
||||||
for (const datalayer of Object.values(this.datalayers)) {
|
|
||||||
if (datalayer.isDeleted) datalayer.connectToMap()
|
|
||||||
if (datalayer.isDirty) datalayer.reset()
|
|
||||||
}
|
|
||||||
this.ensurePanesOrder()
|
|
||||||
this._leafletMap.initTileLayers()
|
|
||||||
this.onDataLayersChanged()
|
|
||||||
this.isDirty = !this.id
|
|
||||||
}
|
|
||||||
|
|
||||||
async save() {
|
async save() {
|
||||||
this.rules.commit()
|
|
||||||
const geojson = {
|
const geojson = {
|
||||||
type: 'Feature',
|
type: 'Feature',
|
||||||
geometry: this.geometry(),
|
geometry: this.geometry(),
|
||||||
|
@ -1301,16 +1284,6 @@ export default class Umap extends ServerStored {
|
||||||
this._leafletMap.fire(name)
|
this._leafletMap.fire(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
askForReset(e) {
|
|
||||||
if (this.getProperty('syncEnabled')) return
|
|
||||||
this.dialog
|
|
||||||
.confirm(translate('Are you sure you want to cancel your changes?'))
|
|
||||||
.then(() => {
|
|
||||||
this.reset()
|
|
||||||
this.disableEdit()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async initSyncEngine() {
|
async initSyncEngine() {
|
||||||
// this.properties.websocketEnabled is set by the server admin
|
// this.properties.websocketEnabled is set by the server admin
|
||||||
if (this.properties.websocketEnabled === false) return
|
if (this.properties.websocketEnabled === false) return
|
||||||
|
@ -1324,7 +1297,6 @@ export default class Umap extends ServerStored {
|
||||||
|
|
||||||
getSyncMetadata() {
|
getSyncMetadata() {
|
||||||
return {
|
return {
|
||||||
engine: this.sync,
|
|
||||||
subject: 'map',
|
subject: 'map',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1348,6 +1320,9 @@ export default class Umap extends ServerStored {
|
||||||
this.bottomBar.redraw()
|
this.bottomBar.redraw()
|
||||||
break
|
break
|
||||||
case 'data':
|
case 'data':
|
||||||
|
if (fields.includes('properties.rules')) {
|
||||||
|
this.rules.load()
|
||||||
|
}
|
||||||
this.eachVisibleDataLayer((datalayer) => {
|
this.eachVisibleDataLayer((datalayer) => {
|
||||||
datalayer.redraw()
|
datalayer.redraw()
|
||||||
})
|
})
|
||||||
|
@ -1522,7 +1497,7 @@ export default class Umap extends ServerStored {
|
||||||
const form = builder.build()
|
const form = builder.build()
|
||||||
row.appendChild(form)
|
row.appendChild(form)
|
||||||
row.classList.toggle('off', !datalayer.isVisible())
|
row.classList.toggle('off', !datalayer.isVisible())
|
||||||
row.dataset.id = stamp(datalayer)
|
row.dataset.id = datalayer.id
|
||||||
})
|
})
|
||||||
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
const onReorder = (src, dst, initialIndex, finalIndex) => {
|
||||||
const movedLayer = this.datalayers[src.dataset.id]
|
const movedLayer = this.datalayers[src.dataset.id]
|
||||||
|
@ -1553,7 +1528,7 @@ export default class Umap extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataLayerByUmapId(id) {
|
getDataLayerByUmapId(id) {
|
||||||
const datalayer = this.findDataLayer((d) => d.id === id)
|
const datalayer = this.datalayers[id]
|
||||||
if (!datalayer) throw new Error(`Can't find datalayer with id ${id}`)
|
if (!datalayer) throw new Error(`Can't find datalayer with id ${id}`)
|
||||||
return datalayer
|
return datalayer
|
||||||
}
|
}
|
||||||
|
@ -1669,7 +1644,6 @@ export default class Umap extends ServerStored {
|
||||||
)
|
)
|
||||||
this.render(fields)
|
this.render(fields)
|
||||||
this._leafletMap._setDefaultCenter()
|
this._leafletMap._setDefaultCenter()
|
||||||
this.isDirty = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
importUmapFile(file) {
|
importUmapFile(file) {
|
||||||
|
@ -1768,13 +1742,26 @@ export default class Umap extends ServerStored {
|
||||||
}
|
}
|
||||||
|
|
||||||
_setCenterAndZoom() {
|
_setCenterAndZoom() {
|
||||||
|
const oldCenter = { ...this.properties.center }
|
||||||
|
const oldZoom = this.properties.zoom
|
||||||
this.properties.center = this._leafletMap.getCenter()
|
this.properties.center = this._leafletMap.getCenter()
|
||||||
this.properties.zoom = this._leafletMap.getZoom()
|
this.properties.zoom = this._leafletMap.getZoom()
|
||||||
this.isDirty = true
|
|
||||||
this._defaultExtent = false
|
this._defaultExtent = false
|
||||||
|
this.sync.startBatch()
|
||||||
|
this.sync.update('properties.center', this.properties.center, oldCenter)
|
||||||
|
this.sync.update('properties.zoom', this.properties.zoom, oldZoom)
|
||||||
|
this.sync.commitBatch()
|
||||||
}
|
}
|
||||||
|
|
||||||
getStaticPathFor(name) {
|
getStaticPathFor(name) {
|
||||||
return SCHEMA.iconUrl.default.replace('marker.svg', name)
|
return SCHEMA.iconUrl.default.replace('marker.svg', name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
this.sync._undoManager.undo()
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
this.sync._undoManager.redo()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -484,6 +484,27 @@ export const debounce = (callback, wait) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setObjectValue(obj, key, value) {
|
||||||
|
const parts = key.split('.')
|
||||||
|
const lastKey = parts.pop()
|
||||||
|
|
||||||
|
// Reduce the current list of attributes,
|
||||||
|
// to find the object to set the property onto
|
||||||
|
const objectToSet = parts.reduce((currentObj, part) => {
|
||||||
|
if (currentObj !== undefined && part in currentObj) return currentObj[part]
|
||||||
|
}, obj)
|
||||||
|
|
||||||
|
// In case the given path doesn't exist, stop here
|
||||||
|
if (objectToSet === undefined) return
|
||||||
|
|
||||||
|
// Set the value (or delete it)
|
||||||
|
if (typeof value === 'undefined') {
|
||||||
|
delete objectToSet[lastKey]
|
||||||
|
} else {
|
||||||
|
objectToSet[lastKey] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const COLORS = [
|
export const COLORS = [
|
||||||
'Black',
|
'Black',
|
||||||
'Navy',
|
'Navy',
|
||||||
|
|
|
@ -297,6 +297,7 @@ U.TileLayerChooser = L.Control.extend({
|
||||||
el,
|
el,
|
||||||
'click',
|
'click',
|
||||||
() => {
|
() => {
|
||||||
|
const oldTileLayer = this.map._umap.properties.tilelayer
|
||||||
this.map.selectTileLayer(tilelayer)
|
this.map.selectTileLayer(tilelayer)
|
||||||
this.map._controls.tilelayers.setLayers()
|
this.map._controls.tilelayers.setLayers()
|
||||||
if (options?.edit) {
|
if (options?.edit) {
|
||||||
|
@ -304,7 +305,8 @@ U.TileLayerChooser = L.Control.extend({
|
||||||
this.map._umap.isDirty = true
|
this.map._umap.isDirty = true
|
||||||
this.map._umap.sync.update(
|
this.map._umap.sync.update(
|
||||||
'properties.tilelayer',
|
'properties.tilelayer',
|
||||||
this.map._umap.properties.tilelayer
|
this.map._umap.properties.tilelayer,
|
||||||
|
oldTileLayer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -606,7 +608,7 @@ U.Editable = L.Editable.extend({
|
||||||
this.on('editable:editing', (event) => {
|
this.on('editable:editing', (event) => {
|
||||||
const feature = event.layer.feature
|
const feature = event.layer.feature
|
||||||
feature.isDirty = true
|
feature.isDirty = true
|
||||||
feature.pullGeometry(false)
|
// feature.pullGeometry(false)
|
||||||
})
|
})
|
||||||
this.on('editable:vertex:ctrlclick', (event) => {
|
this.on('editable:vertex:ctrlclick', (event) => {
|
||||||
const index = event.vertex.getIndex()
|
const index = event.vertex.getIndex()
|
||||||
|
@ -624,18 +626,20 @@ U.Editable = L.Editable.extend({
|
||||||
|
|
||||||
createPolyline: function (latlngs) {
|
createPolyline: function (latlngs) {
|
||||||
const datalayer = this._umap.defaultEditDataLayer()
|
const datalayer = this._umap.defaultEditDataLayer()
|
||||||
const point = new U.LineString(this._umap, datalayer, {
|
const line = new U.LineString(this._umap, datalayer, {
|
||||||
geometry: { type: 'LineString', coordinates: [] },
|
geometry: { type: 'LineString', coordinates: [] },
|
||||||
})
|
})
|
||||||
return point.ui
|
line._just_married = true
|
||||||
|
return line.ui
|
||||||
},
|
},
|
||||||
|
|
||||||
createPolygon: function (latlngs) {
|
createPolygon: function (latlngs) {
|
||||||
const datalayer = this._umap.defaultEditDataLayer()
|
const datalayer = this._umap.defaultEditDataLayer()
|
||||||
const point = new U.Polygon(this._umap, datalayer, {
|
const poly = new U.Polygon(this._umap, datalayer, {
|
||||||
geometry: { type: 'Polygon', coordinates: [] },
|
geometry: { type: 'Polygon', coordinates: [] },
|
||||||
})
|
})
|
||||||
return point.ui
|
poly._just_married = true
|
||||||
|
return poly.ui
|
||||||
},
|
},
|
||||||
|
|
||||||
createMarker: function (latlng) {
|
createMarker: function (latlng) {
|
||||||
|
@ -643,6 +647,7 @@ U.Editable = L.Editable.extend({
|
||||||
const point = new U.Point(this._umap, datalayer, {
|
const point = new U.Point(this._umap, datalayer, {
|
||||||
geometry: { type: 'Point', coordinates: [latlng.lng, latlng.lat] },
|
geometry: { type: 'Point', coordinates: [latlng.lng, latlng.lat] },
|
||||||
})
|
})
|
||||||
|
point._just_married = true
|
||||||
return point.ui
|
return point.ui
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -734,6 +739,7 @@ U.Editable = L.Editable.extend({
|
||||||
// Leaflet.Editable will delete the drawn shape if invalid
|
// Leaflet.Editable will delete the drawn shape if invalid
|
||||||
// (eg. line has only one drawn point)
|
// (eg. line has only one drawn point)
|
||||||
// So let's check if the layer has no more shape
|
// So let's check if the layer has no more shape
|
||||||
|
event.layer.feature.pullGeometry(false)
|
||||||
if (!event.layer.feature.hasGeom()) {
|
if (!event.layer.feature.hasGeom()) {
|
||||||
event.layer.feature.del()
|
event.layer.feature.del()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -986,7 +986,7 @@ a.umap-control-caption,
|
||||||
.umap-main-edit-toolbox .umap-user span,
|
.umap-main-edit-toolbox .umap-user span,
|
||||||
.leaflet-container .leaflet-control-edit-save span,
|
.leaflet-container .leaflet-control-edit-save span,
|
||||||
.leaflet-container .leaflet-control-edit-disable span,
|
.leaflet-container .leaflet-control-edit-disable span,
|
||||||
.leaflet-container .leaflet-control-edit-cancel span {
|
.leaflet-container .edit-cancel span {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.umap-main-edit-toolbox .umap-help-button {
|
.umap-main-edit-toolbox .umap-help-button {
|
||||||
|
|
|
@ -49,63 +49,6 @@ describe('#dispatch', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Updaters', () => {
|
|
||||||
describe('BaseUpdater', () => {
|
|
||||||
let updater
|
|
||||||
let map
|
|
||||||
let obj
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
map = {}
|
|
||||||
updater = new MapUpdater(map)
|
|
||||||
obj = {}
|
|
||||||
})
|
|
||||||
it('should be able to set object properties', () => {
|
|
||||||
let obj = {}
|
|
||||||
updater.updateObjectValue(obj, 'foo', 'foo')
|
|
||||||
expect(obj).deep.equal({ foo: 'foo' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be able to set object properties recursively on existing objects', () => {
|
|
||||||
let obj = { foo: {} }
|
|
||||||
updater.updateObjectValue(obj, 'foo.bar', 'foo')
|
|
||||||
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be able to set object properties recursively on deep objects', () => {
|
|
||||||
let obj = { foo: { bar: { baz: {} } } }
|
|
||||||
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
|
||||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should be able to replace object properties recursively on deep objects', () => {
|
|
||||||
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
|
||||||
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
|
||||||
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not set object properties recursively on non-existing objects', () => {
|
|
||||||
let obj = { foo: {} }
|
|
||||||
updater.updateObjectValue(obj, 'bar.bar', 'value')
|
|
||||||
|
|
||||||
expect(obj).deep.equal({ foo: {} })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should delete keys for undefined values', () => {
|
|
||||||
let obj = { foo: 'foo' }
|
|
||||||
updater.updateObjectValue(obj, 'foo', undefined)
|
|
||||||
|
|
||||||
expect(obj).deep.equal({})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should delete keys for undefined values, recursively', () => {
|
|
||||||
let obj = { foo: { bar: 'bar' } }
|
|
||||||
updater.updateObjectValue(obj, 'foo.bar', undefined)
|
|
||||||
|
|
||||||
expect(obj).deep.equal({ foo: {} })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Operations', () => {
|
describe('Operations', () => {
|
||||||
describe('haveSameContext', () => {
|
describe('haveSameContext', () => {
|
||||||
|
|
|
@ -862,4 +862,51 @@ describe('Utils', () => {
|
||||||
assert.equal(Utils.isObject(''), false)
|
assert.equal(Utils.isObject(''), false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('setObjectValue', () => {
|
||||||
|
it('should be able to set object properties', () => {
|
||||||
|
let obj = {}
|
||||||
|
Utils.setObjectValue(obj, 'foo', 'foo')
|
||||||
|
expect(obj).deep.equal({ foo: 'foo' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to set object properties recursively on existing objects', () => {
|
||||||
|
let obj = { foo: {} }
|
||||||
|
Utils.setObjectValue(obj, 'foo.bar', 'foo')
|
||||||
|
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to set object properties recursively on deep objects', () => {
|
||||||
|
let obj = { foo: { bar: { baz: {} } } }
|
||||||
|
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||||
|
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should be able to replace object properties recursively on deep objects', () => {
|
||||||
|
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
||||||
|
Utils.setObjectValue(obj, 'foo.bar.baz.test', 'value')
|
||||||
|
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not set object properties recursively on non-existing objects', () => {
|
||||||
|
let obj = { foo: {} }
|
||||||
|
Utils.setObjectValue(obj, 'bar.bar', 'value')
|
||||||
|
|
||||||
|
expect(obj).deep.equal({ foo: {} })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete keys for undefined values', () => {
|
||||||
|
let obj = { foo: 'foo' }
|
||||||
|
Utils.setObjectValue(obj, 'foo', undefined)
|
||||||
|
|
||||||
|
expect(obj).deep.equal({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete keys for undefined values, recursively', () => {
|
||||||
|
let obj = { foo: { bar: 'bar' } }
|
||||||
|
Utils.setObjectValue(obj, 'foo.bar', undefined)
|
||||||
|
|
||||||
|
expect(obj).deep.equal({ foo: {} })
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Literal, Optional, Union
|
from typing import List, Literal, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, RootModel
|
from pydantic import BaseModel, Field, RootModel
|
||||||
|
|
||||||
|
@ -14,10 +14,11 @@ class OperationMessage(BaseModel):
|
||||||
"""Message sent from one peer to all the others"""
|
"""Message sent from one peer to all the others"""
|
||||||
|
|
||||||
kind: Literal["OperationMessage"] = "OperationMessage"
|
kind: Literal["OperationMessage"] = "OperationMessage"
|
||||||
verb: Literal["upsert", "update", "delete"]
|
verb: Literal["upsert", "update", "delete", "batch"]
|
||||||
subject: Literal["map", "datalayer", "feature"]
|
subject: Literal["map", "datalayer", "feature"]
|
||||||
metadata: Optional[dict] = None
|
metadata: Optional[dict] = None
|
||||||
key: Optional[str] = None
|
key: Optional[str] = None
|
||||||
|
operations: Optional[List] = None
|
||||||
|
|
||||||
|
|
||||||
class PeerMessage(BaseModel):
|
class PeerMessage(BaseModel):
|
||||||
|
|
|
@ -18,6 +18,7 @@ DATALAYER_DATA = {
|
||||||
"features": [
|
"features": [
|
||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
|
"id": "ExNTQ",
|
||||||
"geometry": {
|
"geometry": {
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
"coordinates": [14.68896484375, 48.55297816440071],
|
"coordinates": [14.68896484375, 48.55297816440071],
|
||||||
|
@ -41,7 +42,7 @@ class LicenceFactory(factory.django.DjangoModelFactory):
|
||||||
|
|
||||||
|
|
||||||
class TileLayerFactory(factory.django.DjangoModelFactory):
|
class TileLayerFactory(factory.django.DjangoModelFactory):
|
||||||
name = "Test zoom layer"
|
name = "Test tilelayer"
|
||||||
url_template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url_template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
attribution = "Test layer attribution"
|
attribution = "Test layer attribution"
|
||||||
|
|
||||||
|
|
|
@ -350,8 +350,7 @@ def test_should_redraw_list_on_feature_delete(live_server, openmap, page, bootst
|
||||||
buttons.first.click()
|
buttons.first.click()
|
||||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
page.locator("dialog").get_by_role("button", name="OK").click()
|
||||||
expect(buttons).to_have_count(2)
|
expect(buttons).to_have_count(2)
|
||||||
page.get_by_role("button", name="Cancel edits").click()
|
page.get_by_role("button", name="Undo").click()
|
||||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
|
||||||
expect(buttons).to_have_count(3)
|
expect(buttons).to_have_count(3)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -261,6 +261,9 @@ def test_can_create_new_rule(live_server, page, openmap):
|
||||||
page.get_by_title("AliceBlue").first.click()
|
page.get_by_title("AliceBlue").first.click()
|
||||||
colors = getColors(markers)
|
colors = getColors(markers)
|
||||||
assert colors.count("rgb(240, 248, 255)") == 3
|
assert colors.count("rgb(240, 248, 255)") == 3
|
||||||
|
page.get_by_role("button", name="Undo").click()
|
||||||
|
colors = getColors(markers)
|
||||||
|
assert colors.count("rgb(240, 248, 255)") == 0
|
||||||
|
|
||||||
|
|
||||||
def test_can_deactive_rule_from_list(live_server, page, openmap):
|
def test_can_deactive_rule_from_list(live_server, page, openmap):
|
||||||
|
|
|
@ -64,8 +64,7 @@ def test_cancel_deleting_datalayer_should_restore(
|
||||||
page.get_by_role("button", name="OK").click()
|
page.get_by_role("button", name="OK").click()
|
||||||
expect(markers).to_have_count(0)
|
expect(markers).to_have_count(0)
|
||||||
expect(page.get_by_text("test datalayer")).to_be_hidden()
|
expect(page.get_by_text("test datalayer")).to_be_hidden()
|
||||||
page.get_by_role("button", name="Cancel edits").click()
|
page.get_by_role("button", name="Undo").click()
|
||||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
|
||||||
expect(markers).to_have_count(1)
|
expect(markers).to_have_count(1)
|
||||||
expect(page.locator(".umap-browser").get_by_text("test datalayer")).to_be_visible()
|
expect(page.locator(".umap-browser").get_by_text("test datalayer")).to_be_visible()
|
||||||
|
|
||||||
|
@ -160,7 +159,6 @@ def test_can_create_new_datalayer(live_server, openmap, page, datalayer):
|
||||||
page.locator('input[name="name"]').click()
|
page.locator('input[name="name"]').click()
|
||||||
page.locator('input[name="name"]').fill("Layer A with a new name")
|
page.locator('input[name="name"]').fill("Layer A with a new name")
|
||||||
expect(page.get_by_text("Layer A with a new name")).to_be_visible()
|
expect(page.get_by_text("Layer A with a new name")).to_be_visible()
|
||||||
page.get_by_role("button", name="Save").click()
|
|
||||||
with page.expect_response(re.compile(".*/datalayer/update/.*")):
|
with page.expect_response(re.compile(".*/datalayer/update/.*")):
|
||||||
page.get_by_role("button", name="Save").click()
|
page.get_by_role("button", name="Save").click()
|
||||||
assert DataLayer.objects.count() == 2
|
assert DataLayer.objects.count() == 2
|
||||||
|
|
|
@ -117,8 +117,7 @@ def test_should_reset_style_on_cancel(live_server, openmap, page, bootstrap):
|
||||||
expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count(
|
expect(page.locator(".leaflet-overlay-pane path[fill='GoldenRod']")).to_have_count(
|
||||||
1
|
1
|
||||||
)
|
)
|
||||||
page.get_by_role("button", name="Cancel edits").click()
|
page.get_by_role("button", name="Undo").click()
|
||||||
page.locator("dialog").get_by_role("button", name="OK").click()
|
|
||||||
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -292,9 +292,10 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
|
||||||
# Change name on page two and save
|
# Change name on page two and save
|
||||||
page_two.locator(".leaflet-marker-icon").click(modifiers=["Shift"])
|
page_two.locator(".leaflet-marker-icon").click(modifiers=["Shift"])
|
||||||
page_two.locator('input[name="name"]').fill("name from page two")
|
page_two.locator('input[name="name"]').fill("name from page two")
|
||||||
|
page_two.wait_for_timeout(300) # Time for the input debounce.
|
||||||
|
|
||||||
# Map should be in dirty status
|
# Map should be in dirty status
|
||||||
expect(page_two.get_by_text("Cancel edits")).to_be_visible()
|
expect(page_two.get_by_text("Save", exact=True)).to_be_enabled()
|
||||||
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
|
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
|
||||||
page_two.get_by_role("button", name="Save").click()
|
page_two.get_by_role("button", name="Save").click()
|
||||||
|
|
||||||
|
@ -306,7 +307,7 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
|
||||||
# We should have an alert with some actions
|
# We should have an alert with some actions
|
||||||
expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
|
expect(page_two.get_by_text("Whoops! Other contributor(s) changed")).to_be_visible()
|
||||||
# Map should still be in dirty status
|
# Map should still be in dirty status
|
||||||
expect(page_two.get_by_text("Cancel edits")).to_be_visible()
|
expect(page_two.get_by_text("Save", exact=True)).to_be_enabled()
|
||||||
|
|
||||||
# Override data from page two
|
# Override data from page two
|
||||||
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
|
with page_two.expect_response(re.compile(r".*/datalayer/update/.*")):
|
||||||
|
@ -317,4 +318,4 @@ def test_should_display_alert_on_conflict(context, live_server, datalayer, openm
|
||||||
data = json.loads(Path(saved.geojson.path).read_text())
|
data = json.loads(Path(saved.geojson.path).read_text())
|
||||||
assert data["features"][0]["properties"]["name"] == "name from page two"
|
assert data["features"][0]["properties"]["name"] == "name from page two"
|
||||||
# Map should not be in dirty status anymore
|
# Map should not be in dirty status anymore
|
||||||
expect(page_two.get_by_text("Cancel edits")).to_be_hidden()
|
expect(page_two.get_by_text("Save", exact=True)).to_be_disabled()
|
||||||
|
|
|
@ -16,17 +16,15 @@ def test_reseting_map_would_remove_from_save_queue(
|
||||||
page.on("request", register_request)
|
page.on("request", register_request)
|
||||||
page.locator('input[name="name"]').click()
|
page.locator('input[name="name"]').click()
|
||||||
page.locator('input[name="name"]').fill("new name")
|
page.locator('input[name="name"]').fill("new name")
|
||||||
page.get_by_role("button", name="Cancel edits").click()
|
page.get_by_role("button", name="Undo").click()
|
||||||
page.get_by_role("button", name="OK").click()
|
|
||||||
page.wait_for_timeout(500)
|
page.wait_for_timeout(500)
|
||||||
page.get_by_role("button", name="Edit").click()
|
|
||||||
page.get_by_role("button", name="Manage layers").click()
|
page.get_by_role("button", name="Manage layers").click()
|
||||||
page.get_by_role("button", name="Edit", exact=True).click()
|
page.get_by_role("button", name="Edit", exact=True).click()
|
||||||
page.locator('input[name="name"]').click()
|
page.locator('input[name="name"]').click()
|
||||||
page.locator('input[name="name"]').fill("new datalayer name")
|
page.locator('input[name="name"]').fill("new datalayer name")
|
||||||
page.wait_for_timeout(300) # Time of the Input debounce
|
page.wait_for_timeout(300) # Time of the Input debounce
|
||||||
with page.expect_response(re.compile(".*/datalayer/update/.*")):
|
with page.expect_response(re.compile(".*/datalayer/update/.*")):
|
||||||
page.get_by_role("button", name="Save").click()
|
page.get_by_role("button", name="Save", exact=True).click()
|
||||||
assert len(requests) == 1
|
assert len(requests) == 1
|
||||||
assert requests == [
|
assert requests == [
|
||||||
(
|
(
|
||||||
|
|
267
umap/tests/integration/test_undo_redo.py
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from umap.models import Map, TileLayer
|
||||||
|
|
||||||
|
from ..base import DataLayerFactory
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.django_db
|
||||||
|
|
||||||
|
DATALAYER_DATA = {
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"properties": {
|
||||||
|
"name": "name poly",
|
||||||
|
},
|
||||||
|
"id": "gyNzM",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[
|
||||||
|
[11.25, 53.585984],
|
||||||
|
[10.151367, 52.975108],
|
||||||
|
[12.689209, 52.167194],
|
||||||
|
[14.084473, 53.199452],
|
||||||
|
[12.634277, 53.618579],
|
||||||
|
[11.25, 53.585984],
|
||||||
|
[11.25, 53.585984],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def map_with_polygon(map, live_server):
|
||||||
|
map.settings["properties"]["zoom"] = 6
|
||||||
|
map.settings["geometry"] = {
|
||||||
|
"type": "Point",
|
||||||
|
"coordinates": [8.429, 53.239],
|
||||||
|
}
|
||||||
|
map.edit_status = Map.ANONYMOUS
|
||||||
|
map.save()
|
||||||
|
DataLayerFactory(map=map, data=DATALAYER_DATA)
|
||||||
|
return map
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_undo_redo_map_name_change(page, live_server, tilelayer):
|
||||||
|
page.goto(f"{live_server.url}/en/map/new/")
|
||||||
|
|
||||||
|
expect(page.locator(".edit-undo")).to_be_disabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||||
|
page.get_by_title("Edit map name and caption").click()
|
||||||
|
name_input = page.locator('.map-metadata input[name="name"]')
|
||||||
|
expect(name_input).to_be_visible()
|
||||||
|
name_input.click()
|
||||||
|
name_input.press("Control+a")
|
||||||
|
name_input.fill("New map name")
|
||||||
|
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||||
|
map_name = page.locator(".umap-main-edit-toolbox .map-name")
|
||||||
|
expect(map_name).to_have_text("New map name")
|
||||||
|
name_input.fill("New name again")
|
||||||
|
expect(map_name).to_have_text("New name again")
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(map_name).to_have_text("New map name")
|
||||||
|
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_enabled()
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
expect(map_name).to_have_text("New name again")
|
||||||
|
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(map_name).to_have_text("New map name")
|
||||||
|
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_enabled()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_undo_redo_layer_color_change(
|
||||||
|
page, map_with_polygon, live_server, tilelayer
|
||||||
|
):
|
||||||
|
page.goto(f"{live_server.url}{map_with_polygon.get_absolute_url()}?edit")
|
||||||
|
|
||||||
|
expect(page.locator(".edit-undo")).to_be_disabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||||
|
page.get_by_role("button", name="Manage layers").click()
|
||||||
|
page.locator(".panel").get_by_title("Edit", exact=True).click()
|
||||||
|
page.get_by_text("Shape properties").click()
|
||||||
|
page.locator(".umap-field-color .define").click()
|
||||||
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
||||||
|
page.get_by_title("DarkRed").first.click()
|
||||||
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(1)
|
||||||
|
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(1)
|
||||||
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(0)
|
||||||
|
expect(page.locator(".edit-undo")).to_be_disabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_enabled()
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkRed']")).to_have_count(1)
|
||||||
|
expect(page.locator(".leaflet-overlay-pane path[fill='DarkBlue']")).to_have_count(0)
|
||||||
|
expect(page.locator(".edit-undo")).to_be_enabled()
|
||||||
|
expect(page.locator(".edit-redo")).to_be_disabled()
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_undo_redo_tilelayer_change(live_server, page, openmap, tilelayer):
|
||||||
|
TileLayer.objects.create(
|
||||||
|
url_template="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
|
||||||
|
attribution="OSM/Carto",
|
||||||
|
name="Black Tiles",
|
||||||
|
)
|
||||||
|
page.goto(f"{live_server.url}{openmap.get_absolute_url()}?edit")
|
||||||
|
old_pattern = re.compile(
|
||||||
|
r"https://[abc]{1}.tile.openstreetmap.fr/osmfr/\d+/\d+/\d+.png"
|
||||||
|
)
|
||||||
|
tiles = page.locator(".leaflet-tile-pane img")
|
||||||
|
expect(tiles.first).to_have_attribute("src", old_pattern)
|
||||||
|
|
||||||
|
new_pattern = re.compile(
|
||||||
|
r"https://[abcd]{1}.basemaps.cartocdn.com/dark_all/\d+/\d+/\d+.png"
|
||||||
|
)
|
||||||
|
page.get_by_role("button", name="Change tilelayers").click()
|
||||||
|
page.locator("li").filter(has_text="Black Tiles").get_by_role("img").click()
|
||||||
|
|
||||||
|
tiles = page.locator(".leaflet-tile-pane img")
|
||||||
|
expect(tiles.first).to_have_attribute("src", new_pattern)
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
tiles = page.locator(".leaflet-tile-pane img")
|
||||||
|
expect(tiles.first).to_have_attribute("src", old_pattern)
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
tiles = page.locator(".leaflet-tile-pane img")
|
||||||
|
expect(tiles.first).to_have_attribute("src", new_pattern)
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_undo_redo_marker_drag(live_server, page, tilelayer):
|
||||||
|
page.goto(f"{live_server.url}/en/map/new")
|
||||||
|
|
||||||
|
marker = page.locator(".leaflet-marker-icon")
|
||||||
|
map = page.locator("#map")
|
||||||
|
|
||||||
|
# Create a marker
|
||||||
|
page.get_by_title("Draw a marker").click()
|
||||||
|
map.click(position={"x": 225, "y": 225})
|
||||||
|
expect(marker).to_have_count(1)
|
||||||
|
|
||||||
|
# Drag marker
|
||||||
|
old_bbox = marker.bounding_box()
|
||||||
|
marker.first.drag_to(map, target_position={"x": 250, "y": 250})
|
||||||
|
assert marker.bounding_box() != old_bbox
|
||||||
|
|
||||||
|
# Undo
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
assert marker.bounding_box() == old_bbox
|
||||||
|
|
||||||
|
# Redo
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
assert marker.bounding_box() != old_bbox
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_undo_redo_polygon_geometry_change(live_server, page, tilelayer):
|
||||||
|
page.goto(f"{live_server.url}/en/map/new")
|
||||||
|
|
||||||
|
# Click on the Draw a polygon button on a new map.
|
||||||
|
page.get_by_title("Draw a polygon").click()
|
||||||
|
|
||||||
|
polygon = page.locator("path[fill='DarkBlue']")
|
||||||
|
expect(polygon).to_have_count(0)
|
||||||
|
|
||||||
|
# Click on the map, it will create a polygon.
|
||||||
|
map = page.locator("#map")
|
||||||
|
map.click(position={"x": 200, "y": 200})
|
||||||
|
map.click(position={"x": 100, "y": 200})
|
||||||
|
map.click(position={"x": 100, "y": 100})
|
||||||
|
map.click(position={"x": 100, "y": 100})
|
||||||
|
|
||||||
|
# It is created on peerA, and should be on peerB
|
||||||
|
expect(polygon).to_have_count(1)
|
||||||
|
old_bbox = polygon.bounding_box()
|
||||||
|
|
||||||
|
edited_vertex = page.locator(".leaflet-middle-icon:nth-child(3)").first
|
||||||
|
edited_vertex.drag_to(map, target_position={"x": 250, "y": 250})
|
||||||
|
page.keyboard.press("Escape")
|
||||||
|
|
||||||
|
assert polygon.bounding_box() != old_bbox
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
assert polygon.bounding_box() == old_bbox
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
assert polygon.bounding_box() != old_bbox
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_undo_redo_marker_create(live_server, page, tilelayer):
|
||||||
|
page.goto(f"{live_server.url}/en/map/new")
|
||||||
|
|
||||||
|
page.get_by_title("Open Browser").click()
|
||||||
|
marker = page.locator(".leaflet-marker-icon")
|
||||||
|
map = page.locator("#map")
|
||||||
|
|
||||||
|
# Create a marker
|
||||||
|
page.get_by_title("Draw a marker").click()
|
||||||
|
map.click(position={"x": 600, "y": 100})
|
||||||
|
expect(marker).to_have_count(1)
|
||||||
|
expect(page.locator(".panel .datalayer")).to_have_count(1)
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(marker).to_have_count(0)
|
||||||
|
# Layer still exists
|
||||||
|
expect(page.locator(".panel .datalayer")).to_have_count(1)
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(page.locator(".panel .datalayer")).to_have_count(0)
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
expect(page.locator(".panel .datalayer")).to_have_count(1)
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
expect(marker).to_have_count(1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_undo_redo_import(live_server, page, tilelayer):
|
||||||
|
page.goto(f"{live_server.url}/map/new/")
|
||||||
|
page.get_by_title("Open Browser").click()
|
||||||
|
|
||||||
|
page.get_by_title("Import data").click()
|
||||||
|
file_input = page.locator("input[type='file']")
|
||||||
|
with page.expect_file_chooser() as fc_info:
|
||||||
|
file_input.click()
|
||||||
|
file_chooser = fc_info.value
|
||||||
|
path = Path(__file__).parent.parent / "fixtures/test_upload_data.json"
|
||||||
|
file_chooser.set_files(path)
|
||||||
|
page.get_by_role("button", name="Import data", exact=True).click()
|
||||||
|
# Close the import panel
|
||||||
|
page.keyboard.press("Escape")
|
||||||
|
|
||||||
|
layers = page.locator(".umap-browser .datalayer")
|
||||||
|
expect(layers).to_have_count(1)
|
||||||
|
|
||||||
|
features_count = page.locator(".umap-browser .datalayer-counter")
|
||||||
|
expect(features_count).to_have_text("(5)")
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(features_count).to_be_hidden()
|
||||||
|
expect(layers).to_have_count(1)
|
||||||
|
|
||||||
|
page.locator(".edit-undo").click()
|
||||||
|
expect(layers).to_have_count(0)
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
expect(layers).to_have_count(1)
|
||||||
|
|
||||||
|
page.locator(".edit-redo").click()
|
||||||
|
expect(features_count).to_have_text("(5)")
|
|
@ -659,3 +659,73 @@ def test_should_sync_line_on_escape(new_page, asgi_live_server, tilelayer):
|
||||||
|
|
||||||
expect(peerA.locator("path")).to_have_count(1)
|
expect(peerA.locator("path")).to_have_count(1)
|
||||||
expect(peerB.locator("path")).to_have_count(1)
|
expect(peerB.locator("path")).to_have_count(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xdist_group(name="websockets")
|
||||||
|
def test_should_sync_datalayer_clear(
|
||||||
|
new_page, asgi_live_server, tilelayer, map, datalayer
|
||||||
|
):
|
||||||
|
map.settings["properties"]["syncEnabled"] = True
|
||||||
|
map.edit_status = Map.ANONYMOUS
|
||||||
|
map.save()
|
||||||
|
|
||||||
|
# Create two tabs
|
||||||
|
peerA = new_page("Page A")
|
||||||
|
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
peerB = new_page("Page B")
|
||||||
|
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||||
|
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||||
|
|
||||||
|
# Clear layer in peer A
|
||||||
|
peerA.get_by_role("button", name="Manage layers").click()
|
||||||
|
peerA.get_by_role("button", name="Edit", exact=True).click()
|
||||||
|
peerA.locator("summary").filter(has_text="Advanced actions").click()
|
||||||
|
peerA.get_by_role("button", name="Empty").click()
|
||||||
|
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(0)
|
||||||
|
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(0)
|
||||||
|
|
||||||
|
# Undo in peer A
|
||||||
|
peerA.get_by_role("button", name="Undo").click()
|
||||||
|
expect(peerA.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||||
|
expect(peerB.locator(".leaflet-marker-icon")).to_have_count(1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.xdist_group(name="websockets")
|
||||||
|
def test_should_save_remote_dirty_datalayers(new_page, asgi_live_server, tilelayer):
|
||||||
|
map = MapFactory(name="sync", edit_status=Map.ANONYMOUS)
|
||||||
|
map.settings["properties"]["syncEnabled"] = True
|
||||||
|
map.save()
|
||||||
|
|
||||||
|
assert not DataLayer.objects.count()
|
||||||
|
|
||||||
|
# Create two tabs
|
||||||
|
peerA = new_page("Page A")
|
||||||
|
peerA.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
peerB = new_page("Page B")
|
||||||
|
peerB.goto(f"{asgi_live_server.url}{map.get_absolute_url()}?edit")
|
||||||
|
|
||||||
|
# Create a new layer from peerA
|
||||||
|
peerA.get_by_role("button", name="Manage layers").click()
|
||||||
|
peerA.get_by_role("button", name="Add a layer").click()
|
||||||
|
|
||||||
|
# Create a new layer from peerB
|
||||||
|
peerB.get_by_role("button", name="Manage layers").click()
|
||||||
|
peerB.get_by_role("button", name="Add a layer").click()
|
||||||
|
|
||||||
|
# Save from peerA to the server
|
||||||
|
counter = 0
|
||||||
|
|
||||||
|
def on_response(response):
|
||||||
|
nonlocal counter
|
||||||
|
if "/datalayer/create/" in response.url:
|
||||||
|
counter += 1
|
||||||
|
# Wait for the two datalayer saves
|
||||||
|
if counter == 2:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
with peerA.expect_response(on_response):
|
||||||
|
peerA.get_by_role("button", name="Save").click()
|
||||||
|
|
||||||
|
assert DataLayer.objects.count() == 2
|
||||||
|
|
|
@ -103,6 +103,7 @@ def test_get_version(map, datalayer):
|
||||||
],
|
],
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
},
|
},
|
||||||
|
"id": "ExNTQ",
|
||||||
"properties": {
|
"properties": {
|
||||||
"_umap_options": {
|
"_umap_options": {
|
||||||
"color": "DarkCyan",
|
"color": "DarkCyan",
|
||||||
|
|
|
@ -694,6 +694,7 @@ def test_download(client, map, datalayer):
|
||||||
"coordinates": [14.68896484375, 48.55297816440071],
|
"coordinates": [14.68896484375, 48.55297816440071],
|
||||||
"type": "Point",
|
"type": "Point",
|
||||||
},
|
},
|
||||||
|
"id": "ExNTQ",
|
||||||
"properties": {
|
"properties": {
|
||||||
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
|
"_umap_options": {"color": "DarkCyan", "iconClass": "Ball"},
|
||||||
"description": "Da place anonymous again 755",
|
"description": "Da place anonymous again 755",
|
||||||
|
|