1- namespace Alidade . Osm . Services ;
2-
3- /// <summary>
4- /// Splits a dirty edit buffer into one or more <see cref="OsmChange"/> objects that
5- /// each fit within the OSM API element limit. The algorithm proceeds in six steps:
6- /// <list type="number">
7- /// <item>Collect all dirty elements (created, modified, or deleted).</item>
8- /// <item>
9- /// Build a dependency graph: ways depend on their member nodes; relations depend
10- /// on all their members.
11- /// </item>
12- /// <item>Assign each element a spatial cluster via its zoom-14 slippy tile centroid.</item>
13- /// <item>
14- /// Merge clusters whose elements are co-required by a relation using a union-find
15- /// structure, so that relations and all their members land in the same changeset.
16- /// </item>
17- /// <item>
18- /// Pack merged clusters into changesets, respecting
19- /// <c>maxElementsPerChangeset</c> (default 10,000).
20- /// </item>
21- /// <item>
22- /// Within each changeset, produce elements in topological order:
23- /// nodes → ways → relations.
24- /// </item>
25- /// </list>
26- /// </summary>
27- public static class ChangesetSplitter
1+ namespace Alidade . Osm . Handlers . Changeset ;
2+
3+ /// <inheritdoc />
4+ public sealed class BuildChangesets : IRequestHandler < BuildChangesets . Query , QueryResult < IReadOnlyList < OsmChange > > >
285{
296 /// <summary>
30- /// Splits the dirty elements in <paramref name="buffer "/> into as many
7+ /// Splits the dirty elements in <paramref name="Buffer "/> into as many
318 /// <see cref="OsmChange"/> objects as needed so that each contains at most
32- /// <paramref name="maxElementsPerChangeset"/> elements.
9+ /// <paramref name="MaxElementsPerChangeset"/> elements. The algorithm proceeds in six steps:
10+ /// <list type="number">
11+ /// <item>Collect all dirty elements (created, modified, or deleted).</item>
12+ /// <item>
13+ /// Build a dependency graph: ways depend on their member nodes; relations depend
14+ /// on all their members.
15+ /// </item>
16+ /// <item>Assign each element a spatial cluster via its zoom-14 slippy tile centroid.</item>
17+ /// <item>
18+ /// Merge clusters whose elements are co-required by a relation using a union-find
19+ /// structure, so that relations and all their members land in the same changeset.
20+ /// </item>
21+ /// <item>
22+ /// Pack merged clusters into changesets, respecting
23+ /// <paramref name="MaxElementsPerChangeset"/> (default 10,000).
24+ /// </item>
25+ /// <item>
26+ /// Within each changeset, produce elements in topological order:
27+ /// nodes → ways → relations.
28+ /// </item>
29+ /// </list>
3330 /// </summary>
34- /// <param name="buffer ">The current edit buffer state.</param>
35- /// <param name="maxElementsPerChangeset ">
31+ /// <param name="Buffer ">The current edit buffer state.</param>
32+ /// <param name="MaxElementsPerChangeset ">
3633 /// Maximum number of elements per changeset. Defaults to 10,000 (the OSM API limit).
3734 /// </param>
38- /// <returns>
39- /// An ordered list of <see cref="OsmChange"/> objects ready for sequential upload,
40- /// or an empty list when the buffer has no dirty elements.
41- /// </returns>
42- public static IReadOnlyList < OsmChange > Split (
43- EditBufferState buffer ,
44- int maxElementsPerChangeset = 10_000 )
35+ public record Query (
36+ EditBufferState Buffer ,
37+ int MaxElementsPerChangeset = 10_000 ) : IRequest < QueryResult < IReadOnlyList < OsmChange > > > ;
38+
39+ /// <inheritdoc />
40+ public Task < QueryResult < IReadOnlyList < OsmChange > > > Handle ( Query request , CancellationToken cancellationToken )
4541 {
42+ EditBufferState buffer = request . Buffer ;
43+ int maxElementsPerChangeset = request . MaxElementsPerChangeset ;
44+
4645 // Collect dirty elements
4746 List < OsmNode > dirtyNodes = [ .. buffer . Nodes . Values . Where ( n => buffer . EditStates . TryGetValue ( n . Ref , out EditState s ) && s != EditState . Fetched ) ] ;
4847 List < OsmWay > dirtyWays = [ .. buffer . Ways . Values . Where ( w => buffer . EditStates . TryGetValue ( w . Ref , out EditState s ) && s != EditState . Fetched ) ] ;
4948 List < OsmRelation > dirtyRelations = [ .. buffer . Relations . Values . Where ( r => buffer . EditStates . TryGetValue ( r . Ref , out EditState s ) && s != EditState . Fetched ) ] ;
5049
5150 if ( dirtyNodes . Count == 0 && dirtyWays . Count == 0 && dirtyRelations . Count == 0 )
5251 {
53- return [ ] ;
52+ return Task . FromResult ( QueryResult < IReadOnlyList < OsmChange > > . Pass ( [ ] ) ) ;
5453 }
5554
5655 // Assign slippy tile cluster IDs (zoom 14)
@@ -129,23 +128,24 @@ public static IReadOnlyList<OsmChange> Split(
129128 }
130129
131130 // Build OsmChange per changeset (topological order)
132- return [ .. changeSets . Select ( refs => BuildChange ( refs , buffer ) ) ] ;
131+ IReadOnlyList < OsmChange > result = [ .. changeSets . Select ( refs => BuildChange ( refs , buffer ) ) ] ;
132+ return Task . FromResult ( QueryResult < IReadOnlyList < OsmChange > > . Pass ( result ) ) ;
133133 }
134134
135135 #region Helpers
136136 private static OsmChange BuildChange ( List < OsmElementRef > refs , EditBufferState buffer )
137137 {
138138 List < OsmNode > createdNodes = [ ] ;
139139 List < OsmNode > modifiedNodes = [ ] ;
140- List < long > deletedNodeIds = [ ] ;
140+ Dictionary < long , int > deletedNodeVersions = [ ] ;
141141
142142 List < OsmWay > createdWays = [ ] ;
143143 List < OsmWay > modifiedWays = [ ] ;
144- List < long > deletedWayIds = [ ] ;
144+ Dictionary < long , int > deletedWayVersions = [ ] ;
145145
146146 List < OsmRelation > createdRelations = [ ] ;
147147 List < OsmRelation > modifiedRelations = [ ] ;
148- List < long > deletedRelationIds = [ ] ;
148+ Dictionary < long , int > deletedRelationVersions = [ ] ;
149149
150150 foreach ( OsmElementRef r in refs )
151151 {
@@ -168,7 +168,7 @@ private static OsmChange BuildChange(List<OsmElementRef> refs, EditBufferState b
168168 }
169169 else if ( state == EditState . Deleted )
170170 {
171- deletedNodeIds . Add ( r . Id ) ;
171+ deletedNodeVersions [ r . Id ] = node . Version ;
172172 }
173173
174174 break ;
@@ -189,7 +189,7 @@ private static OsmChange BuildChange(List<OsmElementRef> refs, EditBufferState b
189189 }
190190 else if ( state == EditState . Deleted )
191191 {
192- deletedWayIds . Add ( r . Id ) ;
192+ deletedWayVersions [ r . Id ] = way . Version ;
193193 }
194194
195195 break ;
@@ -210,7 +210,7 @@ private static OsmChange BuildChange(List<OsmElementRef> refs, EditBufferState b
210210 }
211211 else if ( state == EditState . Deleted )
212212 {
213- deletedRelationIds . Add ( r . Id ) ;
213+ deletedRelationVersions [ r . Id ] = rel . Version ;
214214 }
215215
216216 break ;
@@ -220,7 +220,9 @@ private static OsmChange BuildChange(List<OsmElementRef> refs, EditBufferState b
220220 return new OsmChange (
221221 createdNodes , createdWays , createdRelations ,
222222 modifiedNodes , modifiedWays , modifiedRelations ,
223- deletedNodeIds , deletedWayIds , deletedRelationIds ) ;
223+ deletedNodeVersions . ToImmutableDictionary ( ) ,
224+ deletedWayVersions . ToImmutableDictionary ( ) ,
225+ deletedRelationVersions . ToImmutableDictionary ( ) ) ;
224226 }
225227
226228 private static ( int X , int Y ) LatLonToTile ( double lat , double lon , int zoom )
@@ -289,18 +291,10 @@ private static (int X, int Y) CentroidTileForRelation(
289291
290292 #endregion
291293
292- /// <summary>
293- /// Simple union-find over arbitrary keys
294- /// </summary>
295- /// <typeparam name="T"></typeparam>
296294 private sealed class UnionFind < T > where T : notnull
297295 {
298296 private readonly Dictionary < T , T > _parent = [ ] ;
299297
300- /// <summary>
301- /// Initializes the union-find with one singleton set per item.
302- /// </summary>
303- /// <param name="items">The initial set of elements.</param>
304298 public UnionFind ( IEnumerable < T > items )
305299 {
306300 foreach ( T i in items )
@@ -309,12 +303,6 @@ public UnionFind(IEnumerable<T> items)
309303 }
310304 }
311305
312- /// <summary>
313- /// Returns the canonical representative of the set containing <paramref name="x"/>,
314- /// applying path compression.
315- /// </summary>
316- /// <param name="x">The element to find.</param>
317- /// <returns>The root representative of the set.</returns>
318306 public T Find ( T x )
319307 {
320308 if ( ! _parent . TryGetValue ( x , out T ? p ) )
@@ -331,11 +319,6 @@ public T Find(T x)
331319 return _parent [ x ] ;
332320 }
333321
334- /// <summary>
335- /// Merges the sets containing <paramref name="a"/> and <paramref name="b"/>.
336- /// </summary>
337- /// <param name="a">The first element.</param>
338- /// <param name="b">The second element.</param>
339322 public void Union ( T a , T b )
340323 {
341324 T ra = Find ( a ) ;
0 commit comments