| 1 | package edu.ucsb.cs.scaffold.services; | |
| 2 | ||
| 3 | import edu.ucsb.cs.scaffold.entity.Concept; | |
| 4 | import edu.ucsb.cs.scaffold.entity.ConceptEdge; | |
| 5 | import java.util.ArrayDeque; | |
| 6 | import java.util.ArrayList; | |
| 7 | import java.util.Comparator; | |
| 8 | import java.util.Deque; | |
| 9 | import java.util.HashMap; | |
| 10 | import java.util.HashSet; | |
| 11 | import java.util.List; | |
| 12 | import java.util.Map; | |
| 13 | import java.util.Set; | |
| 14 | import java.util.stream.Collectors; | |
| 15 | import org.springframework.stereotype.Service; | |
| 16 | ||
| 17 | /** | |
| 18 | * Graph algorithms for the prerequisite structure of a course's top-level concepts (subconcepts | |
| 19 | * have no position in this graph). Prerequisite edges only ever connect top-level concepts, so | |
| 20 | * every method here operates on that subgraph. | |
| 21 | * | |
| 22 | * <p>{@link #reset} is the analysis run by {@code POST /api/course/scaffold/reset}: it detects | |
| 23 | * cycles (flagging their edges rather than processing them further), removes edges that are | |
| 24 | * redundant given the graph's transitive structure, ranks concepts by longest path from a root, and | |
| 25 | * lays out each level's x,y position. It is a pure function of its inputs — no repository access — | |
| 26 | * so the controller owns loading input and persisting the result. | |
| 27 | */ | |
| 28 | @Service | |
| 29 | public class ConceptGraphService { | |
| 30 | ||
| 31 | // Index i holds the color for level i+1; levels beyond the palette reuse the last color. | |
| 32 | public static final List<String> LEVEL_COLORS = | |
| 33 | List.of("#c99ffe", "#feaef2", "#93ebff", "#fe9a71", "#2bcd9c"); | |
| 34 | ||
| 35 | public static final String CYCLE_EDGE_COLOR = "#FF0000"; | |
| 36 | ||
| 37 | // Initial guesses; revisit if the resulting layout looks too cramped or too sparse. These | |
| 38 | // are also the default Course.xSpacing/ySpacing values (see the Course entity and its | |
| 39 | // migration), which let each course override the spacing used by POST | |
| 40 | // /api/course/scaffold/reset via PUT /api/course/scaffold/spacing. | |
| 41 | public static final int MIN_HORIZONTAL_SEPARATION = 350; | |
| 42 | public static final int VERTICAL_LEVEL_SEPARATION = 300; | |
| 43 | ||
| 44 | /** The color assigned to a top-level concept at the given longest-path level (1-based). */ | |
| 45 | public String colorForLevel(int level) { | |
| 46 |
1
1. colorForLevel : Replaced integer subtraction with addition → KILLED |
int index = Math.min(Math.max(level, 1), LEVEL_COLORS.size()) - 1; |
| 47 |
1
1. colorForLevel : replaced return value with "" for edu/ucsb/cs/scaffold/services/ConceptGraphService::colorForLevel → KILLED |
return LEVEL_COLORS.get(index); |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * True if adding an edge sourceId -> targetId would create a cycle, i.e. targetId can already | |
| 52 | * reach sourceId via existingEdges. Used to reject new prerequisite edges at creation time, | |
| 53 | * before a cycle can ever be persisted. | |
| 54 | */ | |
| 55 | public boolean wouldCreateCycle(List<ConceptEdge> existingEdges, Long sourceId, Long targetId) { | |
| 56 | Map<Long, List<Long>> adjacency = buildAdjacency(existingEdges); | |
| 57 | Set<Long> visited = new HashSet<>(); | |
| 58 | Deque<Long> queue = new ArrayDeque<>(); | |
| 59 | visited.add(targetId); | |
| 60 | queue.add(targetId); | |
| 61 |
1
1. wouldCreateCycle : negated conditional → KILLED |
while (!queue.isEmpty()) { |
| 62 | Long current = queue.poll(); | |
| 63 |
1
1. wouldCreateCycle : negated conditional → KILLED |
if (current.equals(sourceId)) { |
| 64 |
1
1. wouldCreateCycle : replaced boolean return with false for edu/ucsb/cs/scaffold/services/ConceptGraphService::wouldCreateCycle → KILLED |
return true; |
| 65 | } | |
| 66 | for (Long next : adjacency.getOrDefault(current, List.of())) { | |
| 67 |
1
1. wouldCreateCycle : negated conditional → KILLED |
if (visited.add(next)) { |
| 68 | queue.add(next); | |
| 69 | } | |
| 70 | } | |
| 71 | } | |
| 72 |
1
1. wouldCreateCycle : replaced boolean return with true for edu/ucsb/cs/scaffold/services/ConceptGraphService::wouldCreateCycle → KILLED |
return false; |
| 73 | } | |
| 74 | ||
| 75 | public record Position(int x, int y) {} | |
| 76 | ||
| 77 | public record ResetResult( | |
| 78 | Set<Long> cycleEdgeIds, | |
| 79 | Set<Long> removedEdgeIds, | |
| 80 | Map<Long, Integer> levelByConceptId, | |
| 81 | Map<Long, Position> positionByConceptId) {} | |
| 82 | ||
| 83 | /** | |
| 84 | * Runs the full scaffold reset analysis: cycle detection, transitive reduction, longest-path | |
| 85 | * leveling, and layout. Does not mutate concepts or edges or read/write any repository; the | |
| 86 | * caller applies {@link ResetResult} to persistent entities. | |
| 87 | * | |
| 88 | * @param topLevelConcepts every top-level concept in the course (used for their id/x, to sort and | |
| 89 | * lay out concepts with no edges at all, and as the node set for the graph algorithms) | |
| 90 | * @param edges every prerequisite edge in the course | |
| 91 | */ | |
| 92 | public ResetResult reset(List<Concept> topLevelConcepts, List<ConceptEdge> edges) { | |
| 93 | Map<Long, Integer> priorXByConceptId = | |
| 94 | topLevelConcepts.stream().collect(Collectors.toMap(Concept::getId, Concept::getX)); | |
| 95 |
1
1. reset : replaced return value with null for edu/ucsb/cs/scaffold/services/ConceptGraphService::reset → KILLED |
return reset(topLevelConcepts, edges, priorXByConceptId); |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Like {@link #reset(List, List)}, but sorts each level by the given prior x values instead of | |
| 100 | * each concept's own x column. Used by the controller to sort by the requesting user's private, | |
| 101 | * unsaved drag positions where they exist, falling back to the concept's persisted x otherwise — | |
| 102 | * see {@code POST /api/course/scaffold/reset}. Lays out using the default {@link | |
| 103 | * #MIN_HORIZONTAL_SEPARATION}/{@link #VERTICAL_LEVEL_SEPARATION}; see {@link #reset(List, List, | |
| 104 | * Map, int, int)} to override the spacing. | |
| 105 | */ | |
| 106 | public ResetResult reset( | |
| 107 | List<Concept> topLevelConcepts, | |
| 108 | List<ConceptEdge> edges, | |
| 109 | Map<Long, Integer> priorXByConceptId) { | |
| 110 |
1
1. reset : replaced return value with null for edu/ucsb/cs/scaffold/services/ConceptGraphService::reset → KILLED |
return reset( |
| 111 | topLevelConcepts, | |
| 112 | edges, | |
| 113 | priorXByConceptId, | |
| 114 | MIN_HORIZONTAL_SEPARATION, | |
| 115 | VERTICAL_LEVEL_SEPARATION); | |
| 116 | } | |
| 117 | ||
| 118 | /** | |
| 119 | * Like {@link #reset(List, List, Map)}, but lays out using the given xSpacing/ySpacing instead of | |
| 120 | * the default {@link #MIN_HORIZONTAL_SEPARATION}/{@link #VERTICAL_LEVEL_SEPARATION}. Used by the | |
| 121 | * controller to honor a course's {@code xSpacing}/{@code ySpacing} settings — see {@code POST | |
| 122 | * /api/course/scaffold/reset}. | |
| 123 | */ | |
| 124 | public ResetResult reset( | |
| 125 | List<Concept> topLevelConcepts, | |
| 126 | List<ConceptEdge> edges, | |
| 127 | Map<Long, Integer> priorXByConceptId, | |
| 128 | int xSpacing, | |
| 129 | int ySpacing) { | |
| 130 | Set<Long> nodeIds = topLevelConcepts.stream().map(Concept::getId).collect(Collectors.toSet()); | |
| 131 | ||
| 132 | Map<Long, Long> sccId = computeStronglyConnectedComponents(buildAdjacency(edges), nodeIds); | |
| 133 | Map<Long, Long> sccSize = | |
| 134 |
1
1. lambda$reset$0 : replaced Long return value with 0L for edu/ucsb/cs/scaffold/services/ConceptGraphService::lambda$reset$0 → KILLED |
sccId.values().stream().collect(Collectors.groupingBy(id -> id, Collectors.counting())); |
| 135 | ||
| 136 | Set<Long> cycleEdgeIds = new HashSet<>(); | |
| 137 | List<ConceptEdge> acyclicEdges = new ArrayList<>(); | |
| 138 | for (ConceptEdge edge : edges) { | |
| 139 | Long sourceId = edge.getSource().getId(); | |
| 140 | Long targetId = edge.getTarget().getId(); | |
| 141 | boolean inCycle = | |
| 142 |
3
1. reset : negated conditional → KILLED 2. reset : changed conditional boundary → KILLED 3. reset : negated conditional → KILLED |
sccId.get(sourceId).equals(sccId.get(targetId)) && sccSize.get(sccId.get(sourceId)) > 1; |
| 143 |
1
1. reset : negated conditional → KILLED |
if (inCycle) { |
| 144 | cycleEdgeIds.add(edge.getId()); | |
| 145 | } else { | |
| 146 | acyclicEdges.add(edge); | |
| 147 | } | |
| 148 | } | |
| 149 | ||
| 150 | Set<Long> removedEdgeIds = computeTransitiveReductionRemovals(nodeIds, acyclicEdges); | |
| 151 | ||
| 152 | // The transitively redundant edges are only reported for deletion, not filtered out | |
| 153 | // before leveling: a removed edge u->v is by definition a shortcut for a longer path | |
| 154 | // u -> ... -> v, whose constraint on v's level always dominates, so including or | |
| 155 | // excluding these edges cannot change any longest-path level. | |
| 156 | Map<Long, Integer> levelByConceptId = computeLongestPathLevels(nodeIds, acyclicEdges); | |
| 157 | Map<Long, Position> positionByConceptId = | |
| 158 | computeLayout(topLevelConcepts, levelByConceptId, priorXByConceptId, xSpacing, ySpacing); | |
| 159 | ||
| 160 |
1
1. reset : replaced return value with null for edu/ucsb/cs/scaffold/services/ConceptGraphService::reset → KILLED |
return new ResetResult(cycleEdgeIds, removedEdgeIds, levelByConceptId, positionByConceptId); |
| 161 | } | |
| 162 | ||
| 163 | /** | |
| 164 | * Lays each level out left to right, sorted by each concept's prior x (then id to break ties), | |
| 165 | * centered horizontally at x=0. Each level sits {@code ySpacing} above the previous one, with | |
| 166 | * level 1 at y=0. | |
| 167 | */ | |
| 168 | private Map<Long, Position> computeLayout( | |
| 169 | List<Concept> topLevelConcepts, | |
| 170 | Map<Long, Integer> levelByConceptId, | |
| 171 | Map<Long, Integer> priorXByConceptId, | |
| 172 | int xSpacing, | |
| 173 | int ySpacing) { | |
| 174 | Map<Integer, List<Concept>> byLevel = | |
| 175 | topLevelConcepts.stream() | |
| 176 |
1
1. lambda$computeLayout$1 : replaced Integer return value with 0 for edu/ucsb/cs/scaffold/services/ConceptGraphService::lambda$computeLayout$1 → KILLED |
.collect(Collectors.groupingBy(c -> levelByConceptId.get(c.getId()))); |
| 177 | ||
| 178 | Map<Long, Position> positions = new HashMap<>(); | |
| 179 | for (Map.Entry<Integer, List<Concept>> entry : byLevel.entrySet()) { | |
| 180 | int level = entry.getKey(); | |
| 181 | List<Concept> sorted = | |
| 182 | entry.getValue().stream() | |
| 183 | .sorted( | |
| 184 |
1
1. lambda$computeLayout$2 : replaced Integer return value with 0 for edu/ucsb/cs/scaffold/services/ConceptGraphService::lambda$computeLayout$2 → KILLED |
Comparator.comparing((Concept c) -> priorXByConceptId.get(c.getId())) |
| 185 | .thenComparing(Concept::getId)) | |
| 186 | .toList(); | |
| 187 | int n = sorted.size(); | |
| 188 |
3
1. computeLayout : Replaced integer subtraction with addition → KILLED 2. computeLayout : removed negation → KILLED 3. computeLayout : Replaced integer multiplication with division → KILLED |
int y = -(level - 1) * ySpacing; |
| 189 |
2
1. computeLayout : changed conditional boundary → KILLED 2. computeLayout : negated conditional → KILLED |
for (int i = 0; i < n; i++) { |
| 190 |
4
1. computeLayout : Replaced double multiplication with division → KILLED 2. computeLayout : Replaced double division with multiplication → KILLED 3. computeLayout : Replaced double subtraction with addition → KILLED 4. computeLayout : Replaced integer subtraction with addition → KILLED |
int x = (int) Math.round((i - (n - 1) / 2.0) * xSpacing); |
| 191 | positions.put(sorted.get(i).getId(), new Position(x, y)); | |
| 192 | } | |
| 193 | } | |
| 194 |
1
1. computeLayout : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/services/ConceptGraphService::computeLayout → KILLED |
return positions; |
| 195 | } | |
| 196 | ||
| 197 | /** | |
| 198 | * Tarjan's algorithm: maps each node id to an id shared by its strongly connected component (a | |
| 199 | * nontrivial cycle iff more than one node shares it). The component id is the id of the | |
| 200 | * component's root node — any value unique per component would do, and using the root avoids | |
| 201 | * maintaining a separate counter. | |
| 202 | */ | |
| 203 | private Map<Long, Long> computeStronglyConnectedComponents( | |
| 204 | Map<Long, List<Long>> adjacency, Set<Long> nodeIds) { | |
| 205 | TarjanState state = new TarjanState(adjacency); | |
| 206 | for (Long node : nodeIds) { | |
| 207 |
1
1. computeStronglyConnectedComponents : negated conditional → KILLED |
if (!state.index.containsKey(node)) { |
| 208 |
1
1. computeStronglyConnectedComponents : removed call to edu/ucsb/cs/scaffold/services/ConceptGraphService$TarjanState::strongConnect → KILLED |
state.strongConnect(node); |
| 209 | } | |
| 210 | } | |
| 211 |
1
1. computeStronglyConnectedComponents : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/services/ConceptGraphService::computeStronglyConnectedComponents → KILLED |
return state.sccId; |
| 212 | } | |
| 213 | ||
| 214 | private static final class TarjanState { | |
| 215 | private final Map<Long, List<Long>> adjacency; | |
| 216 | private final Map<Long, Integer> index = new HashMap<>(); | |
| 217 | private final Map<Long, Integer> lowlink = new HashMap<>(); | |
| 218 | private final Set<Long> onStack = new HashSet<>(); | |
| 219 | private final Deque<Long> stack = new ArrayDeque<>(); | |
| 220 | private final Map<Long, Long> sccId = new HashMap<>(); | |
| 221 | private int counter = 0; | |
| 222 | ||
| 223 | TarjanState(Map<Long, List<Long>> adjacency) { | |
| 224 | this.adjacency = adjacency; | |
| 225 | } | |
| 226 | ||
| 227 | void strongConnect(Long v) { | |
| 228 | index.put(v, counter); | |
| 229 | lowlink.put(v, counter); | |
| 230 |
1
1. strongConnect : Replaced integer addition with subtraction → KILLED |
counter++; |
| 231 |
1
1. strongConnect : removed call to java/util/Deque::push → KILLED |
stack.push(v); |
| 232 | onStack.add(v); | |
| 233 | ||
| 234 | for (Long w : adjacency.getOrDefault(v, List.of())) { | |
| 235 |
1
1. strongConnect : negated conditional → KILLED |
if (!index.containsKey(w)) { |
| 236 |
1
1. strongConnect : removed call to edu/ucsb/cs/scaffold/services/ConceptGraphService$TarjanState::strongConnect → KILLED |
strongConnect(w); |
| 237 | lowlink.put(v, Math.min(lowlink.get(v), lowlink.get(w))); | |
| 238 |
1
1. strongConnect : negated conditional → KILLED |
} else if (onStack.contains(w)) { |
| 239 | lowlink.put(v, Math.min(lowlink.get(v), index.get(w))); | |
| 240 | } | |
| 241 | } | |
| 242 | ||
| 243 |
1
1. strongConnect : negated conditional → KILLED |
if (lowlink.get(v).equals(index.get(v))) { |
| 244 | Long w; | |
| 245 | do { | |
| 246 | w = stack.pop(); | |
| 247 | onStack.remove(w); | |
| 248 | // v is this component's root: every member gets tagged with its id. | |
| 249 | sccId.put(w, v); | |
| 250 |
1
1. strongConnect : negated conditional → KILLED |
} while (!w.equals(v)); |
| 251 | } | |
| 252 | } | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * An edge u->v is redundant if some other direct successor w of u can also reach v; such an | |
| 257 | * edge is a "shortcut" whose removal does not change reachability. Requires an acyclic edge set. | |
| 258 | */ | |
| 259 | private Set<Long> computeTransitiveReductionRemovals( | |
| 260 | Set<Long> nodeIds, List<ConceptEdge> acyclicEdges) { | |
| 261 | Map<Long, List<Long>> adjacency = buildAdjacency(acyclicEdges); | |
| 262 | Map<Long, Set<Long>> reachable = new HashMap<>(); | |
| 263 | for (Long node : nodeIds) { | |
| 264 | reachable.put(node, bfsReachable(node, adjacency)); | |
| 265 | } | |
| 266 | ||
| 267 | Set<Long> removed = new HashSet<>(); | |
| 268 | for (ConceptEdge edge : acyclicEdges) { | |
| 269 | Long u = edge.getSource().getId(); | |
| 270 | Long v = edge.getTarget().getId(); | |
| 271 | boolean redundant = | |
| 272 | adjacency.getOrDefault(u, List.of()).stream() | |
| 273 |
3
1. lambda$computeTransitiveReductionRemovals$3 : replaced boolean return with true for edu/ucsb/cs/scaffold/services/ConceptGraphService::lambda$computeTransitiveReductionRemovals$3 → KILLED 2. lambda$computeTransitiveReductionRemovals$3 : negated conditional → KILLED 3. lambda$computeTransitiveReductionRemovals$3 : negated conditional → KILLED |
.anyMatch(w -> !w.equals(v) && reachable.getOrDefault(w, Set.of()).contains(v)); |
| 274 |
1
1. computeTransitiveReductionRemovals : negated conditional → KILLED |
if (redundant) { |
| 275 | removed.add(edge.getId()); | |
| 276 | } | |
| 277 | } | |
| 278 |
1
1. computeTransitiveReductionRemovals : replaced return value with Collections.emptySet for edu/ucsb/cs/scaffold/services/ConceptGraphService::computeTransitiveReductionRemovals → KILLED |
return removed; |
| 279 | } | |
| 280 | ||
| 281 | /** | |
| 282 | * Longest path from any root (no incoming edge) to each node; roots are level 1. Requires an | |
| 283 | * acyclic edge set. | |
| 284 | */ | |
| 285 | private Map<Long, Integer> computeLongestPathLevels(Set<Long> nodeIds, List<ConceptEdge> edges) { | |
| 286 | Map<Long, List<Long>> outNeighbors = buildAdjacency(edges); | |
| 287 | Map<Long, Integer> inDegree = new HashMap<>(); | |
| 288 | for (Long node : nodeIds) { | |
| 289 | inDegree.put(node, 0); | |
| 290 | } | |
| 291 | for (ConceptEdge edge : edges) { | |
| 292 | inDegree.merge(edge.getTarget().getId(), 1, Integer::sum); | |
| 293 | } | |
| 294 | ||
| 295 | Map<Long, Integer> level = new HashMap<>(); | |
| 296 | Deque<Long> queue = new ArrayDeque<>(); | |
| 297 | for (Long node : nodeIds) { | |
| 298 | level.put(node, 1); | |
| 299 |
1
1. computeLongestPathLevels : negated conditional → KILLED |
if (inDegree.get(node) == 0) { |
| 300 | queue.add(node); | |
| 301 | } | |
| 302 | } | |
| 303 | ||
| 304 |
1
1. computeLongestPathLevels : negated conditional → TIMED_OUT |
while (!queue.isEmpty()) { |
| 305 | Long u = queue.poll(); | |
| 306 | for (Long v : outNeighbors.getOrDefault(u, List.of())) { | |
| 307 |
1
1. computeLongestPathLevels : Replaced integer addition with subtraction → KILLED |
level.put(v, Math.max(level.get(v), level.get(u) + 1)); |
| 308 |
1
1. computeLongestPathLevels : negated conditional → KILLED |
if (inDegree.merge(v, -1, Integer::sum) == 0) { |
| 309 | queue.add(v); | |
| 310 | } | |
| 311 | } | |
| 312 | } | |
| 313 |
1
1. computeLongestPathLevels : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/services/ConceptGraphService::computeLongestPathLevels → KILLED |
return level; |
| 314 | } | |
| 315 | ||
| 316 | private Set<Long> bfsReachable(Long start, Map<Long, List<Long>> adjacency) { | |
| 317 | Set<Long> visited = new HashSet<>(adjacency.getOrDefault(start, List.of())); | |
| 318 | Deque<Long> queue = new ArrayDeque<>(visited); | |
| 319 |
1
1. bfsReachable : negated conditional → TIMED_OUT |
while (!queue.isEmpty()) { |
| 320 | Long current = queue.poll(); | |
| 321 | for (Long next : adjacency.getOrDefault(current, List.of())) { | |
| 322 |
1
1. bfsReachable : negated conditional → TIMED_OUT |
if (visited.add(next)) { |
| 323 | queue.add(next); | |
| 324 | } | |
| 325 | } | |
| 326 | } | |
| 327 |
1
1. bfsReachable : replaced return value with Collections.emptySet for edu/ucsb/cs/scaffold/services/ConceptGraphService::bfsReachable → KILLED |
return visited; |
| 328 | } | |
| 329 | ||
| 330 | private Map<Long, List<Long>> buildAdjacency(List<ConceptEdge> edges) { | |
| 331 | Map<Long, List<Long>> adjacency = new HashMap<>(); | |
| 332 | for (ConceptEdge edge : edges) { | |
| 333 | adjacency | |
| 334 |
1
1. lambda$buildAdjacency$4 : replaced return value with Collections.emptyList for edu/ucsb/cs/scaffold/services/ConceptGraphService::lambda$buildAdjacency$4 → KILLED |
.computeIfAbsent(edge.getSource().getId(), k -> new ArrayList<>()) |
| 335 | .add(edge.getTarget().getId()); | |
| 336 | } | |
| 337 |
1
1. buildAdjacency : replaced return value with Collections.emptyMap for edu/ucsb/cs/scaffold/services/ConceptGraphService::buildAdjacency → KILLED |
return adjacency; |
| 338 | } | |
| 339 | } | |
Mutations | ||
| 46 |
1.1 |
|
| 47 |
1.1 |
|
| 61 |
1.1 |
|
| 63 |
1.1 |
|
| 64 |
1.1 |
|
| 67 |
1.1 |
|
| 72 |
1.1 |
|
| 95 |
1.1 |
|
| 110 |
1.1 |
|
| 134 |
1.1 |
|
| 142 |
1.1 2.2 3.3 |
|
| 143 |
1.1 |
|
| 160 |
1.1 |
|
| 176 |
1.1 |
|
| 184 |
1.1 |
|
| 188 |
1.1 2.2 3.3 |
|
| 189 |
1.1 2.2 |
|
| 190 |
1.1 2.2 3.3 4.4 |
|
| 194 |
1.1 |
|
| 207 |
1.1 |
|
| 208 |
1.1 |
|
| 211 |
1.1 |
|
| 230 |
1.1 |
|
| 231 |
1.1 |
|
| 235 |
1.1 |
|
| 236 |
1.1 |
|
| 238 |
1.1 |
|
| 243 |
1.1 |
|
| 250 |
1.1 |
|
| 273 |
1.1 2.2 3.3 |
|
| 274 |
1.1 |
|
| 278 |
1.1 |
|
| 299 |
1.1 |
|
| 304 |
1.1 |
|
| 307 |
1.1 |
|
| 308 |
1.1 |
|
| 313 |
1.1 |
|
| 319 |
1.1 |
|
| 322 |
1.1 |
|
| 327 |
1.1 |
|
| 334 |
1.1 |
|
| 337 |
1.1 |