]> gerrit.simantics Code Review - simantics/district.git/blob - org.simantics.district.selection/src/org/simantics/district/selection/ElementSelector.java
Bug fixes to element selection query processing
[simantics/district.git] / org.simantics.district.selection / src / org / simantics / district / selection / ElementSelector.java
1 package org.simantics.district.selection;
2
3 import java.awt.geom.Path2D;
4 import java.util.ArrayList;
5 import java.util.Collection;
6 import java.util.HashSet;
7 import java.util.List;
8 import java.util.Set;
9
10 import org.simantics.Simantics;
11 import org.simantics.db.ReadGraph;
12 import org.simantics.db.Resource;
13 import org.simantics.db.common.request.ResourceRead;
14 import org.simantics.db.common.utils.ListUtils;
15 import org.simantics.db.exception.DatabaseException;
16 import org.simantics.db.layer0.request.ActiveModels;
17 import org.simantics.db.layer0.request.ActiveRuns;
18 import org.simantics.db.layer0.request.Configuration;
19 import org.simantics.db.layer0.variable.RVI;
20 import org.simantics.db.layer0.variable.Variable;
21 import org.simantics.db.layer0.variable.Variables;
22 import org.simantics.diagram.stubs.DiagramResource;
23 import org.simantics.district.network.ontology.DistrictNetworkResource;
24 import org.simantics.district.region.ontology.DiagramRegionsResource;
25 import org.simantics.district.selection.ElementSelector.AggregateCondition.Type;
26 import org.simantics.layer0.Layer0;
27 import org.simantics.modeling.ModelingResources;
28 import org.simantics.scl.runtime.Lists;
29 import org.simantics.scl.runtime.function.FunctionImpl1;
30 import org.simantics.scl.runtime.tuple.Tuple2;
31 import org.simantics.structural.stubs.StructuralResource2;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 public class ElementSelector {
36         String name;
37         String expression;
38         Integer count;
39         Resource resource;
40         
41         Generator generator;
42         Selector selector;
43         Condition condition;
44         
45         static Logger LOG = LoggerFactory.getLogger(ElementSelector.class);
46         
47         static ElementSelectionResource ES;
48         static Layer0 L0;
49         static StructuralResource2 STR;
50         static ModelingResources MOD;
51         static DiagramResource DIA;
52         static DistrictNetworkResource DN;
53         
54         ElementSelector(ReadGraph graph, Resource resource) throws DatabaseException {
55                 super();
56                 
57                 L0 = Layer0.getInstance(graph);
58                 ES = ElementSelectionResource.getInstance(graph);
59                 STR = StructuralResource2.getInstance(graph);
60                 MOD = ModelingResources.getInstance(graph);
61                 DIA = DiagramResource.getInstance(graph);
62                 DN = DistrictNetworkResource.getInstance(graph);
63                 
64                 this.resource = resource;
65                 this.count = -1;
66                 try {
67                         this.name = graph.getRelatedValue(resource, L0.HasLabel);
68                         this.expression = buildExpression(graph, resource);
69                 } catch (DatabaseException e) {
70                         LOG.error("Error reading element selector", e);
71                         throw e;
72                 }
73         }
74         
75         public static ElementSelector getSelector(ReadGraph graph, Resource resource) throws DatabaseException {
76                 return graph.syncRequest(new ElementSelectorQuery(resource));
77         }
78
79         public String getName() {
80                 return name;
81         }
82
83         public String getExpression() {
84                 return expression;
85         }
86
87         public Resource getResource() {
88                 return resource;
89         }
90
91         public List<Resource> selectElementsFrom(ReadGraph graph, Resource model) throws DatabaseException {
92                 if (selector == null) {
93                         buildSelection(graph);
94                 }
95                 
96                 Collection<Resource> result = gather(graph, model);
97                 return result instanceof List ? (List<Resource>) result : new ArrayList<>(result);
98         }
99
100         private static Generator buildGenerator(ReadGraph graph, Resource resource) throws DatabaseException {
101                 if (!graph.isInstanceOf(resource, ES.Generator))
102                         throw new IllegalArgumentException("Resource " + resource + " is not a valid generator");
103                 
104                 if (graph.isInstanceOf(resource, ES.Generator_Model)) {
105                         return new ModelGenerator();
106                 }
107                 else if (graph.isInstanceOf(resource, ES.Generator_Diagram)) {
108                         return new DiagramGenerator(graph.getSingleObject(resource, ES.Generator_HasDiagram));
109                 }
110                 else if (graph.isInstanceOf(resource, ES.Generator_Explicit)) {
111                         return new ExplicitGenerator(graph.getObjects(resource, ES.Generator_HasSelectedElement));
112                 }
113                 else {
114                         throw new IllegalArgumentException("Unknown generator type " + graph.getURI(graph.getSingleType(resource)));
115                 }
116         }
117
118         private static Selector buildSelector(ReadGraph graph, Resource resource) throws DatabaseException {
119                 if (!graph.isInstanceOf(resource, ES.Selector))
120                         throw new IllegalArgumentException("Resource " + resource + " is not a valid selector");
121                 
122                 Selector s;
123                 if (graph.isInstanceOf(resource, ES.Selector_All)) {
124                         s = new All();
125                 }
126                 else if (graph.isInstanceOf(resource, ES.PropertySelector)) {
127                         String propertyName = graph.getRelatedValue(resource, ES.PropertySelector_HasSelectionPropertyName);
128                         Integer resultCount = graph.getRelatedValue(resource, ES.PropertySelector_HasResultCount);
129                         boolean isSmallest = graph.isInstanceOf(resource, ES.Selector_NLowest);
130                         s = new PropertySelector(isSmallest, propertyName, resultCount);
131                 }
132                 else {
133                         throw new IllegalArgumentException("Unknown selector type " + graph.getURI(graph.getSingleType(resource)));
134                 }
135                 return s;
136         }
137
138         private static Condition buildCondition(ReadGraph graph, Resource resource) throws DatabaseException {
139                 if (resource == null)
140                         return null;
141                 
142                 if (!graph.isInstanceOf(resource, ES.Condition))
143                         throw new IllegalArgumentException("Resource " + resource + " is not a valid condition");
144                 
145                 Condition cond;
146                 if (graph.isInstanceOf(resource, ES.PropertyCondition)) {
147                         String propertyName = graph.getRelatedValue(resource, ES.PropertyCondition_HasPropertyName);
148                         Double lowerLimit = graph.getPossibleRelatedValue(resource, ES.PropertyCondition_HasLowerLimit);
149                         Double upperLimit = graph.getPossibleRelatedValue(resource, ES.PropertyCondition_HasUpperLimit);
150                         cond = new PropertyCondition(propertyName, lowerLimit, upperLimit);
151                 }
152                 else if (graph.isInstanceOf(resource, ES.RegionCondition)) {
153                         DiagramRegionsResource DR = DiagramRegionsResource.getInstance(graph);
154                         double[] region = graph.getRelatedValue(graph.getSingleObject(resource, ES.RegionCondition_HasRegion), DR.Region_area);
155                         cond = new RegionCondition(region);
156                 }
157                 else if (graph.isInstanceOf(resource, ES.RouteCondition)) {
158                         Set<Resource> routePoints = new HashSet<>(ListUtils.toList(graph, graph.getSingleObject(resource, ES.RouteCondition_HasRoute)));
159                         cond = new RouteCondition(routePoints);
160                 }
161                 else if (graph.isInstanceOf(resource, ES.AggregateCondition)) {
162                         boolean isDisjunction = graph.isInstanceOf(resource, ES.Disjunction);
163                         Collection<Resource> conditionResources = graph.getObjects(resource, ES.HasSubcondition);
164                         List<Condition> conditions = new ArrayList<>(conditionResources.size());
165                         for (Resource c : conditionResources) {
166                                 conditions.add(buildCondition(graph, c));
167                         }
168                         Type type;
169                         if (graph.isInstanceOf(resource, ES.Conjunction))
170                                 type = AggregateCondition.Type.CONJUNCTION;
171                         else if (graph.isInstanceOf(resource, ES.Negation))
172                                 type = AggregateCondition.Type.NEGATION;
173                         else if (graph.isInstanceOf(resource, ES.Disjunction))
174                                 type = AggregateCondition.Type.DISJUNCTION;
175                         else
176                                 throw new IllegalArgumentException("Unknown aggreate condition type " + graph.getURI(graph.getSingleType(resource)));
177                                 
178                         cond = new AggregateCondition(type, conditions);
179                 }
180                 else {
181                         throw new IllegalArgumentException("Unknown condition type " + graph.getURI(graph.getSingleType(resource)));
182                 }
183                 
184                 cond.isInverse = graph.hasStatement(resource, ES.Condition_IsInverse, resource);
185                 return cond;
186         }
187
188         private static String buildExpression(ReadGraph graph, Resource r) throws DatabaseException {
189                 if (graph.isInstanceOf(r, ES.Selection)) {
190                         String exp = "select " + buildExpression(graph, graph.getSingleObject(r, ES.Selection_HasSelector)) + 
191                                      " from " + buildExpression(graph, graph.getSingleObject(r, ES.Selection_HasGenerator));
192                         
193                         Resource cond = graph.getPossibleObject(r, ES.Selection_HasCondition);
194                         return cond != null ? exp + " where {" + buildExpression(graph, cond) + "}" : exp;
195                 }
196                 else if (graph.isInstanceOf(r, ES.Condition)) {
197                         if (graph.isInstanceOf(r, ES.PropertyCondition)) {
198                                 return buildPropertyConditionExpression(graph, r);
199                         }
200                         else if (graph.isInstanceOf(r, ES.RegionCondition)) {
201                                 Resource region = graph.getSingleObject(r, ES.RegionCondition_HasRegion);
202                                 String name = graph.getRelatedValue(region, L0.HasLabel);
203                                 return "in region " + name;
204                         }
205                         else if (graph.isInstanceOf(r, ES.RouteCondition)) {
206                                 Resource route = graph.getSingleObject(r, ES.RouteCondition_HasRoute);
207                                 String name = graph.getRelatedValue(route, L0.HasLabel);
208                                 return "in route " + name;
209                         }
210                         else if (graph.isInstanceOf(r, ES.AggregateCondition)) {
211                                 String op = graph.isInstanceOf(r, ES.Conjunction) ? " and " : " or ";
212                                 List<String> exps = new ArrayList<>();
213                                 Collection<Resource> objects = graph.getObjects(r, ES.HasSubcondition);
214                                 for (Resource c : objects) {
215                                         String exp = buildExpression(graph, c);
216                                         exps.add(objects.size() > 1 ? "{" + exp + "}" : exp);
217                                 }
218                                 String result = String.join(op, exps);
219                                 if (graph.isInstanceOf(r, ES.Negation))
220                                         result = "not {" + result + "}";
221                                 return result;
222                         }
223                         else {
224                                 throw new DatabaseException("Unsupported condition resource type <" + graph.getURI(graph.getSingleType(r)) + ">");
225                         }
226                 }
227                 else if (graph.isInstanceOf(r, ES.Selector)) {
228                         if (graph.isInstanceOf(r, ES.Selector_All)) {
229                                 return "all";
230                         }
231                         else if (graph.isInstanceOf(r, ES.PropertySelector)) {
232                                 String op;
233                                 if (graph.isInstanceOf(r, ES.Selector_NLowest))
234                                         op = "bottom";
235                                 else if (graph.isInstanceOf(r, ES.Selector_NHighest))
236                                         op = "top";
237                                 else
238                                         throw new DatabaseException("Unsupported property selector resource type <" + graph.getURI(graph.getSingleType(r)) + ">");
239                                 
240                                 String name = graph.getRelatedValue(r, ES.PropertySelector_HasSelectionPropertyName);
241                                 Integer count = graph.getRelatedValue(r, ES.PropertySelector_HasResultCount);
242                                 return op + " " + count + " of " + name;
243                         }
244                         else {
245                                 throw new DatabaseException("Unsupported selector resource type <" + graph.getURI(graph.getSingleType(r)) + ">");
246                         }
247                 }
248                 else if (graph.isInstanceOf(r, ES.Generator)) {
249                         if (graph.isInstanceOf(r, ES.Generator_Model)) {
250                                 return "model";
251                         }
252                         else if (graph.isInstanceOf(r, ES.Generator_Diagram)) {
253                                 return "diagram \"" + graph.getRelatedValue(graph.getSingleObject(r, ES.Generator_HasDiagram), L0.HasName) + "\"";
254                         }
255                         else if (graph.isInstanceOf(r, ES.Generator_Explicit)) {
256                                 return "<list of " + graph.getObjects(r, ES.Generator_HasSelectedElement).size() + " elements>";
257                         }
258                         else {
259                                 throw new DatabaseException("Unsupported generator resource type <" + graph.getURI(graph.getSingleType(r)) + ">");
260                         }
261                 }
262                 else {
263                         throw new DatabaseException("Unsupported resource type <" + graph.getURI(graph.getSingleType(r)) + ">");
264                 }
265         }
266
267         private static String buildPropertyConditionExpression(ReadGraph graph, Resource r) throws DatabaseException {
268                 String propertyName = graph.getRelatedValue(r, ES.PropertyCondition_HasPropertyName);
269                 Double lowerLimit = graph.getPossibleRelatedValue(r, ES.PropertyCondition_HasLowerLimit);
270                 Double upperLimit = graph.getPossibleRelatedValue(r, ES.PropertyCondition_HasUpperLimit);
271                 if (lowerLimit == null && upperLimit == null) {
272                         return "has property " + propertyName;
273                 }
274                 else {
275                         StringBuilder result = new StringBuilder();
276                         if (lowerLimit != null) {
277                                 result.append(lowerLimit);
278                                 result.append(" < ");
279                         }
280                         result.append(propertyName);
281                         if (upperLimit != null) {
282                                 result.append(" < ");
283                                 result.append(upperLimit);
284                         }
285                         return result.toString();
286                 }
287         }
288
289         private static Collection<Resource> elementsOfDiagram(ReadGraph graph, Resource diagram) throws DatabaseException {
290                 if (graph.isInstanceOf(diagram, STR.Composite)) {
291                         // Resource is a composite - get diagram
292                         return elementsOfDiagram(graph, graph.getSingleObject(diagram, MOD.CompositeToDiagram));
293                 }
294                 
295                 Collection<Resource> elements = graph.getObjects(diagram, L0.ConsistsOf);
296                 Collection<Resource> result = new ArrayList<>();
297                 for (Resource r : elements)
298                         if (graph.isInstanceOf(r, DIA.Element))
299                                 result .add(r);
300                 return result;
301         }
302
303         private void buildSelection(ReadGraph graph) throws DatabaseException {
304                 Resource selector = graph.getSingleObject(resource, ES.Selection_HasSelector);
305                 Resource generator = graph.getSingleObject(resource, ES.Selection_HasGenerator);
306                 Resource condition = graph.getPossibleObject(resource, ES.Selection_HasCondition);
307                 
308                 this.selector = buildSelector(graph, selector);
309                 this.generator = buildGenerator(graph, generator);
310                 this.condition = buildCondition(graph, condition);
311         }
312
313         private Collection<Resource> filterElementsFrom(ReadGraph graph, Collection<Resource> elements) throws DatabaseException {
314                 if (condition == null) return elements;
315                 
316                 ArrayList<Resource> result = new ArrayList<>();
317                 for (Resource r : elements) {
318                         if (condition.match(graph, r))
319                                 result.add(r);
320                 }
321                 
322                 return result;
323         }
324         
325         private Collection<Resource> gather(ReadGraph graph, Resource model) throws DatabaseException {
326                 return selector.select(graph, filterElementsFrom(graph, generator.generate(graph, model)));
327         }
328         
329         static Variable getVariableForElement(ReadGraph graph, Resource element) throws DatabaseException {
330                 Resource component = graph.getPossibleObject(element, MOD.ElementToComponent);
331                 if (component != null) {
332                         Variable var = Variables.getVariable(graph, component);
333                         RVI realRvi = var.getRVI(graph);
334                         
335                         for (Resource activeModel : graph.syncRequest(new ActiveModels(Simantics.getProjectResource()))) {
336                                 for (Variable run : graph.syncRequest(new ActiveRuns(activeModel))) {
337                                         Variable v = realRvi.resolvePossible(graph, run);
338                                         if (v != null) {
339                                                 return v;
340                                         }
341                                 }
342                                 Variable configuration = Variables.getPossibleConfigurationContext(graph, activeModel);
343                                 if (configuration != null) {
344                                         Variable v = realRvi.resolvePossible(graph, configuration);
345                                         if (v != null) {
346                                                 return v;
347                                         }
348                                 }
349                         }
350                 }
351                 
352                 return null;
353         }
354         
355         static Double getPropertyValue(ReadGraph graph, Resource element, String propertyName) {
356                 try {
357                         Variable v = getVariableForElement(graph, element);
358                         if (v != null) {
359                                 Number value = v.getPossiblePropertyValue(graph, propertyName);
360                                 if (value != null)
361                                         return value.doubleValue();
362                         }
363                         
364                         // No property found - try possible mapped element property as well
365                         Resource mappedElement = graph.getPossibleObject(element, DN.MappedComponent);
366                         if (mappedElement != null)
367                                 return getPropertyValue(graph, mappedElement, propertyName);
368                 }
369                 catch (DatabaseException e) {
370                 }
371                 
372                 return null;
373         }
374         
375         // Generators
376         
377         static abstract class Generator {
378                 abstract Collection<Resource> generate(ReadGraph graph, Resource model) throws DatabaseException;
379         }
380         
381         static class ModelGenerator extends Generator {
382                 @Override
383                 Collection<Resource> generate(ReadGraph graph, Resource model) throws DatabaseException {
384                         Resource conf = graph.syncRequest(new Configuration(model));
385                         
386                         ArrayList<Resource> result = new ArrayList<>();
387                         
388                         // Iterate over diagrams
389                         for (Resource comp : graph.getObjects(conf, L0.ConsistsOf)) {
390                                 if (!graph.isInstanceOf(comp, STR.Composite)) continue;
391                                 
392                                 Resource diagram = graph.getPossibleObject(comp, MOD.CompositeToDiagram);
393                                 if (diagram != null) result.addAll(elementsOfDiagram(graph, diagram));
394                         }
395                         
396                         return result;
397                 }
398         }
399         
400         static class DiagramGenerator extends Generator {
401                 Resource diagram;
402                 
403                 public DiagramGenerator(Resource diagram) {
404                         this.diagram = diagram;
405                 }
406
407                 @Override
408                 Collection<Resource> generate(ReadGraph graph, Resource model) throws DatabaseException {
409                         return elementsOfDiagram(graph, diagram);
410                 }
411         }
412         
413         static class ExplicitGenerator extends Generator {
414                 public ExplicitGenerator(Collection<Resource> elements) {
415                         this.elements = elements;
416                 }
417
418                 Collection<Resource> elements;
419
420                 @Override
421                 Collection<Resource> generate(ReadGraph graph, Resource model) {
422                         return elements;
423                 }
424         }
425         
426         // Selectors
427         
428         static abstract class Selector {
429                 abstract Collection<Resource> select(ReadGraph graph, Collection<Resource> elements);
430         }
431         
432         static class All extends Selector {
433                 public All() {
434                 }
435
436                 @Override
437                 Collection<Resource> select(ReadGraph graph, Collection<Resource> elements) {
438                         return elements;
439                 }
440         }
441         
442         static class PropertySelector extends Selector {
443                 public PropertySelector(boolean smallest, String propertyName, int resultCount) {
444                         this.smallest = smallest;
445                         this.propertyName = propertyName;
446                         this.resultCount = resultCount;
447                 }
448                 
449                 boolean smallest;
450                 String propertyName;
451                 int resultCount;
452                 
453                 @SuppressWarnings("unchecked")
454                 @Override
455                 Collection<Resource> select(ReadGraph graph, Collection<Resource> elements) {
456                         List<Resource> result = new ArrayList<>(elements);
457                         List<Tuple2> result2 = Lists.map(new FunctionImpl1<Resource, Tuple2>() {
458                                 @Override
459                                 public Tuple2 apply(Resource r) {
460                                         return new Tuple2(r, getPropertyValue(graph, r, propertyName));
461                                 }
462                         }, result);
463                         
464                         result2 = Lists.filter(new FunctionImpl1<Tuple2, Boolean>() {
465                                 @Override
466                                 public Boolean apply(Tuple2 t) {
467                                         return t.c1 != null;
468                                 }
469                         }, result2);
470                         
471                         result2.sort((t1, t2) -> smallest ? Double.compare((Double) t1.c1, (Double) t2.c1) : Double.compare((Double) t2.c1, (Double) t1.c1));
472                         
473                         if (resultCount < result2.size())
474                                 result2 = result2.subList(0, resultCount);
475                         
476                         result = Lists.map(new FunctionImpl1<Tuple2, Resource>() {
477                                 @Override
478                                 public Resource apply(Tuple2 p0) {
479                                         return (Resource) p0.c0;
480                                 }
481                         }, result2);
482                         
483                         return result;
484                 }
485         }
486         
487         // Conditions
488         
489         static abstract class Condition {
490                 boolean isInverse;
491                 abstract boolean match(ReadGraph graph, Resource r) throws DatabaseException;
492         }
493         
494         static class PropertyCondition extends Condition {
495                 public PropertyCondition(String propertyName, Double lowerLimit, Double upperLimit) {
496                         super();
497                         this.propertyName = propertyName;
498                         this.lowerLimit = lowerLimit;
499                         this.upperLimit = upperLimit;
500                 }
501                 
502                 String propertyName;
503                 Double lowerLimit;
504                 Double upperLimit;
505                 
506                 @Override
507                 boolean match(ReadGraph graph, Resource r) {
508                         Double value = getPropertyValue(graph, r, propertyName);
509                         return value != null && (lowerLimit == null || value >= lowerLimit) && (upperLimit == null || value <= upperLimit);
510                 }
511         }
512         
513         static class RegionCondition extends Condition {
514                 public RegionCondition(double[] region) {
515                         super();
516                         this.region = region;
517                         
518                         Path2D path = new Path2D.Double();
519                         double startX = region[0];
520                         double startY = region[1];
521                         path.moveTo(startX, startY);
522                         for (int i = 2; i < region.length; i+=2)
523                                 path.lineTo(region[i], region[i+1]);
524                         path.closePath();
525
526                         this.path = path;
527                 }
528
529                 double[] region;
530                 Path2D path;
531
532                 @Override
533                 boolean match(ReadGraph graph, Resource r) throws DatabaseException {
534                         double[] transform = graph.getRelatedValue(r, DIA.HasTransform);
535                         double x = transform[4];
536                         double y = transform[5];
537                         return path.contains(x, y);
538                 } 
539         }
540         
541         static class RouteCondition extends Condition {
542                 public RouteCondition(Set<Resource> routePoints) {
543                         super();
544                         this.routePoints = routePoints;
545                 }
546
547                 Set<Resource> routePoints;
548
549                 @Override
550                 boolean match(ReadGraph graph, Resource r) throws DatabaseException {
551                         return routePoints.contains(r);
552                 }
553         }
554         
555         static class DiagramCondition extends Condition {
556                 public DiagramCondition(Resource diagram) {
557                         super();
558                         this.diagram = diagram;
559                 }
560
561                 Resource diagram;
562
563                 @Override
564                 boolean match(ReadGraph graph, Resource r) throws DatabaseException {
565                         return graph.getSingleObject(r, L0.PartOf).equals(diagram);
566                 }
567         }
568         
569         static class AggregateCondition extends Condition {
570                 static enum Type { DISJUNCTION, CONJUNCTION, NEGATION };
571                 
572                 public AggregateCondition(Type type, List<Condition> conditions) {
573                         super();
574                         this.type = type;
575                         this.conditions = conditions;
576                 }
577                 
578                 Type type;
579                 List<Condition> conditions;
580                 
581                 @Override
582                 boolean match(ReadGraph graph, Resource r) throws DatabaseException {
583                         switch (type) {
584                         case DISJUNCTION:
585                                 for (Condition c : conditions)
586                                         if (c.match(graph, r)) return true;
587                                 return false;
588                         case CONJUNCTION:
589                                 for (Condition c : conditions)
590                                         if (!c.match(graph, r)) return false;
591                                 return true;
592                         case NEGATION:
593                                 for (Condition c : conditions)
594                                         if (c.match(graph, r)) return false;
595                                 return true;
596                         default:
597                                 // Should not happen
598                                 throw new IllegalArgumentException("Unknown aggregate condition type " + type);
599                         }
600                 }
601         }
602         
603         public static class ElementSelectorQuery extends ResourceRead<ElementSelector> {
604                 public ElementSelectorQuery(Resource resource) {
605                         super(resource);
606                 }
607                 
608                 @Override
609                 public ElementSelector perform(ReadGraph graph) throws DatabaseException {
610                         return new ElementSelector(graph, resource);
611                 }
612         }
613 }