1 package org.opentrafficsim.road.gtu.lane.tactical;
2
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.LinkedHashMap;
6 import java.util.Map;
7
8 import org.djunits.unit.AccelerationUnit;
9 import org.djunits.unit.DurationUnit;
10 import org.djunits.unit.LengthUnit;
11 import org.djunits.value.ValueRuntimeException;
12 import org.djunits.value.storage.StorageType;
13 import org.djunits.value.vdouble.scalar.Acceleration;
14 import org.djunits.value.vdouble.scalar.Duration;
15 import org.djunits.value.vdouble.scalar.Length;
16 import org.djunits.value.vdouble.scalar.Speed;
17 import org.djunits.value.vdouble.scalar.Time;
18 import org.djunits.value.vdouble.vector.AccelerationVector;
19 import org.djutils.draw.point.OrientedPoint2d;
20 import org.opentrafficsim.base.geometry.OtsLine2d;
21 import org.opentrafficsim.base.parameters.ParameterException;
22 import org.opentrafficsim.base.parameters.ParameterTypeLength;
23 import org.opentrafficsim.base.parameters.ParameterTypes;
24 import org.opentrafficsim.core.gtu.GtuException;
25 import org.opentrafficsim.core.gtu.GtuType;
26 import org.opentrafficsim.core.gtu.plan.operational.OperationalPlan;
27 import org.opentrafficsim.core.gtu.plan.operational.Segments;
28 import org.opentrafficsim.core.network.LateralDirectionality;
29 import org.opentrafficsim.core.network.Link;
30 import org.opentrafficsim.core.network.NetworkException;
31 import org.opentrafficsim.core.network.Node;
32 import org.opentrafficsim.road.gtu.lane.LaneBasedGtu;
33 import org.opentrafficsim.road.gtu.lane.perception.CategoricalLanePerception;
34 import org.opentrafficsim.road.gtu.lane.perception.LanePerception;
35 import org.opentrafficsim.road.gtu.lane.perception.categories.DefaultSimplePerception;
36 import org.opentrafficsim.road.gtu.lane.perception.categories.DirectDefaultSimplePerception;
37 import org.opentrafficsim.road.gtu.lane.perception.headway.Headway;
38 import org.opentrafficsim.road.gtu.lane.perception.headway.HeadwayTrafficLight;
39 import org.opentrafficsim.road.gtu.lane.tactical.following.GtuFollowingModelOld;
40 import org.opentrafficsim.road.gtu.lane.tactical.lanechangemobil.LaneChangeModel;
41 import org.opentrafficsim.road.gtu.lane.tactical.lanechangemobil.LaneMovementStep;
42 import org.opentrafficsim.road.network.lane.CrossSectionElement;
43 import org.opentrafficsim.road.network.lane.CrossSectionLink;
44 import org.opentrafficsim.road.network.lane.Lane;
45 import org.opentrafficsim.road.network.lane.LanePosition;
46 import org.opentrafficsim.road.network.lane.object.detector.LaneDetector;
47 import org.opentrafficsim.road.network.lane.object.detector.SinkDetector;
48
49
50
51
52
53
54
55
56
57
58
59
60 public class LaneBasedCfLcTacticalPlanner extends AbstractLaneBasedTacticalPlanner
61 {
62
63 private static final long serialVersionUID = 20151125L;
64
65
66 protected static final ParameterTypeLength LOOKBACKOLD = ParameterTypes.LOOKBACKOLD;
67
68
69 private static final Acceleration STAYINCURRENTLANEINCENTIVE = new Acceleration(0.1, AccelerationUnit.METER_PER_SECOND_2);
70
71
72 private static final Acceleration PREFERREDLANEINCENTIVE = new Acceleration(0.3, AccelerationUnit.METER_PER_SECOND_2);
73
74
75 private static final Acceleration NONPREFERREDLANEINCENTIVE = new Acceleration(-0.3, AccelerationUnit.METER_PER_SECOND_2);
76
77
78 public static final Length NOLANECHANGENEEDED = new Length(Double.MAX_VALUE, LengthUnit.SI);
79
80
81 public static final Length GETOFFTHISLANENOW = Length.ZERO;
82
83
84 private static final Duration TIMEHORIZON = new Duration(90, DurationUnit.SECOND);
85
86
87 private LaneChangeModel laneChangeModel;
88
89
90
91
92
93
94
95 public LaneBasedCfLcTacticalPlanner(final GtuFollowingModelOld carFollowingModel, final LaneChangeModel laneChangeModel,
96 final LaneBasedGtu gtu)
97 {
98 super(carFollowingModel, gtu, new CategoricalLanePerception(gtu));
99 this.laneChangeModel = laneChangeModel;
100 getPerception().addPerceptionCategory(new DirectDefaultSimplePerception(getPerception()));
101 }
102
103 @Override
104 public final OperationalPlan generateOperationalPlan(final Time startTime, final OrientedPoint2d locationAtStartTime)
105 throws NetworkException, GtuException, ParameterException
106 {
107 try
108 {
109
110 LaneBasedGtu laneBasedGTU = getGtu();
111 LanePerception perception = getPerception();
112
113
114 if (laneBasedGTU.getMaximumSpeed().si < OperationalPlan.DRIFTING_SPEED_SI)
115 {
116 return OperationalPlan.standStill(getGtu(), getGtu().getLocation(), startTime, Duration.ONE);
117 }
118
119 Length maximumForwardHeadway = laneBasedGTU.getParameters().getParameter(LOOKAHEAD);
120 DefaultSimplePerception simplePerception = perception.getPerceptionCategory(DefaultSimplePerception.class);
121 simplePerception.updateSpeedLimit();
122 simplePerception.updateForwardHeadwayGtu();
123 simplePerception.updateForwardHeadwayObject();
124 simplePerception.updateBackwardHeadway();
125 simplePerception.updateNeighboringHeadwaysLeft();
126 simplePerception.updateNeighboringHeadwaysRight();
127 Speed speedLimit = simplePerception.getSpeedLimit();
128
129
130 Headway sameLaneLeader = simplePerception.getForwardHeadwayGtu();
131
132 Headway sameLaneFollower = simplePerception.getBackwardHeadway();
133 Collection<Headway> sameLaneTraffic = new ArrayList<>();
134 if (sameLaneLeader.getObjectType().isGtu())
135 {
136 sameLaneTraffic.add(sameLaneLeader);
137 }
138 if (sameLaneFollower.getObjectType().isGtu())
139 {
140 sameLaneTraffic.add(sameLaneFollower);
141 }
142
143
144 LanePathInfo lanePathInfo = buildLanePathInfo(laneBasedGTU, maximumForwardHeadway);
145
146
147 NextSplitInfo nextSplitInfo = determineNextSplit(laneBasedGTU, maximumForwardHeadway);
148 boolean currentLaneFine = nextSplitInfo.correctCurrentLanes().contains(lanePathInfo.getReferenceLane());
149
150
151
152
153
154 Collection<Headway> leftLaneTraffic = simplePerception.getNeighboringHeadwaysLeft();
155 Collection<Headway> rightLaneTraffic = simplePerception.getNeighboringHeadwaysRight();
156
157
158 final LateralDirectionality preferred = LateralDirectionality.RIGHT;
159 final Acceleration defaultLeftLaneIncentive =
160 preferred.isLeft() ? PREFERREDLANEINCENTIVE : NONPREFERREDLANEINCENTIVE;
161 final Acceleration defaultRightLaneIncentive =
162 preferred.isRight() ? PREFERREDLANEINCENTIVE : NONPREFERREDLANEINCENTIVE;
163
164 AccelerationVector defaultLaneIncentives = new AccelerationVector(new double[] {defaultLeftLaneIncentive.getSI(),
165 STAYINCURRENTLANEINCENTIVE.getSI(), defaultRightLaneIncentive.getSI()}, AccelerationUnit.SI);
166 AccelerationVector laneIncentives = laneIncentives(laneBasedGTU, defaultLaneIncentives);
167 LaneMovementStep lcmr = this.laneChangeModel.computeLaneChangeAndAcceleration(laneBasedGTU, sameLaneTraffic,
168 rightLaneTraffic, leftLaneTraffic, speedLimit,
169 new Acceleration(laneIncentives.get(preferred.isRight() ? 2 : 0)), new Acceleration(laneIncentives.get(1)),
170 new Acceleration(laneIncentives.get(preferred.isRight() ? 0 : 2)));
171 Duration duration = lcmr.getGfmr().getValidUntil().minus(getGtu().getSimulator().getSimulatorAbsTime());
172 if (lcmr.getLaneChangeDirection() != null)
173 {
174 laneBasedGTU.changeLaneInstantaneously(lcmr.getLaneChangeDirection());
175
176
177 lanePathInfo = buildLanePathInfo(laneBasedGTU, maximumForwardHeadway);
178 }
179
180
181 Headway object = simplePerception.getForwardHeadwayObject();
182 Acceleration a = lcmr.getGfmr().getAcceleration();
183 if (object instanceof HeadwayTrafficLight)
184 {
185
186 a = Acceleration.min(a, ((GtuFollowingModelOld) getCarFollowingModel()).computeAcceleration(getGtu().getSpeed(),
187 getGtu().getMaximumSpeed(), Speed.ZERO, object.getDistance(), speedLimit));
188 }
189
190
191 Length dist = lanePathInfo.path().getTypedLength().minus(getGtu().getFront().dx());
192 a = Acceleration.min(a, ((GtuFollowingModelOld) getCarFollowingModel()).computeAcceleration(getGtu().getSpeed(),
193 getGtu().getMaximumSpeed(), Speed.ZERO, dist, speedLimit));
194
195
196 if (a.si < 1E-6 && laneBasedGTU.getSpeed().si < OperationalPlan.DRIFTING_SPEED_SI)
197 {
198 return OperationalPlan.standStill(getGtu(), getGtu().getLocation(), startTime, Duration.ONE);
199 }
200 OtsLine2d path = lanePathInfo.path();
201 OperationalPlan op = new OperationalPlan(getGtu(), path, startTime, Segments.off(getGtu().getSpeed(), duration, a));
202 return op;
203 }
204 catch (ValueRuntimeException exception)
205 {
206 throw new GtuException(exception);
207 }
208 }
209
210
211
212
213
214
215
216
217
218
219
220
221 private AccelerationVector laneIncentives(final LaneBasedGtu gtu, final AccelerationVector defaultLaneIncentives)
222 throws NetworkException, ValueRuntimeException, GtuException
223 {
224 Length leftSuitability = suitability(gtu, LateralDirectionality.LEFT);
225 Length currentSuitability = suitability(gtu, null);
226 Length rightSuitability = suitability(gtu, LateralDirectionality.RIGHT);
227 if (leftSuitability == NOLANECHANGENEEDED && currentSuitability == NOLANECHANGENEEDED
228 && rightSuitability == NOLANECHANGENEEDED)
229 {
230 return checkLaneDrops(gtu, defaultLaneIncentives);
231 }
232 if ((leftSuitability == NOLANECHANGENEEDED || leftSuitability == GETOFFTHISLANENOW)
233 && currentSuitability == NOLANECHANGENEEDED
234 && (rightSuitability == NOLANECHANGENEEDED || rightSuitability == GETOFFTHISLANENOW))
235 {
236 return checkLaneDrops(gtu, new AccelerationVector(new double[] {acceleration(gtu, leftSuitability),
237 defaultLaneIncentives.get(1).getSI(), acceleration(gtu, rightSuitability)}, AccelerationUnit.SI));
238 }
239 if (currentSuitability == NOLANECHANGENEEDED)
240 {
241 return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability),
242 defaultLaneIncentives.get(1).getSI(), acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
243 }
244 return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability), acceleration(gtu, currentSuitability),
245 acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
246 }
247
248
249
250
251
252
253
254
255
256
257
258
259 private AccelerationVector checkLaneDrops(final LaneBasedGtu gtu, final AccelerationVector defaultLaneIncentives)
260 throws NetworkException, ValueRuntimeException, GtuException
261 {
262
263 Length leftSuitability = Double.isNaN(defaultLaneIncentives.get(0).si) || defaultLaneIncentives.get(0).si < -10
264 ? GETOFFTHISLANENOW : laneDrop(gtu, LateralDirectionality.LEFT);
265 Length currentSuitability = laneDrop(gtu, null);
266 Length rightSuitability = Double.isNaN(defaultLaneIncentives.get(2).si) || defaultLaneIncentives.get(2).si < -10
267 ? GETOFFTHISLANENOW : laneDrop(gtu, LateralDirectionality.RIGHT);
268
269 if ((leftSuitability == NOLANECHANGENEEDED || leftSuitability == GETOFFTHISLANENOW)
270 && currentSuitability == NOLANECHANGENEEDED
271 && (rightSuitability == NOLANECHANGENEEDED || rightSuitability == GETOFFTHISLANENOW))
272 {
273 return defaultLaneIncentives;
274 }
275
276 if (currentSuitability == NOLANECHANGENEEDED)
277 {
278 return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability),
279 defaultLaneIncentives.get(1).getSI(), acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
280 }
281 if (currentSuitability.le(leftSuitability))
282 {
283 return new AccelerationVector(
284 new double[] {PREFERREDLANEINCENTIVE.getSI(), NONPREFERREDLANEINCENTIVE.getSI(), GETOFFTHISLANENOW.getSI()},
285 AccelerationUnit.SI);
286 }
287 if (currentSuitability.le(rightSuitability))
288 {
289 return new AccelerationVector(
290 new double[] {GETOFFTHISLANENOW.getSI(), NONPREFERREDLANEINCENTIVE.getSI(), PREFERREDLANEINCENTIVE.getSI()},
291 AccelerationUnit.SI, StorageType.DENSE);
292 }
293 return new AccelerationVector(new double[] {acceleration(gtu, leftSuitability), acceleration(gtu, currentSuitability),
294 acceleration(gtu, rightSuitability)}, AccelerationUnit.SI);
295 }
296
297
298
299
300
301
302
303
304
305
306
307
308
309 private Length laneDrop(final LaneBasedGtu gtu, final LateralDirectionality direction) throws NetworkException, GtuException
310 {
311 LanePosition dlp = gtu.getReferencePosition();
312 Lane lane = dlp.lane();
313 Length longitudinalPosition = dlp.position();
314 if (null != direction)
315 {
316 lane = getPerception().getPerceptionCategory(DefaultSimplePerception.class).bestAccessibleAdjacentLane(lane,
317 direction, longitudinalPosition);
318 }
319 if (null == lane)
320 {
321 return GETOFFTHISLANENOW;
322 }
323 double remainingLength = lane.getLength().getSI() - longitudinalPosition.getSI();
324 double remainingTimeSI = TIMEHORIZON.getSI() - remainingLength / lane.getSpeedLimit(gtu.getType()).getSI();
325 while (remainingTimeSI >= 0)
326 {
327 for (LaneDetector detector : lane.getDetectors())
328 {
329 if (detector instanceof SinkDetector)
330 {
331 return NOLANECHANGENEEDED;
332 }
333 }
334 int branching = lane.nextLanes(gtu.getType()).size();
335 if (branching == 0)
336 {
337 return new Length(remainingLength, LengthUnit.SI);
338 }
339 if (branching > 1)
340 {
341 return NOLANECHANGENEEDED;
342 }
343 lane = lane.nextLanes(gtu.getType()).iterator().next();
344 remainingTimeSI -= lane.getLength().getSI() / lane.getSpeedLimit(gtu.getType()).getSI();
345 remainingLength += lane.getLength().getSI();
346 }
347 return NOLANECHANGENEEDED;
348 }
349
350
351
352
353
354
355
356
357
358
359
360 private Length suitability(final LaneBasedGtu gtu, final LateralDirectionality direction)
361 throws NetworkException, GtuException
362 {
363 LanePosition dlp = gtu.getReferencePosition();
364 Lane lane = dlp.lane();
365 Length longitudinalPosition = dlp.position().plus(gtu.getFront().dx());
366 if (null != direction)
367 {
368 lane = getPerception().getPerceptionCategory(DefaultSimplePerception.class).bestAccessibleAdjacentLane(lane,
369 direction, longitudinalPosition);
370 }
371 if (null == lane)
372 {
373 return GETOFFTHISLANENOW;
374 }
375 try
376 {
377 return suitability(lane, longitudinalPosition, gtu, TIMEHORIZON);
378
379 }
380 catch (NetworkException ne)
381 {
382 System.err.println(gtu + " has a route problem in suitability: " + ne.getMessage());
383 return NOLANECHANGENEEDED;
384 }
385 }
386
387
388
389
390
391
392
393 private double acceleration(final LaneBasedGtu gtu, final Length stopDistance)
394 {
395
396
397 double v = gtu.getSpeed().getSI();
398 double a = -v * v / 2 / stopDistance.getSI();
399 return a;
400 }
401
402
403
404
405
406
407
408
409
410
411
412
413 private Length suitability(final Lane lane, final Length longitudinalPosition, final LaneBasedGtu gtu,
414 final Duration timeHorizon) throws NetworkException
415 {
416 double remainingDistance = lane.getLength().getSI() - longitudinalPosition.getSI();
417 double spareTime = timeHorizon.getSI() - remainingDistance / lane.getSpeedLimit(gtu.getType()).getSI();
418
419 Node nextNode = lane.getLink().getEndNode();
420 Link lastLink = lane.getLink();
421 Node nextSplitNode = null;
422 Lane currentLane = lane;
423 CrossSectionLink linkBeforeBranch = lane.getLink();
424 while (null != nextNode)
425 {
426 if (spareTime <= 0)
427 {
428 return NOLANECHANGENEEDED;
429 }
430 int laneCount = countCompatibleLanes(linkBeforeBranch, gtu.getType());
431 if (0 == laneCount)
432 {
433 throw new NetworkException("No compatible Lanes on Link " + linkBeforeBranch);
434 }
435 if (1 == laneCount)
436 {
437 return NOLANECHANGENEEDED;
438
439 }
440 int branching = nextNode.getLinks().size();
441 if (branching > 2)
442 {
443 nextSplitNode = nextNode;
444 break;
445 }
446 else if (1 == branching)
447 {
448 return NOLANECHANGENEEDED;
449 }
450 else
451 {
452 Link nextLink = gtu.getStrategicalPlanner().nextLink(lastLink, gtu.getType());
453 if (nextLink instanceof CrossSectionLink)
454 {
455 nextNode = nextLink.getEndNode();
456
457 remainingDistance += nextLink.getLength().getSI();
458 linkBeforeBranch = (CrossSectionLink) nextLink;
459
460 if (currentLane.nextLanes(gtu.getType()).size() == 0)
461 {
462
463
464 if (currentLane.accessibleAdjacentLanesLegal(LateralDirectionality.RIGHT, gtu.getType()).size() > 0)
465 {
466 for (Lane adjacentLane : currentLane.accessibleAdjacentLanesLegal(LateralDirectionality.RIGHT,
467 gtu.getType()))
468 {
469 if (adjacentLane.nextLanes(gtu.getType()).size() > 0)
470 {
471 currentLane = adjacentLane;
472 break;
473 }
474
475
476 }
477 }
478 for (Lane adjacentLane : currentLane.accessibleAdjacentLanesLegal(LateralDirectionality.LEFT,
479 gtu.getType()))
480 {
481 if (adjacentLane.nextLanes(gtu.getType()).size() > 0)
482 {
483 currentLane = adjacentLane;
484 break;
485 }
486
487
488 }
489 if (currentLane.nextLanes(gtu.getType()).size() == 0)
490 {
491 throw new NetworkException(
492 "Lane ends and there is not a compatible adjacent lane that does " + "not end");
493 }
494 }
495
496 for (Lane nextLane : currentLane.nextLanes(gtu.getType()))
497 {
498 currentLane = currentLane.nextLanes(gtu.getType()).iterator().next();
499 break;
500 }
501 spareTime -= currentLane.getLength().getSI() / currentLane.getSpeedLimit(gtu.getType()).getSI();
502 }
503 else
504 {
505
506
507 return NOLANECHANGENEEDED;
508 }
509 lastLink = nextLink;
510 }
511 }
512 if (null == nextNode)
513 {
514 throw new NetworkException("Cannot find the next branch or sink node");
515 }
516
517
518 Map<Lane, Length> suitabilityOfLanesBeforeBranch = new LinkedHashMap<>();
519 Link linkAfterBranch = gtu.getStrategicalPlanner().nextLink(lastLink, gtu.getType());
520 for (CrossSectionElement cse : linkBeforeBranch.getCrossSectionElementList())
521 {
522 if (cse instanceof Lane)
523 {
524 Lane l = (Lane) cse;
525 if (l.getType().isCompatible(gtu.getType()))
526 {
527 for (Lane connectingLane : l.nextLanes(gtu.getType()))
528 {
529 if (connectingLane.getLink() == linkAfterBranch && connectingLane.getType().isCompatible(gtu.getType()))
530 {
531 Length currentValue = suitabilityOfLanesBeforeBranch.get(l);
532
533
534
535 Length value = suitability(connectingLane, new Length(0, LengthUnit.SI), gtu,
536 new Duration(spareTime, DurationUnit.SI));
537
538 value = value.plus(new Length(remainingDistance, LengthUnit.SI));
539
540
541 suitabilityOfLanesBeforeBranch.put(l,
542 null == currentValue || value.le(currentValue) ? value : currentValue);
543 }
544 }
545 }
546 }
547 }
548 if (suitabilityOfLanesBeforeBranch.size() == 0)
549 {
550 throw new NetworkException("No lanes available on Link " + linkBeforeBranch);
551 }
552 Length currentLaneSuitability = suitabilityOfLanesBeforeBranch.get(currentLane);
553 if (null != currentLaneSuitability)
554 {
555 return currentLaneSuitability;
556 }
557
558 int totalLanes = countCompatibleLanes(currentLane.getLink(), gtu.getType());
559 Length leftSuitability = computeSuitabilityWithLaneChanges(currentLane, remainingDistance,
560 suitabilityOfLanesBeforeBranch, totalLanes, LateralDirectionality.LEFT, gtu.getType());
561 Length rightSuitability = computeSuitabilityWithLaneChanges(currentLane, remainingDistance,
562 suitabilityOfLanesBeforeBranch, totalLanes, LateralDirectionality.RIGHT, gtu.getType());
563 if (leftSuitability.ge(rightSuitability))
564 {
565 return leftSuitability;
566 }
567 else if (rightSuitability.ge(leftSuitability))
568 {
569
570 return rightSuitability;
571 }
572 if (leftSuitability.le(GETOFFTHISLANENOW))
573 {
574 throw new NetworkException("Changing lanes in any direction does not get the GTU on a suitable lane");
575 }
576 return leftSuitability;
577 }
578
579
580
581
582
583
584
585
586
587
588
589
590
591 protected final Length computeSuitabilityWithLaneChanges(final Lane startLane, final double remainingDistance,
592 final Map<Lane, Length> suitabilities, final int totalLanes, final LateralDirectionality direction,
593 final GtuType gtuType)
594 {
595
596
597
598
599
600
601
602
603
604 int laneChangesUsed = 0;
605 Lane currentLane = startLane;
606 Length currentSuitability = null;
607 while (null == currentSuitability)
608 {
609 laneChangesUsed++;
610 if (currentLane.accessibleAdjacentLanesLegal(direction, gtuType).size() == 0)
611 {
612 return GETOFFTHISLANENOW;
613 }
614 currentLane = currentLane.accessibleAdjacentLanesLegal(direction, gtuType).iterator().next();
615 currentSuitability = suitabilities.get(currentLane);
616 }
617 double fraction = currentSuitability == NOLANECHANGENEEDED ? 0 : 0.5;
618 int notSuitableLaneCount = totalLanes - suitabilities.size();
619 return new Length(
620 remainingDistance * (notSuitableLaneCount - laneChangesUsed + 1 + fraction) / (notSuitableLaneCount + fraction),
621 LengthUnit.SI);
622 }
623
624
625
626
627
628
629
630
631 protected final int countCompatibleLanes(final CrossSectionLink link, final GtuType gtuType)
632 {
633 int result = 0;
634 for (CrossSectionElement cse : link.getCrossSectionElementList())
635 {
636 if (cse instanceof Lane)
637 {
638 Lane l = (Lane) cse;
639 if (l.getType().isCompatible(gtuType))
640 {
641 result++;
642 }
643 }
644 }
645 return result;
646 }
647
648 @Override
649 public final String toString()
650 {
651 return "LaneBasedCFLCTacticalPlanner [laneChangeModel=" + this.laneChangeModel + "]";
652 }
653
654 }