///|
/// One auditable graph mutation. Values are stored as JSON text so history is
/// compact and exchangeable without weakening the typed graph representation.
pub(all) struct GraphChange {
version : Int
kind : String
edge_id : Int
previous : String
current : String
reason : String
} derive(Eq, Debug, FromJson, ToJson)
///|
/// Versioned graph state used by editors, maintenance tools and simulations.
pub(all) struct GraphState {
version : Int
graph : Graph
changes : Array[GraphChange]
} derive(Eq, Debug, FromJson, ToJson)
///|
pub fn new_graph_state(graph : Graph) -> GraphState? {
if valid_graph(graph) {
Some({ version: 1, graph, changes: [] })
} else {
None
}
}
///|
pub fn graph_state_to_json(state : GraphState) -> String {
ToJson::to_json(state).stringify(indent=2)
}
///|
pub fn graph_state_from_json(source : String) -> GraphState raise {
@json.from_json(@json.parse(source))
}
///|
fn edge_index_by_id(graph : Graph, edge_id : Int) -> Int {
let mut index = 0
while index < graph.edges.length() {
if graph.edges[index].id == edge_id {
return index
}
index = index + 1
}
-1
}
///|
fn append_change(
state : GraphState,
graph : Graph,
kind : String,
edge_id : Int,
previous : String,
current : String,
reason : String,
) -> GraphState {
let next_version = state.version + 1
let changes = state.changes.copy()
changes.push({
version: next_version,
kind,
edge_id,
previous,
current,
reason,
})
{ version: next_version, graph, changes }
}
///|
/// Replace an edge while preserving its identifier and recording the change.
pub fn update_edge(
state : GraphState,
replacement : Edge,
reason : String,
) -> GraphState? {
let index = edge_index_by_id(state.graph, replacement.id)
if index < 0 {
return None
}
let edges = state.graph.edges.copy()
let previous = edges[index]
edges[index] = replacement
let graph = { nodes: state.graph.nodes.copy(), edges }
if !valid_graph(graph) {
return None
}
Some(
append_change(
state,
graph,
"update-edge",
replacement.id,
ToJson::to_json(previous).stringify(),
ToJson::to_json(replacement).stringify(),
reason,
),
)
}
///|
/// Mark or clear a construction closure without mutating an earlier version.
pub fn set_edge_construction(
state : GraphState,
edge_id : Int,
construction : Bool,
reason : String,
) -> GraphState? {
let index = edge_index_by_id(state.graph, edge_id)
if index < 0 {
return None
}
let edge = state.graph.edges[index]
update_edge(
state,
{
id: edge.id,
from: edge.from,
to: edge.to,
meters: edge.meters,
slope_percent: edge.slope_percent,
steps: edge.steps,
lighting: edge.lighting,
crossing: edge.crossing,
construction,
surface: edge.surface,
width_cm: edge.width_cm,
covered: edge.covered,
note: edge.note,
},
reason,
)
}
///|
/// Add a newly surveyed route segment and record its introduction.
pub fn add_edge(
state : GraphState,
edge : Edge,
reason : String,
) -> GraphState? {
if edge_index_by_id(state.graph, edge.id) >= 0 {
return None
}
let edges = state.graph.edges.copy()
edges.push(edge)
let graph = { nodes: state.graph.nodes.copy(), edges }
if !valid_graph(graph) {
return None
}
Some(
append_change(
state,
graph,
"add-edge",
edge.id,
"null",
ToJson::to_json(edge).stringify(),
reason,
),
)
}
///|
/// Remove a segment by identifier while keeping an audit record.
pub fn remove_edge(
state : GraphState,
edge_id : Int,
reason : String,
) -> GraphState? {
let index = edge_index_by_id(state.graph, edge_id)
if index < 0 {
return None
}
let removed = state.graph.edges[index]
let edges : Array[Edge] = []
let mut current = 0
while current < state.graph.edges.length() {
if current != index {
edges.push(state.graph.edges[current])
}
current = current + 1
}
let graph = { nodes: state.graph.nodes.copy(), edges }
Some(
append_change(
state,
graph,
"remove-edge",
edge_id,
ToJson::to_json(removed).stringify(),
"null",
reason,
),
)
}
///|
pub fn graph_change_at(state : GraphState, version : Int) -> GraphChange? {
let mut index = 0
while index < state.changes.length() {
if state.changes[index].version == version {
return Some(state.changes[index])
}
index = index + 1
}
None
}