umap-project 2.6.3__py3-none-any.whl → 2.7.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of umap-project might be problematic. Click here for more details.
- umap/__init__.py +1 -1
- umap/admin.py +64 -1
- umap/context_processors.py +1 -0
- umap/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- umap/locale/cs_CZ/LC_MESSAGES/django.po +96 -92
- umap/locale/de/LC_MESSAGES/django.mo +0 -0
- umap/locale/de/LC_MESSAGES/django.po +19 -18
- umap/locale/en/LC_MESSAGES/django.po +47 -43
- umap/locale/fr/LC_MESSAGES/django.mo +0 -0
- umap/locale/fr/LC_MESSAGES/django.po +51 -47
- umap/locale/pt/LC_MESSAGES/django.mo +0 -0
- umap/locale/pt/LC_MESSAGES/django.po +64 -60
- umap/management/commands/clean_tilelayer.py +152 -0
- umap/management/commands/purge_purgatory.py +28 -0
- umap/models.py +27 -2
- umap/settings/base.py +2 -0
- umap/static/umap/base.css +4 -4
- umap/static/umap/css/contextmenu.css +5 -0
- umap/static/umap/css/icon.css +7 -2
- umap/static/umap/img/16-white.svg +9 -2
- umap/static/umap/img/16.svg +3 -0
- umap/static/umap/img/source/16-white.svg +10 -3
- umap/static/umap/img/source/16.svg +4 -1
- umap/static/umap/js/modules/autocomplete.js +7 -3
- umap/static/umap/js/modules/browser.js +7 -1
- umap/static/umap/js/modules/caption.js +6 -1
- umap/static/umap/js/modules/data/features.js +176 -2
- umap/static/umap/js/modules/data/layer.js +31 -26
- umap/static/umap/js/modules/formatter.js +3 -2
- umap/static/umap/js/modules/global.js +2 -0
- umap/static/umap/js/modules/importers/communesfr.js +13 -1
- umap/static/umap/js/modules/permissions.js +123 -93
- umap/static/umap/js/modules/rendering/ui.js +37 -212
- umap/static/umap/js/modules/sync/engine.js +365 -14
- umap/static/umap/js/modules/sync/hlc.js +106 -0
- umap/static/umap/js/modules/sync/updaters.js +4 -4
- umap/static/umap/js/modules/sync/websocket.js +1 -1
- umap/static/umap/js/modules/ui/base.js +2 -2
- umap/static/umap/js/modules/ui/contextmenu.js +34 -17
- umap/static/umap/js/modules/urls.js +5 -1
- umap/static/umap/js/modules/utils.js +5 -1
- umap/static/umap/js/umap.controls.js +47 -47
- umap/static/umap/js/umap.core.js +3 -3
- umap/static/umap/js/umap.forms.js +3 -1
- umap/static/umap/js/umap.js +95 -112
- umap/static/umap/locale/br.js +13 -4
- umap/static/umap/locale/br.json +13 -4
- umap/static/umap/locale/ca.js +21 -12
- umap/static/umap/locale/ca.json +21 -12
- umap/static/umap/locale/cs_CZ.js +87 -78
- umap/static/umap/locale/cs_CZ.json +87 -78
- umap/static/umap/locale/de.js +17 -8
- umap/static/umap/locale/de.json +17 -8
- umap/static/umap/locale/en.js +9 -2
- umap/static/umap/locale/en.json +9 -2
- umap/static/umap/locale/eu.js +10 -3
- umap/static/umap/locale/eu.json +10 -3
- umap/static/umap/locale/fa_IR.js +11 -4
- umap/static/umap/locale/fa_IR.json +11 -4
- umap/static/umap/locale/fr.js +11 -4
- umap/static/umap/locale/fr.json +11 -4
- umap/static/umap/locale/hu.js +10 -3
- umap/static/umap/locale/hu.json +10 -3
- umap/static/umap/locale/pt.js +17 -8
- umap/static/umap/locale/pt.json +17 -8
- umap/static/umap/locale/pt_PT.js +13 -4
- umap/static/umap/locale/pt_PT.json +13 -4
- umap/static/umap/locale/zh_TW.js +13 -4
- umap/static/umap/locale/zh_TW.json +13 -4
- umap/static/umap/map.css +7 -22
- umap/static/umap/unittests/hlc.js +158 -0
- umap/static/umap/unittests/sync.js +321 -15
- umap/static/umap/unittests/utils.js +23 -0
- umap/static/umap/vendors/georsstogeojson/GeoRSSToGeoJSON.js +111 -80
- umap/templates/umap/dashboard_menu.html +4 -2
- umap/templates/umap/js.html +0 -4
- umap/tests/integration/test_anonymous_owned_map.py +1 -0
- umap/tests/integration/test_basics.py +1 -1
- umap/tests/integration/test_circles_layer.py +12 -0
- umap/tests/integration/test_datalayer.py +5 -0
- umap/tests/integration/test_draw_polygon.py +17 -9
- umap/tests/integration/test_draw_polyline.py +12 -8
- umap/tests/integration/test_edit_datalayer.py +4 -6
- umap/tests/integration/test_edit_map.py +1 -1
- umap/tests/integration/test_import.py +5 -0
- umap/tests/integration/test_map.py +5 -0
- umap/tests/integration/test_owned_map.py +1 -1
- umap/tests/integration/test_view_polygon.py +12 -12
- umap/tests/integration/test_websocket_sync.py +65 -3
- umap/tests/test_clean_tilelayer.py +83 -0
- umap/tests/test_datalayer.py +24 -0
- umap/tests/test_map_views.py +1 -0
- umap/tests/test_purge_purgatory.py +25 -0
- umap/tests/test_websocket_server.py +22 -0
- umap/urls.py +5 -1
- umap/views.py +6 -3
- umap/websocket_server.py +130 -27
- {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/METADATA +9 -9
- {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/RECORD +102 -97
- umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css +0 -1
- umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js +0 -7
- {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/WHEEL +0 -0
- {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/entry_points.txt +0 -0
- {umap_project-2.6.3.dist-info → umap_project-2.7.0b0.dist-info}/licenses/LICENSE +0 -0
umap/static/umap/map.css
CHANGED
|
@@ -592,17 +592,16 @@ ul.photon-autocomplete {
|
|
|
592
592
|
left: 0;
|
|
593
593
|
right: 0;
|
|
594
594
|
height: 46px;
|
|
595
|
-
background-color: var(--color-darkGray);
|
|
596
595
|
padding: 0 10px;
|
|
597
596
|
text-align: start;
|
|
598
597
|
line-height: var(--control-size);
|
|
599
598
|
cursor: auto;
|
|
600
599
|
border-bottom: 1px solid #222;
|
|
601
600
|
z-index: var(--zindex-panels);
|
|
602
|
-
opacity: 0.98;
|
|
603
|
-
color: #fff;
|
|
604
601
|
display: flex;
|
|
605
602
|
justify-content: space-between;
|
|
603
|
+
background-color: var(--background-color);
|
|
604
|
+
color: var(--text-color);
|
|
606
605
|
}
|
|
607
606
|
.umap-left-edit-toolbox,
|
|
608
607
|
.umap-right-edit-toolbox {
|
|
@@ -820,7 +819,6 @@ a.umap-control-caption,
|
|
|
820
819
|
.umap-browser .off .feature {
|
|
821
820
|
display: none;
|
|
822
821
|
}
|
|
823
|
-
.umap-facet-search .formbox,
|
|
824
822
|
.umap-browser .datalayer {
|
|
825
823
|
margin-bottom: 2px;
|
|
826
824
|
border-radius: 2px;
|
|
@@ -831,7 +829,7 @@ a.umap-control-caption,
|
|
|
831
829
|
.umap-browser.dark .datalayer ul {
|
|
832
830
|
border: 1px solid #232729;
|
|
833
831
|
}
|
|
834
|
-
.umap-browser h5
|
|
832
|
+
.umap-browser h5 {
|
|
835
833
|
margin-bottom: 0;
|
|
836
834
|
overflow: hidden;
|
|
837
835
|
padding-inline-start: 5px;
|
|
@@ -849,7 +847,7 @@ a.umap-control-caption,
|
|
|
849
847
|
.umap-browser h5 span {
|
|
850
848
|
margin-inline-start: 10px;
|
|
851
849
|
}
|
|
852
|
-
.umap-browser li
|
|
850
|
+
.umap-browser li {
|
|
853
851
|
padding: 2px 0;
|
|
854
852
|
white-space: nowrap;
|
|
855
853
|
overflow: hidden;
|
|
@@ -867,13 +865,15 @@ a.umap-control-caption,
|
|
|
867
865
|
-moz-box-sizing:border-box;
|
|
868
866
|
-webkit-box-sizing:border-box;
|
|
869
867
|
box-sizing: border-box;
|
|
870
|
-
background: none;
|
|
871
868
|
display: inline-block;
|
|
872
869
|
padding: 0;
|
|
873
870
|
width: 24px;
|
|
874
871
|
text-align: center;
|
|
875
872
|
margin-inline-start: 5px;
|
|
876
873
|
}
|
|
874
|
+
.umap-browser .marker .feature-color {
|
|
875
|
+
background: none;
|
|
876
|
+
}
|
|
877
877
|
.umap-browser.dark .datalayer .feature-color {
|
|
878
878
|
box-shadow: 0 0 2px 0 #999 inset;
|
|
879
879
|
}
|
|
@@ -884,18 +884,6 @@ a.umap-control-caption,
|
|
|
884
884
|
font-style: normal;
|
|
885
885
|
font-weight: bold;
|
|
886
886
|
}
|
|
887
|
-
.umap-browser .polygon .feature-color,
|
|
888
|
-
.umap-browser .polyline .feature-color {
|
|
889
|
-
box-shadow: 0 0 2px 0 black inset;
|
|
890
|
-
background-image: url('./img/24.svg');
|
|
891
|
-
background-size: 500%;
|
|
892
|
-
}
|
|
893
|
-
.umap-browser .polyline .feature-color {
|
|
894
|
-
background-position: -72px -23px;
|
|
895
|
-
}
|
|
896
|
-
.umap-browser .polygon .feature-color {
|
|
897
|
-
background-position: -48px -25px;
|
|
898
|
-
}
|
|
899
887
|
.umap-browser .datalayer-toggle-list {
|
|
900
888
|
float: inline-end;
|
|
901
889
|
margin-inline-end: 5px;
|
|
@@ -1499,9 +1487,6 @@ span.popup-icon {
|
|
|
1499
1487
|
.umap-main-edit-toolbox .umap-user {
|
|
1500
1488
|
margin-inline-end: 10px;
|
|
1501
1489
|
}
|
|
1502
|
-
.umap-main-edit-toolbox .umap-user:after {
|
|
1503
|
-
display: none;
|
|
1504
|
-
}
|
|
1505
1490
|
}
|
|
1506
1491
|
@media all and (max-width: 640px) {
|
|
1507
1492
|
.umap-main-edit-toolbox .umap-user {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it } from 'mocha'
|
|
2
|
+
import sinon from 'sinon'
|
|
3
|
+
|
|
4
|
+
import pkg from 'chai'
|
|
5
|
+
const { expect } = pkg
|
|
6
|
+
|
|
7
|
+
import { HybridLogicalClock } from '../js/modules/sync/hlc.js'
|
|
8
|
+
|
|
9
|
+
describe('HybridLogicalClock', () => {
|
|
10
|
+
let clock
|
|
11
|
+
|
|
12
|
+
describe('#parse', () => {
|
|
13
|
+
it('should reject invalid values', () => {
|
|
14
|
+
clock = new HybridLogicalClock()
|
|
15
|
+
expect(() => clock.parse('invalid')).to.throw()
|
|
16
|
+
expect(() => clock.parse('123:456')).to.throw()
|
|
17
|
+
expect(() => clock.parse('123:456:789:000')).to.throw()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('should parse correct values', () => {
|
|
21
|
+
clock = new HybridLogicalClock()
|
|
22
|
+
const result = clock.parse('1625097600000:42:abc-123')
|
|
23
|
+
expect(result).to.deep.equal({
|
|
24
|
+
walltime: '1625097600000',
|
|
25
|
+
nn: 42,
|
|
26
|
+
id: 'abc-123',
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should default to 0 for nn if none is provided', () => {
|
|
31
|
+
clock = new HybridLogicalClock()
|
|
32
|
+
const result = clock.parse('1625097600000::abc-123')
|
|
33
|
+
expect(result).to.deep.equal({
|
|
34
|
+
walltime: '1625097600000',
|
|
35
|
+
nn: 0,
|
|
36
|
+
id: 'abc-123',
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('#serialize', () => {
|
|
42
|
+
it('should correctly serialize the clock', () => {
|
|
43
|
+
clock = new HybridLogicalClock(1625097600000, 42, 'abc-123')
|
|
44
|
+
expect(clock.serialize()).to.equal('1625097600000:42:abc-123')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('#tick', () => {
|
|
49
|
+
it('should increment walltime when current time is greater', () => {
|
|
50
|
+
const now = Date.now()
|
|
51
|
+
clock = new HybridLogicalClock(now - 1000, 0, 'test')
|
|
52
|
+
const result = clock.tick()
|
|
53
|
+
const parsed = clock.parse(result)
|
|
54
|
+
expect(parsed.walltime).to.be.at.least(now.toString())
|
|
55
|
+
expect(parsed.nn).to.equal(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should increment nn when current time is not greater', () => {
|
|
59
|
+
const now = Date.now()
|
|
60
|
+
clock = new HybridLogicalClock(now, 5, 'test')
|
|
61
|
+
sinon.useFakeTimers(now)
|
|
62
|
+
const result = clock.tick()
|
|
63
|
+
const parsed = clock.parse(result)
|
|
64
|
+
expect(parsed.walltime).to.equal(now.toString())
|
|
65
|
+
expect(parsed.nn).to.equal(6)
|
|
66
|
+
sinon.restore()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('#receive', () => {
|
|
71
|
+
it("should use current time when it's greater than both local and remote", () => {
|
|
72
|
+
const now = Date.now()
|
|
73
|
+
clock = new HybridLogicalClock(now - 1000, 0, 'local')
|
|
74
|
+
const result = clock.receive(`${now - 500}:0:remote`)
|
|
75
|
+
expect(result.walltime).to.be.at.least(now)
|
|
76
|
+
expect(result.nn).to.equal(0)
|
|
77
|
+
expect(result.id).to.equal('local')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should increment nn when local and remote times are equal', () => {
|
|
81
|
+
const now = Date.now()
|
|
82
|
+
clock = new HybridLogicalClock(now, 5, 'local')
|
|
83
|
+
const result = clock.receive(`${now}:7:remote`)
|
|
84
|
+
expect(result.walltime).to.equal(now)
|
|
85
|
+
expect(result.nn).to.equal(8)
|
|
86
|
+
expect(result.id).to.equal('local')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should use remote time and increment nn when remote time is greater', () => {
|
|
90
|
+
const now = Date.now()
|
|
91
|
+
clock = new HybridLogicalClock(now - 1000, 5, 'local')
|
|
92
|
+
const result = clock.receive(`${now}:7:remote`)
|
|
93
|
+
expect(result.walltime).to.be.least(now.toString())
|
|
94
|
+
expect(result.nn).to.equal(8)
|
|
95
|
+
expect(result.id).to.equal('local')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should increment local nn when local time is greater', () => {
|
|
99
|
+
const now = Date.now()
|
|
100
|
+
clock = new HybridLogicalClock(now, 5, 'local')
|
|
101
|
+
const result = clock.receive(`${now - 1000}:7:remote`)
|
|
102
|
+
expect(result.walltime).to.be.least(now)
|
|
103
|
+
expect(result.nn).to.equal(6)
|
|
104
|
+
expect(result.id).to.equal('local')
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should maintain causal order across multiple operations', () => {
|
|
109
|
+
const hlc = new HybridLogicalClock()
|
|
110
|
+
|
|
111
|
+
// Simulate a sequence of events
|
|
112
|
+
const event1 = hlc.tick()
|
|
113
|
+
|
|
114
|
+
// Simulate some time passing
|
|
115
|
+
const clock = sinon.useFakeTimers(Date.now() + 100)
|
|
116
|
+
|
|
117
|
+
const event2 = hlc.tick()
|
|
118
|
+
|
|
119
|
+
// Simulate receiving a message from another node
|
|
120
|
+
const remoteEvent = hlc.receive(`${Date.now() - 50}:5:remote-id`)
|
|
121
|
+
|
|
122
|
+
const event3 = hlc.tick()
|
|
123
|
+
|
|
124
|
+
// Advance time significantly
|
|
125
|
+
clock.tick(1000)
|
|
126
|
+
|
|
127
|
+
const event4 = hlc.tick()
|
|
128
|
+
|
|
129
|
+
// Clean up the fake timer
|
|
130
|
+
clock.restore()
|
|
131
|
+
|
|
132
|
+
// Parse all events
|
|
133
|
+
const parsedEvent1 = hlc.parse(event1)
|
|
134
|
+
const parsedEvent2 = hlc.parse(event2)
|
|
135
|
+
const parsedEvent3 = hlc.parse(event3)
|
|
136
|
+
const parsedEvent4 = hlc.parse(event4)
|
|
137
|
+
|
|
138
|
+
// Assertions to ensure causal order is maintained
|
|
139
|
+
expect(parsedEvent2.walltime).to.be.greaterThan(parsedEvent1.walltime)
|
|
140
|
+
expect(parsedEvent3.walltime).to.equal(parsedEvent2.walltime)
|
|
141
|
+
expect(parsedEvent3.nn).to.be.greaterThan(parsedEvent2.nn)
|
|
142
|
+
expect(parsedEvent4.walltime).to.be.greaterThan(parsedEvent3.walltime)
|
|
143
|
+
|
|
144
|
+
// Check that all events have the same id
|
|
145
|
+
const uniqueIds = new Set([
|
|
146
|
+
parsedEvent1.id,
|
|
147
|
+
parsedEvent2.id,
|
|
148
|
+
parsedEvent3.id,
|
|
149
|
+
parsedEvent4.id,
|
|
150
|
+
])
|
|
151
|
+
expect(uniqueIds.size).to.equal(1)
|
|
152
|
+
|
|
153
|
+
// Ensure we can compare events as strings and maintain the same order
|
|
154
|
+
const events = [event1, event2, event3, event4]
|
|
155
|
+
const sortedEvents = [...events].sort()
|
|
156
|
+
expect(sortedEvents).to.deep.equal(events)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -5,10 +5,10 @@ import pkg from 'chai'
|
|
|
5
5
|
const { expect } = pkg
|
|
6
6
|
|
|
7
7
|
import { MapUpdater } from '../js/modules/sync/updaters.js'
|
|
8
|
-
import { SyncEngine } from '../js/modules/sync/engine.js'
|
|
8
|
+
import { SyncEngine, Operations } from '../js/modules/sync/engine.js'
|
|
9
9
|
|
|
10
10
|
describe('SyncEngine', () => {
|
|
11
|
-
it('should initialize methods even before start',
|
|
11
|
+
it('should initialize methods even before start', () => {
|
|
12
12
|
const engine = new SyncEngine({})
|
|
13
13
|
engine.upsert()
|
|
14
14
|
engine.update()
|
|
@@ -16,8 +16,8 @@ describe('SyncEngine', () => {
|
|
|
16
16
|
})
|
|
17
17
|
})
|
|
18
18
|
|
|
19
|
-
describe('#dispatch',
|
|
20
|
-
it('should raise an error on unknown updater',
|
|
19
|
+
describe('#dispatch', () => {
|
|
20
|
+
it('should raise an error on unknown updater', () => {
|
|
21
21
|
const dispatcher = new SyncEngine({})
|
|
22
22
|
expect(() => {
|
|
23
23
|
dispatcher.dispatch({
|
|
@@ -27,7 +27,7 @@ describe('#dispatch', function () {
|
|
|
27
27
|
})
|
|
28
28
|
}).to.throw(Error)
|
|
29
29
|
})
|
|
30
|
-
it('should produce an error on malformated messages',
|
|
30
|
+
it('should produce an error on malformated messages', () => {
|
|
31
31
|
const dispatcher = new SyncEngine({})
|
|
32
32
|
expect(() => {
|
|
33
33
|
dispatcher.dispatch({
|
|
@@ -36,7 +36,7 @@ describe('#dispatch', function () {
|
|
|
36
36
|
})
|
|
37
37
|
}).to.throw(Error)
|
|
38
38
|
})
|
|
39
|
-
it('should raise an unknown operations',
|
|
39
|
+
it('should raise an unknown operations', () => {
|
|
40
40
|
const dispatcher = new SyncEngine({})
|
|
41
41
|
expect(() => {
|
|
42
42
|
dispatcher.dispatch({
|
|
@@ -47,55 +47,55 @@ describe('#dispatch', function () {
|
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
describe('Updaters', () => {
|
|
50
|
-
describe('BaseUpdater',
|
|
50
|
+
describe('BaseUpdater', () => {
|
|
51
51
|
let updater
|
|
52
52
|
let map
|
|
53
53
|
let obj
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
beforeEach(() => {
|
|
56
56
|
map = {}
|
|
57
57
|
updater = new MapUpdater(map)
|
|
58
58
|
obj = {}
|
|
59
59
|
})
|
|
60
|
-
it('should be able to set object properties',
|
|
60
|
+
it('should be able to set object properties', () => {
|
|
61
61
|
let obj = {}
|
|
62
62
|
updater.updateObjectValue(obj, 'foo', 'foo')
|
|
63
63
|
expect(obj).deep.equal({ foo: 'foo' })
|
|
64
64
|
})
|
|
65
65
|
|
|
66
|
-
it('should be able to set object properties recursively on existing objects',
|
|
66
|
+
it('should be able to set object properties recursively on existing objects', () => {
|
|
67
67
|
let obj = { foo: {} }
|
|
68
68
|
updater.updateObjectValue(obj, 'foo.bar', 'foo')
|
|
69
69
|
expect(obj).deep.equal({ foo: { bar: 'foo' } })
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it('should be able to set object properties recursively on deep objects',
|
|
72
|
+
it('should be able to set object properties recursively on deep objects', () => {
|
|
73
73
|
let obj = { foo: { bar: { baz: {} } } }
|
|
74
74
|
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
|
75
75
|
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
-
it('should be able to replace object properties recursively on deep objects',
|
|
78
|
+
it('should be able to replace object properties recursively on deep objects', () => {
|
|
79
79
|
let obj = { foo: { bar: { baz: { test: 'test' } } } }
|
|
80
80
|
updater.updateObjectValue(obj, 'foo.bar.baz.test', 'value')
|
|
81
81
|
expect(obj).deep.equal({ foo: { bar: { baz: { test: 'value' } } } })
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
it('should not set object properties recursively on non-existing objects',
|
|
84
|
+
it('should not set object properties recursively on non-existing objects', () => {
|
|
85
85
|
let obj = { foo: {} }
|
|
86
86
|
updater.updateObjectValue(obj, 'bar.bar', 'value')
|
|
87
87
|
|
|
88
88
|
expect(obj).deep.equal({ foo: {} })
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
it('should delete keys for undefined values',
|
|
91
|
+
it('should delete keys for undefined values', () => {
|
|
92
92
|
let obj = { foo: 'foo' }
|
|
93
93
|
updater.updateObjectValue(obj, 'foo', undefined)
|
|
94
94
|
|
|
95
95
|
expect(obj).deep.equal({})
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it('should delete keys for undefined values, recursively',
|
|
98
|
+
it('should delete keys for undefined values, recursively', () => {
|
|
99
99
|
let obj = { foo: { bar: 'bar' } }
|
|
100
100
|
updater.updateObjectValue(obj, 'foo.bar', undefined)
|
|
101
101
|
|
|
@@ -103,3 +103,309 @@ describe('Updaters', () => {
|
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
105
|
})
|
|
106
|
+
|
|
107
|
+
describe('Operations', () => {
|
|
108
|
+
describe('haveSameContext', () => {
|
|
109
|
+
const createOperation = (overrides = {}) => ({
|
|
110
|
+
subject: 'feature',
|
|
111
|
+
metadata: {
|
|
112
|
+
id: 'UxNjQ',
|
|
113
|
+
layerId: '606d26bd-230f-4d3e-a2a7-0c3caed71548',
|
|
114
|
+
featureType: 'marker',
|
|
115
|
+
},
|
|
116
|
+
...overrides,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('should check if subject and metadata are the same', () => {
|
|
120
|
+
const op1 = createOperation()
|
|
121
|
+
const op2 = createOperation()
|
|
122
|
+
const op3 = createOperation({
|
|
123
|
+
subject: 'datalayer',
|
|
124
|
+
metadata: { id: '606d26bd-230f-4d3e-a2a7-0c3caed71548' },
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
expect(Operations.haveSameContext(op1, op2)).to.be.true
|
|
128
|
+
expect(Operations.haveSameContext(op1, op3)).to.be.false
|
|
129
|
+
expect(Operations.haveSameContext(op2, op3)).to.be.false
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should check if the key matches if there is any provided', () => {
|
|
133
|
+
const op1 = createOperation({ key: 'properties.name' })
|
|
134
|
+
const op2 = createOperation({ key: 'properties.name' })
|
|
135
|
+
const op3 = createOperation({ key: 'geometry' })
|
|
136
|
+
const op4 = createOperation()
|
|
137
|
+
|
|
138
|
+
expect(Operations.haveSameContext(op1, op2)).to.be.true
|
|
139
|
+
expect(Operations.haveSameContext(op1, op3)).to.be.false
|
|
140
|
+
expect(Operations.haveSameContext(op1, op4)).to.be.true
|
|
141
|
+
expect(Operations.haveSameContext(op4, createOperation())).to.be.true
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should use deep equality for subject and metadata', () => {
|
|
145
|
+
const op1 = createOperation({ metadata: { nested: { value: 1 } } })
|
|
146
|
+
const op2 = createOperation({ metadata: { nested: { value: 1 } } })
|
|
147
|
+
const op3 = createOperation({ metadata: { nested: { value: 2 } } })
|
|
148
|
+
|
|
149
|
+
expect(Operations.haveSameContext(op1, op2)).to.be.true
|
|
150
|
+
expect(Operations.haveSameContext(op1, op3)).to.be.false
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('sort', () => {
|
|
155
|
+
it('should sort operations by timestamp', () => {
|
|
156
|
+
const operations = [
|
|
157
|
+
{ hlc: '1727193550:44:id1' },
|
|
158
|
+
{ hlc: '1727193549:42:id1' },
|
|
159
|
+
{ hlc: '1727193551:43:id1' },
|
|
160
|
+
]
|
|
161
|
+
const sorted = Operations.sort(operations)
|
|
162
|
+
expect(sorted).to.deep.equal([
|
|
163
|
+
{ hlc: '1727193549:42:id1' },
|
|
164
|
+
{ hlc: '1727193550:44:id1' },
|
|
165
|
+
{ hlc: '1727193551:43:id1' },
|
|
166
|
+
])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should sort operations by NN when timestamp is the same', () => {
|
|
170
|
+
const operations = [
|
|
171
|
+
{ hlc: '1727193549:42:id1' },
|
|
172
|
+
{ hlc: '1727193549:44:id1' },
|
|
173
|
+
{ hlc: '1727193549:43:id1' },
|
|
174
|
+
]
|
|
175
|
+
const sorted = Operations.sort(operations)
|
|
176
|
+
expect(sorted).to.deep.equal([
|
|
177
|
+
{ hlc: '1727193549:42:id1' },
|
|
178
|
+
{ hlc: '1727193549:43:id1' },
|
|
179
|
+
{ hlc: '1727193549:44:id1' },
|
|
180
|
+
])
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should sort operations by id if other fields are equal', () => {
|
|
184
|
+
const operations = [
|
|
185
|
+
{ hlc: '1727193549:42:id3' },
|
|
186
|
+
{ hlc: '1727193549:42:id2' },
|
|
187
|
+
{ hlc: '1727193549:42:id1' },
|
|
188
|
+
]
|
|
189
|
+
const sorted = Operations.sort(operations)
|
|
190
|
+
expect(sorted).to.deep.equal([
|
|
191
|
+
{ hlc: '1727193549:42:id1' },
|
|
192
|
+
{ hlc: '1727193549:42:id2' },
|
|
193
|
+
{ hlc: '1727193549:42:id3' },
|
|
194
|
+
])
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
describe('addLocal', () => {
|
|
199
|
+
it('should add a local operation with a new hlc', () => {
|
|
200
|
+
const ops = new Operations()
|
|
201
|
+
const inputMessage = { verb: 'update', subject: 'test' }
|
|
202
|
+
const result = ops.addLocal(inputMessage)
|
|
203
|
+
expect(result).to.have.property('hlc')
|
|
204
|
+
expect(result.hlc).to.match(/^\d+:\d+:[^:]+$/)
|
|
205
|
+
expect(result).to.include(inputMessage)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('sorted', () => {
|
|
210
|
+
it('should return sorted operations', () => {
|
|
211
|
+
const ops = new Operations()
|
|
212
|
+
ops._operations = [{ hlc: '1727193549:43:id1' }, { hlc: '1727193549:42:id1' }]
|
|
213
|
+
const sorted = ops.sorted()
|
|
214
|
+
expect(sorted[0].hlc).to.equal('1727193549:42:id1')
|
|
215
|
+
expect(sorted[1].hlc).to.equal('1727193549:43:id1')
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
describe('shouldBypassOperation', () => {
|
|
220
|
+
let ops
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
ops = new Operations()
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const createOperation = (overrides = {}) => ({
|
|
227
|
+
verb: 'update',
|
|
228
|
+
subject: 'feature',
|
|
229
|
+
metadata: {
|
|
230
|
+
id: 'UxNjQ',
|
|
231
|
+
layerId: '606d26bd-230f-4d3e-a2a7-0c3caed71548',
|
|
232
|
+
featureType: 'marker',
|
|
233
|
+
},
|
|
234
|
+
key: 'properties.name',
|
|
235
|
+
value: 'default',
|
|
236
|
+
hlc: '0000000000000:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
|
|
237
|
+
...overrides,
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const createUpsertOperation = (overrides = {}) =>
|
|
241
|
+
createOperation({
|
|
242
|
+
verb: 'upsert',
|
|
243
|
+
key: undefined,
|
|
244
|
+
value: {
|
|
245
|
+
type: 'Feature',
|
|
246
|
+
geometry: {
|
|
247
|
+
coordinates: [0.439453, 48.04871],
|
|
248
|
+
type: 'Point',
|
|
249
|
+
},
|
|
250
|
+
properties: {},
|
|
251
|
+
id: 'UxNjQ',
|
|
252
|
+
},
|
|
253
|
+
...overrides,
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('should return false if no local operation is newer', () => {
|
|
257
|
+
const remote = createUpsertOperation({ hlc: '1727184449050:44:id2' })
|
|
258
|
+
ops._operations = [
|
|
259
|
+
createOperation({
|
|
260
|
+
hlc: '1727184449010:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
|
|
261
|
+
}),
|
|
262
|
+
createUpsertOperation({
|
|
263
|
+
hlc: '1727184449020:0:b4a221a0-7b62-4588-a6af-041b041006dc',
|
|
264
|
+
}),
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
const result = ops.shouldBypassOperation(remote)
|
|
268
|
+
expect(result).to.be.false
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should return true if a similar "delete" operation is newer', () => {
|
|
272
|
+
const remote = createOperation({
|
|
273
|
+
verb: 'delete',
|
|
274
|
+
metadata: { id: 'M1NTA', layerId: '1234', featureType: 'marker' },
|
|
275
|
+
hlc: '1:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
ops._operations = [
|
|
279
|
+
createOperation({
|
|
280
|
+
verb: 'delete',
|
|
281
|
+
metadata: { id: 'M1NTA', layerId: '1234', featureType: 'marker' },
|
|
282
|
+
hlc: '2:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
|
|
283
|
+
}),
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
const result = ops.shouldBypassOperation(remote)
|
|
287
|
+
expect(result).to.be.true
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('update', () => {
|
|
291
|
+
it('should check for related updates', () => {
|
|
292
|
+
ops._operations = [
|
|
293
|
+
createOperation({
|
|
294
|
+
value: 'y',
|
|
295
|
+
hlc: '1:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
|
|
296
|
+
}),
|
|
297
|
+
createOperation({
|
|
298
|
+
value: 'youpi',
|
|
299
|
+
hlc: '9:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
|
|
300
|
+
}),
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
const remoteOperation = createOperation({
|
|
304
|
+
value: 'something else',
|
|
305
|
+
hlc: '0:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const result = ops.shouldBypassOperation(remoteOperation)
|
|
309
|
+
expect(result).to.be.true
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('should check for related deletes', () => {
|
|
313
|
+
ops._operations = [
|
|
314
|
+
{
|
|
315
|
+
verb: 'delete',
|
|
316
|
+
subject: 'feature',
|
|
317
|
+
metadata: {
|
|
318
|
+
id: 'M1NTA',
|
|
319
|
+
layerId: '123',
|
|
320
|
+
featureType: 'marker',
|
|
321
|
+
},
|
|
322
|
+
hlc: '1727196583562:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
|
|
323
|
+
key: undefined,
|
|
324
|
+
},
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
const remoteOperation = createOperation({
|
|
328
|
+
metadata: { id: 'M1NTA', layerId: '123', featureType: 'marker' },
|
|
329
|
+
key: 'geometry',
|
|
330
|
+
value: { coordinates: [2.944336, 47.070122], type: 'Point' },
|
|
331
|
+
hlc: '0:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const result = ops.shouldBypassOperation(remoteOperation)
|
|
335
|
+
expect(result).to.be.true
|
|
336
|
+
})
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
describe('upsert', () => {
|
|
340
|
+
it('should take precedence over updates (even if fresher)', () => {
|
|
341
|
+
ops._operations = [
|
|
342
|
+
createOperation({
|
|
343
|
+
value: 'youpi',
|
|
344
|
+
hlc: '1000000000000:0:f4df51cc-7617-4bd4-8bd2-599cdf17da65',
|
|
345
|
+
}),
|
|
346
|
+
]
|
|
347
|
+
|
|
348
|
+
const remoteOperation = createUpsertOperation({
|
|
349
|
+
hlc: '0000000000000:0:b4a221a0-7b62-4588-a6af-041b041006dc',
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
const result = ops.shouldBypassOperation(remoteOperation)
|
|
353
|
+
expect(result).to.be.false
|
|
354
|
+
})
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
describe('delete', () => {
|
|
358
|
+
it('should check for the same delete', () => {
|
|
359
|
+
ops._operations = [
|
|
360
|
+
createOperation({
|
|
361
|
+
verb: 'delete',
|
|
362
|
+
metadata: { id: 'I3MDg', layerId: null, featureType: 'polygon' },
|
|
363
|
+
key: undefined,
|
|
364
|
+
hlc: '1:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
|
|
365
|
+
}),
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
const remoteOperation = createOperation({
|
|
369
|
+
verb: 'delete',
|
|
370
|
+
metadata: { id: 'I3MDg', layerId: null, featureType: 'polygon' },
|
|
371
|
+
key: undefined,
|
|
372
|
+
hlc: '0:0:3f45b56f-f750-4b50-90d7-9ecce4b0cf53',
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
const result = ops.shouldBypassOperation(remoteOperation)
|
|
376
|
+
expect(result).to.be.true
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
})
|
|
380
|
+
describe('storeRemoteOperations', () => {
|
|
381
|
+
it('should store remote operations and update the local HLC', () => {
|
|
382
|
+
const ops = new Operations()
|
|
383
|
+
const remoteOps = [{ hlc: '1727193549:42:id2' }, { hlc: '1727193549:43:id2' }]
|
|
384
|
+
ops.storeRemoteOperations(remoteOps)
|
|
385
|
+
expect(ops._operations).to.deep.equal(remoteOps)
|
|
386
|
+
})
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
describe('getOperationsSince', () => {
|
|
390
|
+
it('should return operations since a given HLC', () => {
|
|
391
|
+
const ops = new Operations()
|
|
392
|
+
ops._operations = [
|
|
393
|
+
{ hlc: '1727193549:42:id1' },
|
|
394
|
+
{ hlc: '1727193549:43:id1' },
|
|
395
|
+
{ hlc: '1727193549:44:id1' },
|
|
396
|
+
]
|
|
397
|
+
const result = ops.getOperationsSince('1727193549:42:id1')
|
|
398
|
+
expect(result).to.deep.equal([
|
|
399
|
+
{ hlc: '1727193549:43:id1' },
|
|
400
|
+
{ hlc: '1727193549:44:id1' },
|
|
401
|
+
])
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
it('should return all operations if no HLC is provided', () => {
|
|
405
|
+
const ops = new Operations()
|
|
406
|
+
ops._operations = [{ hlc: '1727193549:42:id1' }, { hlc: '1727193549:43:id1' }]
|
|
407
|
+
const result = ops.getOperationsSince()
|
|
408
|
+
expect(result).to.deep.equal(ops._operations)
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
})
|
|
@@ -779,4 +779,27 @@ describe('Utils', () => {
|
|
|
779
779
|
)
|
|
780
780
|
})
|
|
781
781
|
})
|
|
782
|
+
|
|
783
|
+
describe('#isObject', () => {
|
|
784
|
+
it('should return true for objects', () => {
|
|
785
|
+
assert.equal(Utils.isObject({}), true)
|
|
786
|
+
assert.equal(Utils.isObject({ foo: 'bar' }), true)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
it('should return false for Array', () => {
|
|
790
|
+
assert.equal(Utils.isObject([]), false)
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
it('should return false on null', () => {
|
|
794
|
+
assert.equal(Utils.isObject(null), false)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('should return false on undefined', () => {
|
|
798
|
+
assert.equal(Utils.isObject(undefined), false)
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('should return false on string', () => {
|
|
802
|
+
assert.equal(Utils.isObject(''), false)
|
|
803
|
+
})
|
|
804
|
+
})
|
|
782
805
|
})
|