Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Commit 450b4d6

Browse files
Changeset refactor
1 parent 7e260a9 commit 450b4d6

14 files changed

Lines changed: 221 additions & 123 deletions

File tree

Alidade.Osm/Services/ChangesetSplitter.cs renamed to Alidade.Osm/Handlers/Changeset/BuildChangesets.cs

Lines changed: 49 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,55 @@
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);

Alidade.Osm/Handlers/Editing/CloseChangeset.cs renamed to Alidade.Osm/Handlers/Changeset/CloseChangeset.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Alidade.Osm.Handlers.Editing;
1+
namespace Alidade.Osm.Handlers.Changeset;
22

33
/// <inheritdoc />
44
public class CloseChangeset(IOsmEditingService osm) : IRequestHandler<CloseChangeset.Command, CommandResult>

Alidade.Osm/Handlers/Editing/CreateChangeset.cs renamed to Alidade.Osm/Handlers/Changeset/CreateChangeset.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Alidade.Osm.Handlers.Editing;
1+
namespace Alidade.Osm.Handlers.Changeset;
22

33
/// <inheritdoc />
44
public class CreateChangeset(IOsmEditingService osm) : IRequestHandler<CreateChangeset.Query, QueryResult<int>>

Alidade.Osm/Handlers/Editing/UploadChangeset.cs renamed to Alidade.Osm/Handlers/Changeset/UploadChangeset.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using Alidade.Osm.Handlers.Parsing;
22

3-
namespace Alidade.Osm.Handlers.Editing;
3+
namespace Alidade.Osm.Handlers.Changeset;
44

55
/// <inheritdoc />
66
public class UploadChangeset(IOsmEditingService osm, ISender sender) : IRequestHandler<UploadChangeset.Query, QueryResult<DiffResult>>

Alidade.Osm/Handlers/Parsing/BuildOsmChangeXml.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,14 @@ static XElement RelEl(OsmRelation r, int id)
5757
change.ModifiedWays.Select(w => WayEl(w, csId)),
5858
change.ModifiedRelations.Select(r => RelEl(r, csId))),
5959
new XElement("delete",
60-
change.DeletedNodeIds.Select(id => new XElement("node",
61-
new XAttribute("id", id), new XAttribute("version", "1"),
60+
change.DeletedRelationVersions.Select(kv => new XElement("relation",
61+
new XAttribute("id", kv.Key), new XAttribute("version", kv.Value),
6262
new XAttribute("changeset", csId))),
63-
change.DeletedWayIds.Select(id => new XElement("way",
64-
new XAttribute("id", id), new XAttribute("version", "1"),
63+
change.DeletedWayVersions.Select(kv => new XElement("way",
64+
new XAttribute("id", kv.Key), new XAttribute("version", kv.Value),
65+
new XAttribute("changeset", csId))),
66+
change.DeletedNodeVersions.Select(kv => new XElement("node",
67+
new XAttribute("id", kv.Key), new XAttribute("version", kv.Value),
6568
new XAttribute("changeset", csId)))));
6669

6770
string xml = new XDocument(doc).ToString();

Alidade.Osm/Models/EditBufferSnapshot.cs renamed to Alidade.Osm/Models/EditBuffer/EditBufferSnapshot.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Alidade.Osm.Models;
1+
namespace Alidade.Osm.Models.EditBuffer;
22

33
/// <summary>
44
/// A lightweight, immutable snapshot of the edit buffer taken synchronously on the

Alidade.Osm/Models/EditBuffer/EditBufferState.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ public EditBufferState(ImmutableDictionary<long, OsmNode> Nodes, ImmutableDictio
5959
/// </summary>
6060
public required long NextNegativeId { get; init; }
6161

62+
/// <summary>
63+
/// Imagery sources used during this edit session, appended as the user switches layers.
64+
/// Persisted in the draft so that the tag survives a page refresh.
65+
/// </summary>
66+
public ImmutableList<string> ImageryUsed { get; init; } = [];
67+
6268
/// <summary>
6369
/// Returns <see langword="true"/> when any element has been created, modified, or deleted.
6470
/// </summary>

Alidade.Osm/Models/OsmChange.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,26 @@ namespace Alidade.Osm.Models;
33
/// <summary>
44
/// An osmChange document grouping creates, modifies, and deletes to be uploaded in a single
55
/// changeset. Corresponds to the <c>&lt;osmChange&gt;</c> XML element in the OSM API v0.6 format.
6-
/// Elements must be ordered so that dependencies are satisfied: nodes before ways, ways before
7-
/// relations.
6+
/// Creates and modifies must be ordered nodes → ways → relations so that dependencies are
7+
/// satisfied. Deletes must use the reverse order (relations → ways → nodes) so that members
8+
/// are freed before their containers are removed.
89
/// </summary>
910
/// <param name="CreatedNodes">Nodes to be created; must have negative placeholder IDs.</param>
1011
/// <param name="CreatedWays">Ways to be created; must have negative placeholder IDs.</param>
1112
/// <param name="CreatedRelations">Relations to be created; must have negative placeholder IDs.</param>
1213
/// <param name="ModifiedNodes">Nodes that were fetched from the server and have been edited locally.</param>
1314
/// <param name="ModifiedWays">Ways that were fetched from the server and have been edited locally.</param>
1415
/// <param name="ModifiedRelations">Relations that were fetched from the server and have been edited locally.</param>
15-
/// <param name="DeletedNodeIds">IDs of nodes to delete.</param>
16-
/// <param name="DeletedWayIds">IDs of ways to delete.</param>
17-
/// <param name="DeletedRelationIds">IDs of relations to delete.</param>
16+
/// <param name="DeletedNodeVersions">IDs and server versions of nodes to delete.</param>
17+
/// <param name="DeletedWayVersions">IDs and server versions of ways to delete.</param>
18+
/// <param name="DeletedRelationVersions">IDs and server versions of relations to delete.</param>
1819
public record OsmChange(
1920
IReadOnlyList<OsmNode> CreatedNodes,
2021
IReadOnlyList<OsmWay> CreatedWays,
2122
IReadOnlyList<OsmRelation> CreatedRelations,
2223
IReadOnlyList<OsmNode> ModifiedNodes,
2324
IReadOnlyList<OsmWay> ModifiedWays,
2425
IReadOnlyList<OsmRelation> ModifiedRelations,
25-
IReadOnlyList<long> DeletedNodeIds,
26-
IReadOnlyList<long> DeletedWayIds,
27-
IReadOnlyList<long> DeletedRelationIds);
26+
ImmutableDictionary<long, int> DeletedNodeVersions,
27+
ImmutableDictionary<long, int> DeletedWayVersions,
28+
ImmutableDictionary<long, int> DeletedRelationVersions);

0 commit comments

Comments
 (0)