Line data Source code
1 : /****************************************************************************/
2 : // Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3 : // Copyright (C) 2002-2025 German Aerospace Center (DLR) and others.
4 : // This program and the accompanying materials are made available under the
5 : // terms of the Eclipse Public License 2.0 which is available at
6 : // https://www.eclipse.org/legal/epl-2.0/
7 : // This Source Code may also be made available under the following Secondary
8 : // Licenses when the conditions for such availability set forth in the Eclipse
9 : // Public License 2.0 are satisfied: GNU General Public License, version 2
10 : // or later which is available at
11 : // https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12 : // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13 : /****************************************************************************/
14 : /// @file ROPerson.cpp
15 : /// @author Daniel Krajzewicz
16 : /// @author Axel Wegener
17 : /// @author Michael Behrisch
18 : /// @author Jakob Erdmann
19 : /// @date Sept 2002
20 : ///
21 : // A vehicle as used by router
22 : /****************************************************************************/
23 : #include <config.h>
24 :
25 : #include <string>
26 : #include <iostream>
27 : #include <utils/common/StringTokenizer.h>
28 : #include <utils/common/ToString.h>
29 : #include <utils/common/StringUtils.h>
30 : #include <utils/common/MsgHandler.h>
31 : #include <utils/geom/GeoConvHelper.h>
32 : #include <utils/vehicle/SUMOVTypeParameter.h>
33 : #include <utils/options/OptionsCont.h>
34 : #include <utils/iodevices/OutputDevice.h>
35 : #include "RORouteDef.h"
36 : #include "RORoute.h"
37 : #include "ROVehicle.h"
38 : #include "ROHelper.h"
39 : #include "RONet.h"
40 : #include "ROLane.h"
41 : #include "ROPerson.h"
42 :
43 : const std::string ROPerson::PlanItem::UNDEFINED_STOPPING_PLACE;
44 :
45 : // ===========================================================================
46 : // method definitions
47 : // ===========================================================================
48 3338 : ROPerson::ROPerson(const SUMOVehicleParameter& pars, const SUMOVTypeParameter* type)
49 3338 : : RORoutable(pars, type) {
50 3338 : }
51 :
52 :
53 6676 : ROPerson::~ROPerson() {
54 6880 : for (PlanItem* const it : myPlan) {
55 3542 : delete it;
56 : }
57 6676 : }
58 :
59 :
60 : void
61 3090 : ROPerson::addTrip(std::vector<PlanItem*>& plan, const std::string& id,
62 : const ROEdge* const from, const ROEdge* const to, const SVCPermissions modeSet,
63 : const std::string& vTypes,
64 : const double departPos, const std::string& stopOrigin,
65 : const double arrivalPos, const std::string& busStop,
66 : double walkFactor, const std::string& group) {
67 3090 : PersonTrip* trip = new PersonTrip(from, to, modeSet, departPos, stopOrigin, arrivalPos, busStop, walkFactor, group);
68 3090 : RONet* net = RONet::getInstance();
69 3090 : SUMOVehicleParameter pars;
70 3090 : pars.departProcedure = DepartDefinition::TRIGGERED;
71 3090 : if (departPos != 0) {
72 206 : pars.departPosProcedure = DepartPosDefinition::GIVEN;
73 206 : pars.departPos = departPos;
74 206 : pars.parametersSet |= VEHPARS_DEPARTPOS_SET;
75 : }
76 6220 : for (StringTokenizer st(vTypes); st.hasNext();) {
77 40 : pars.vtypeid = st.next();
78 40 : pars.parametersSet |= VEHPARS_VTYPE_SET;
79 40 : const SUMOVTypeParameter* type = net->getVehicleTypeSecure(pars.vtypeid);
80 40 : if (type == nullptr) {
81 0 : delete trip;
82 0 : throw InvalidArgument("The vehicle type '" + pars.vtypeid + "' in a trip for person '" + id + "' is not known.");
83 : }
84 80 : pars.id = id + "_" + toString(trip->getVehicles().size());
85 80 : trip->addVehicle(new ROVehicle(pars, new RORouteDef("!" + pars.id, 0, false, false), type, net));
86 : // update modeset with routing-category vClass
87 40 : if (type->vehicleClass == SVC_BICYCLE) {
88 : trip->updateModes(SVC_BICYCLE);
89 : } else {
90 : trip->updateModes(SVC_PASSENGER);
91 : }
92 3090 : }
93 3090 : if (trip->getVehicles().empty()) {
94 3050 : if ((modeSet & SVC_PASSENGER) != 0) {
95 614 : pars.id = id + "_0";
96 1228 : trip->addVehicle(new ROVehicle(pars, new RORouteDef("!" + pars.id, 0, false, false), net->getVehicleTypeSecure(DEFAULT_VTYPE_ID), net));
97 : }
98 3050 : if ((modeSet & SVC_BICYCLE) != 0) {
99 24 : pars.id = id + "_b0";
100 : pars.vtypeid = DEFAULT_BIKETYPE_ID;
101 12 : pars.parametersSet |= VEHPARS_VTYPE_SET;
102 24 : trip->addVehicle(new ROVehicle(pars, new RORouteDef("!" + pars.id, 0, false, false), net->getVehicleTypeSecure(DEFAULT_BIKETYPE_ID), net));
103 : }
104 3050 : if ((modeSet & SVC_TAXI) != 0) {
105 : // add dummy taxi for routing (never added to output)
106 : pars.id = "taxi"; // id is written as 'line'
107 : pars.vtypeid = DEFAULT_TAXITYPE_ID;
108 186 : trip->addVehicle(new ROVehicle(pars, new RORouteDef("!" + pars.id, 0, false, false), net->getVehicleTypeSecure(DEFAULT_TAXITYPE_ID), net));
109 : }
110 : }
111 3090 : plan.push_back(trip);
112 3090 : }
113 :
114 :
115 : void
116 100 : ROPerson::addRide(std::vector<PlanItem*>& plan, const ROEdge* const from, const ROEdge* const to, const std::string& lines,
117 : double arrivalPos, const std::string& destStop, const std::string& group) {
118 300 : plan.push_back(new PersonTrip(to, destStop));
119 100 : plan.back()->addTripItem(new Ride(-1, from, to, lines, group, -1., arrivalPos, -1., destStop));
120 100 : }
121 :
122 :
123 : void
124 72 : ROPerson::addWalk(std::vector<PlanItem*>& plan, const ConstROEdgeVector& edges, const double duration, const double speed, const double departPos, const double arrivalPos, const std::string& busStop) {
125 72 : if (plan.empty() || plan.back()->isStop()) {
126 204 : plan.push_back(new PersonTrip(edges.back(), busStop));
127 : }
128 72 : plan.back()->addTripItem(new Walk(-1, edges, -1., duration, speed, departPos, arrivalPos, busStop));
129 72 : }
130 :
131 :
132 : void
133 128 : ROPerson::addStop(std::vector<PlanItem*>& plan, const SUMOVehicleParameter::Stop& stopPar, const ROEdge* const stopEdge) {
134 128 : plan.push_back(new Stop(stopPar, stopEdge));
135 128 : }
136 :
137 :
138 : void
139 880 : ROPerson::Ride::saveAsXML(OutputDevice& os, const bool extended, OptionsCont& options) const {
140 880 : os.openTag(SUMO_TAG_RIDE);
141 880 : std::string comment = "";
142 1760 : if ((extended || options.getBool("write-costs")) && myCost >= 0.) {
143 20 : os.writeAttr(SUMO_ATTR_COST, myCost);
144 : }
145 880 : if (from != nullptr) {
146 307 : os.writeAttr(SUMO_ATTR_FROM, from->getID());
147 : }
148 880 : if (to != nullptr) {
149 311 : os.writeAttr(SUMO_ATTR_TO, to->getID());
150 : }
151 880 : if (destStop != "") {
152 669 : const std::string element = RONet::getInstance()->getStoppingPlaceElement(destStop);
153 669 : os.writeAttr(element, destStop);
154 669 : const std::string name = RONet::getInstance()->getStoppingPlaceName(destStop);
155 669 : if (name != "") {
156 1224 : comment = " <!-- " + name + " -->";
157 : }
158 211 : } else if (arrPos != 0 && arrPos != std::numeric_limits<double>::infinity()) {
159 36 : os.writeAttr(SUMO_ATTR_ARRIVALPOS, arrPos);
160 : }
161 880 : if (lines != LINE_ANY) {
162 : // no need to write the default
163 247 : os.writeAttr(SUMO_ATTR_LINES, lines);
164 : }
165 880 : if (group != "") {
166 12 : os.writeAttr(SUMO_ATTR_GROUP, group);
167 : }
168 880 : if (intended != "" && intended != lines) {
169 569 : os.writeAttr(SUMO_ATTR_INTENDED, intended);
170 : }
171 880 : if (depart >= 0) {
172 1138 : os.writeAttr(SUMO_ATTR_DEPART, time2string(depart));
173 : }
174 1760 : if (options.getBool("exit-times")) {
175 24 : os.writeAttr("started", time2string(getStart()));
176 24 : os.writeAttr("ended", time2string(getStart() + getDuration()));
177 : }
178 1760 : if (options.getBool("route-length") && length != -1) {
179 24 : os.writeAttr("routeLength", length);
180 : }
181 880 : os.closeTag(comment);
182 880 : }
183 :
184 :
185 : void
186 192 : ROPerson::Stop::saveAsXML(OutputDevice& os, const bool /*extended*/, const bool /*asTrip*/, OptionsCont& /*options*/) const {
187 192 : stopDesc.write(os, false);
188 192 : std::string comment = "";
189 312 : for (std::string sID : stopDesc.getStoppingPlaceIDs()) {
190 120 : const std::string name = RONet::getInstance()->getStoppingPlaceName(sID);
191 120 : if (name != "") {
192 48 : comment += name + " ";
193 : }
194 192 : }
195 192 : if (comment != "") {
196 72 : comment = " <!-- " + comment + " -->";
197 : }
198 192 : stopDesc.writeParams(os);
199 192 : os.closeTag(comment);
200 192 : }
201 :
202 : void
203 2893 : ROPerson::Walk::saveAsXML(OutputDevice& os, const bool extended, OptionsCont& options) const {
204 2893 : os.openTag(SUMO_TAG_WALK);
205 2893 : std::string comment = "";
206 5786 : if ((extended || options.getBool("write-costs")) && myCost >= 0.) {
207 32 : os.writeAttr(SUMO_ATTR_COST, myCost);
208 : }
209 2893 : if (dur > 0) {
210 0 : os.writeAttr(SUMO_ATTR_DURATION, dur);
211 : }
212 2893 : if (v > 0) {
213 0 : os.writeAttr(SUMO_ATTR_SPEED, v);
214 : }
215 2893 : os.writeAttr(SUMO_ATTR_EDGES, edges);
216 5786 : if (options.getBool("exit-times")) {
217 16 : os.writeAttr("started", time2string(getStart()));
218 16 : os.writeAttr("ended", time2string(getStart() + getDuration()));
219 8 : if (!exitTimes.empty()) {
220 16 : os.writeAttr("exitTimes", exitTimes);
221 : }
222 : }
223 5786 : if (options.getBool("route-length")) {
224 8 : double length = 0;
225 28 : for (const ROEdge* roe : edges) {
226 : length += roe->getLength();
227 : }
228 16 : os.writeAttr("routeLength", length);
229 : }
230 2893 : if (destStop != "") {
231 802 : const std::string element = RONet::getInstance()->getStoppingPlaceElement(destStop);
232 802 : os.writeAttr(element, destStop);
233 802 : const std::string name = RONet::getInstance()->getStoppingPlaceName(destStop);
234 802 : if (name != "") {
235 1731 : comment = " <!-- " + name + " -->";
236 : }
237 2091 : } else if (arr != 0 && arr != std::numeric_limits<double>::infinity()) {
238 455 : os.writeAttr(SUMO_ATTR_ARRIVALPOS, arr);
239 : }
240 2893 : os.closeTag(comment);
241 2893 : }
242 :
243 : ROPerson::PlanItem*
244 350 : ROPerson::PersonTrip::clone() const {
245 350 : PersonTrip* result = new PersonTrip(from, to, modes, dep, stopOrigin, arr, stopDest, walkFactor, group);
246 370 : for (auto* item : myTripItems) {
247 20 : result->myTripItems.push_back(item->clone());
248 : }
249 350 : return result;
250 : }
251 :
252 : void
253 3440 : ROPerson::PersonTrip::saveVehicles(OutputDevice& os, OutputDevice* const typeos, bool asAlternatives, OptionsCont& options) const {
254 3648 : for (ROVehicle* veh : myVehicles) {
255 208 : if (!RONet::getInstance()->knowsVehicle(veh->getID())) {
256 204 : veh->saveAsXML(os, typeos, asAlternatives, options);
257 : }
258 : }
259 3440 : }
260 :
261 : void
262 4488 : ROPerson::PersonTrip::saveAsXML(OutputDevice& os, const bool extended, const bool asTrip, OptionsCont& options) const {
263 4488 : if ((asTrip || extended) && from != nullptr) {
264 2018 : const bool writeGeoTrip = asTrip && options.getBool("write-trips.geo");
265 2018 : os.openTag(SUMO_TAG_PERSONTRIP);
266 2018 : if (writeGeoTrip) {
267 8 : Position fromPos = from->getLanes()[0]->getShape().positionAtOffset2D(getDepartPos());
268 4 : if (GeoConvHelper::getFinal().usingGeoProjection()) {
269 0 : os.setPrecision(gPrecisionGeo);
270 0 : GeoConvHelper::getFinal().cartesian2geo(fromPos);
271 0 : os.writeAttr(SUMO_ATTR_FROMLONLAT, fromPos);
272 0 : os.setPrecision(gPrecision);
273 : } else {
274 4 : os.writeAttr(SUMO_ATTR_FROMXY, fromPos);
275 : }
276 : } else {
277 2014 : os.writeAttr(SUMO_ATTR_FROM, from->getID());
278 : }
279 2018 : if (writeGeoTrip) {
280 8 : Position toPos = to->getLanes()[0]->getShape().positionAtOffset2D(MIN2(getArrivalPos(), to->getLanes()[0]->getShape().length2D()));
281 4 : if (GeoConvHelper::getFinal().usingGeoProjection()) {
282 0 : os.setPrecision(gPrecisionGeo);
283 0 : GeoConvHelper::getFinal().cartesian2geo(toPos);
284 0 : os.writeAttr(SUMO_ATTR_TOLONLAT, toPos);
285 0 : os.setPrecision(gPrecision);
286 : } else {
287 4 : os.writeAttr(SUMO_ATTR_TOXY, toPos);
288 : }
289 : } else {
290 2014 : os.writeAttr(SUMO_ATTR_TO, to->getID());
291 : }
292 : std::vector<std::string> allowedModes;
293 2018 : if ((modes & SVC_BUS) != 0) {
294 1560 : allowedModes.push_back("public");
295 : }
296 2018 : if ((modes & SVC_PASSENGER) != 0) {
297 852 : allowedModes.push_back("car");
298 : }
299 2018 : if ((modes & SVC_TAXI) != 0) {
300 110 : allowedModes.push_back("taxi");
301 : }
302 2018 : if ((modes & SVC_BICYCLE) != 0) {
303 24 : allowedModes.push_back("bicycle");
304 : }
305 2018 : if (allowedModes.size() > 0) {
306 1944 : os.writeAttr(SUMO_ATTR_MODES, toString(allowedModes));
307 : }
308 2018 : if (!writeGeoTrip) {
309 2014 : if (dep != 0 && dep != std::numeric_limits<double>::infinity()) {
310 133 : os.writeAttr(SUMO_ATTR_DEPARTPOS, dep);
311 : }
312 2014 : if (arr != 0 && arr != std::numeric_limits<double>::infinity()) {
313 249 : os.writeAttr(SUMO_ATTR_ARRIVALPOS, arr);
314 : }
315 : }
316 2018 : if (getStopDest() != "") {
317 104 : os.writeAttr(SUMO_ATTR_BUS_STOP, getStopDest());
318 : }
319 2018 : if (walkFactor != 1) {
320 2018 : os.writeAttr(SUMO_ATTR_WALKFACTOR, walkFactor);
321 : }
322 2018 : if (extended && myTripItems.size() != 0) {
323 : std::vector<double> costs;
324 2374 : for (const TripItem* const tripItem : myTripItems) {
325 1390 : costs.push_back(tripItem->getCost());
326 : }
327 984 : os.writeAttr(SUMO_ATTR_COSTS, costs);
328 984 : }
329 2018 : os.closeTag();
330 2018 : } else {
331 6243 : for (const TripItem* const it : myTripItems) {
332 3773 : it->saveAsXML(os, extended, options);
333 : }
334 : }
335 4488 : }
336 :
337 : SUMOTime
338 3405 : ROPerson::PersonTrip::getDuration() const {
339 : SUMOTime result = 0;
340 8570 : for (TripItem* tItem : myTripItems) {
341 5165 : result += tItem->getDuration();
342 : }
343 3405 : return result;
344 : }
345 :
346 : bool
347 3237 : ROPerson::computeIntermodal(SUMOTime time, const RORouterProvider& provider,
348 : const PersonTrip* const trip, const ROVehicle* const veh,
349 : std::vector<TripItem*>& resultItems, MsgHandler* const errorHandler) {
350 3237 : const double speed = getMaxSpeed() * trip->getWalkFactor();
351 : std::vector<ROIntermodalRouter::TripItem> result;
352 3237 : provider.getIntermodalRouter().compute(trip->getOrigin(), trip->getDestination(),
353 : trip->getDepartPos(), trip->getStopOrigin(),
354 3237 : trip->getArrivalPos(), trip->getStopDest(),
355 : speed, veh, *getType(), trip->getModes(), time, result);
356 : bool carUsed = false;
357 : SUMOTime start = time;
358 : int index = 0;
359 3237 : const int lastIndex = (int)result.size() - 1;
360 8314 : for (const ROIntermodalRouter::TripItem& item : result) {
361 5077 : if (!item.edges.empty()) {
362 4993 : if (item.line == "") {
363 : double depPos = trip->getDepartPos(false);
364 : double arrPos = trip->getArrivalPos(false);
365 4033 : if (trip->getOrigin()->isTazConnector()) {
366 : // walk the whole length of the first edge
367 480 : const ROEdge* first = item.edges.front();
368 480 : if (std::find(first->getPredecessors().begin(), first->getPredecessors().end(), trip->getOrigin()) != first->getPredecessors().end()) {
369 : depPos = 0;
370 : } else {
371 : depPos = first->getLength();
372 : }
373 : }
374 4033 : if (trip->getDestination()->isTazConnector()) {
375 : // walk the whole length of the last edge
376 476 : const ROEdge* last = item.edges.back();
377 476 : if (std::find(last->getSuccessors().begin(), last->getSuccessors().end(), trip->getDestination()) != last->getSuccessors().end()) {
378 : arrPos = last->getLength();
379 : } else {
380 : arrPos = 0;
381 : }
382 : }
383 4033 : if (&item == &result.back() && trip->getStopDest() == "") {
384 5876 : resultItems.push_back(new Walk(start, item.edges, item.cost, item.exitTimes, depPos, arrPos));
385 : } else {
386 1095 : resultItems.push_back(new Walk(start, item.edges, item.cost, item.exitTimes, depPos, arrPos, item.destStop));
387 : }
388 960 : } else if (veh != nullptr && item.line == veh->getID()) {
389 174 : double cost = item.cost;
390 174 : if (veh->getVClass() != SVC_TAXI) {
391 116 : RORoute* route = new RORoute(veh->getID() + "_RouteDef", item.edges);
392 116 : route->setProbability(1);
393 116 : veh->getRouteDefinition()->addLoadedAlternative(route);
394 : carUsed = true;
395 58 : } else if (resultItems.empty()) {
396 : // if this is the first plan item the initial taxi waiting time wasn't added yet
397 38 : const double taxiWait = STEPS2TIME(string2time(OptionsCont::getOptions().getString("persontrip.taxi.waiting-time")));
398 38 : cost += taxiWait;
399 : }
400 174 : double arrPos = index == lastIndex ? trip->getArrivalPos(false) : item.arrivalPos;
401 348 : resultItems.push_back(new Ride(start, item.edges.front(), item.edges.back(), veh->getID(), trip->getGroup(), cost, arrPos, item.length, item.destStop));
402 : } else {
403 : // write origin for first element of the plan
404 786 : const ROEdge* origin = trip == myPlan.front() && resultItems.empty() ? trip->getOrigin() : nullptr;
405 786 : const std::string line = OptionsCont::getOptions().getBool("persontrip.ride-public-line") ? item.line : LINE_ANY;
406 786 : resultItems.push_back(new Ride(start, origin, nullptr, line, trip->getGroup(), item.cost, item.arrivalPos, item.length, item.destStop, item.intended, TIME2STEPS(item.depart)));
407 : }
408 : }
409 5077 : start += TIME2STEPS(item.cost);
410 5077 : index++;
411 : }
412 3237 : if (result.empty()) {
413 138 : errorHandler->inform("No route for trip in person '" + getID() + "'.");
414 69 : myRoutingSuccess = false;
415 : }
416 3237 : return carUsed;
417 3237 : }
418 :
419 :
420 : void
421 3329 : ROPerson::computeRoute(const RORouterProvider& provider,
422 : const bool /* removeLoops */, MsgHandler* errorHandler) {
423 3329 : myRoutingSuccess = true;
424 3329 : SUMOTime time = getParameter().depart;
425 6862 : for (PlanItem* const it : myPlan) {
426 3533 : if (it->needsRouting()) {
427 : PersonTrip* trip = static_cast<PersonTrip*>(it);
428 : const std::vector<ROVehicle*>& vehicles = trip->getVehicles();
429 : std::vector<TripItem*> resultItems;
430 : std::vector<TripItem*> best;
431 : const ROVehicle* bestVeh = nullptr;
432 3229 : if (vehicles.empty()) {
433 2519 : computeIntermodal(time, provider, trip, nullptr, best, errorHandler);
434 : } else {
435 : double bestCost = std::numeric_limits<double>::infinity();
436 1428 : for (const ROVehicle* const v : vehicles) {
437 718 : const bool carUsed = computeIntermodal(time, provider, trip, v, resultItems, errorHandler);
438 : double cost = 0.;
439 1546 : for (const TripItem* const tripIt : resultItems) {
440 828 : cost += tripIt->getCost();
441 : }
442 718 : if (cost < bestCost) {
443 : bestCost = cost;
444 710 : bestVeh = carUsed ? v : nullptr;
445 : best.swap(resultItems);
446 : }
447 726 : for (const TripItem* const tripIt : resultItems) {
448 8 : delete tripIt;
449 : }
450 : resultItems.clear();
451 : }
452 : }
453 3229 : trip->setItems(best, bestVeh);
454 3229 : }
455 3533 : time += it->getDuration();
456 : }
457 3329 : }
458 :
459 :
460 : void
461 4400 : ROPerson::saveAsXML(OutputDevice& os, OutputDevice* const typeos, bool asAlternatives, OptionsCont& options, int cloneIndex) const {
462 : // write the person's vehicles
463 8800 : const bool writeTrip = options.exists("write-trips") && options.getBool("write-trips");
464 8800 : const bool writeFlow = options.exists("keep-flows") && options.getBool("keep-flows") && isPartOfFlow();
465 4400 : if (!writeTrip) {
466 6990 : for (const PlanItem* const it : myPlan) {
467 3604 : it->saveVehicles(os, typeos, asAlternatives, options);
468 : }
469 : }
470 :
471 4400 : if (typeos != nullptr && getType() != nullptr && !getType()->saved) {
472 74 : getType()->write(*typeos);
473 74 : getType()->saved = true;
474 : }
475 4400 : if (getType() != nullptr && !getType()->saved) {
476 1162 : getType()->write(os);
477 1162 : getType()->saved = asAlternatives;
478 : }
479 4400 : const SumoXMLTag tag = writeFlow ? SUMO_TAG_PERSONFLOW : SUMO_TAG_PERSON;
480 : // write the person
481 4400 : if (cloneIndex == 0) {
482 8736 : getParameter().write(os, options, tag);
483 : } else {
484 32 : SUMOVehicleParameter p = getParameter();
485 : // @note id collisions may occur if scale-suffic occurs in other vehicle ids
486 64 : p.id += options.getString("scale-suffix") + toString(cloneIndex);
487 32 : p.write(os, options, tag);
488 32 : }
489 :
490 9080 : for (const PlanItem* const it : myPlan) {
491 4680 : it->saveAsXML(os, asAlternatives, writeTrip, options);
492 : }
493 :
494 : // write params
495 4400 : getParameter().writeParams(os);
496 4400 : os.closeTag();
497 4400 : }
498 :
499 :
500 : /****************************************************************************/
|