KPublicTransport

location.cpp
1/*
2 SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
3
4 SPDX-License-Identifier: LGPL-2.0-or-later
5*/
6
7#include "location.h"
8
9#include "datatypes_p.h"
10#include "equipment.h"
11#include "equipmentutil.h"
12#include "json_p.h"
13#include "mergeutil_p.h"
14#include "rentalvehicle.h"
15#include "rentalvehicleutil_p.h"
16#include "ifopt/ifoptutil.h"
17
18#include <KTimeZone>
19#include <KCountry>
20#include <KCountrySubdivision>
21
22#include <QDebug>
23#include <QHash>
24#include <QJsonArray>
25#include <QJsonObject>
26#include <QRegularExpression>
27#include <QTimeZone>
28
29#include <cmath>
30
31using namespace KPublicTransport;
32using namespace Qt::Literals;
33
34namespace KPublicTransport {
35
36class LocationPrivate : public QSharedData
37{
38public:
40 QString name;
41 double latitude = NAN;
42 double longitude = NAN;
43 QTimeZone timeZone;
45
46 QString streetAddress;
47 QString postalCode;
48 QString locality;
49 QString region;
50 QString country;
51
52 int floorLevel = std::numeric_limits<int>::lowest();
53
54 QVariant data;
55};
56
57}
58
59KPUBLICTRANSPORT_MAKE_GADGET(Location)
60KPUBLICTRANSPORT_MAKE_PROPERTY(Location, Location::Type, type, setType)
61KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, name, setName)
62KPUBLICTRANSPORT_MAKE_PROPERTY(Location, double, latitude, setLatitude)
63KPUBLICTRANSPORT_MAKE_PROPERTY(Location, double, longitude, setLongitude)
64KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, streetAddress, setStreetAddress)
65KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, postalCode, setPostalCode)
66KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QString, locality, setLocality)
67KPUBLICTRANSPORT_MAKE_PROPERTY(Location, int, floorLevel, setFloorLevel)
68
69void Location::setRegion(const QString &regionCode)
70{
71 d.detach();
72 d->region = regionCode;
73}
74
76{
77 if (d->region.isEmpty() && hasCoordinate()) {
78 auto subdivision = KCountrySubdivision::fromLocation((float)latitude(), (float)longitude());
79 const_cast<Location *>(this)->setRegion(subdivision.code());
80 }
81
82 return d->region;
83}
84
85void Location::setCountry(const QString &countryCode)
86{
87 d.detach();
88 d->country = countryCode;
89}
90
92{
93 if (d->country.isEmpty() && hasCoordinate()) {
94 auto country = KCountry::fromLocation((float)latitude(), (float)longitude());
95 const_cast<Location *>(this)->setCountry(country.alpha2());
96 }
97
98 return d->country;
99}
100
101KPUBLICTRANSPORT_MAKE_PROPERTY(Location, QVariant, data, setData)
102
103void Location::setCoordinate(double latitude, double longitude)
104{
105 d.detach();
106 d->latitude = latitude;
107 d->longitude = longitude;
108}
109
110bool Location::hasCoordinate() const
111{
112 return !std::isnan(d->latitude) && !std::isnan(d->longitude) && std::abs(d->latitude) <= 90.0 && std::abs(d->longitude) <= 180.0;
113}
114
115bool Location::hasFloorLevel() const
116{
117 return d->floorLevel > std::numeric_limits<int>::lowest() && d->floorLevel < std::numeric_limits<int>::max();
118}
119
121{
122 return !hasCoordinate() && d->name.isEmpty() && d->ids.isEmpty() && d->streetAddress.isEmpty();
123}
124
126{
127 if (d->timeZone.isValid()) {
128 return d->timeZone;
129 }
130 if (hasCoordinate()) {
131 if (const auto tzId = KTimeZone::fromLocation((float)latitude(), (float)longitude()); tzId) {
132 return QTimeZone(tzId);
133 }
134 }
135 return {};
136}
137
138void Location::setTimeZone(const QTimeZone &tz)
139{
140 d.detach();
141 d->timeZone = tz;
142}
143
144QString Location::identifier(const QString &identifierType) const
145{
146 return d->ids.value(identifierType);
147}
148
149void Location::setIdentifier(const QString &identifierType, const QString &id)
150{
151 d.detach();
152 d->ids.insert(identifierType, id);
153}
154
155bool Location::hasIdentifier(const QString &identifierType) const
156{
157 return !d->ids.value(identifierType).isEmpty();
158}
159
161{
162 return d->data.value<RentalVehicleStation>();
163}
164
166{
167 return d->data.value<RentalVehicle>();
168}
169
171{
172 return d->data.value<KPublicTransport::Equipment>();
173}
174
175QHash<QString, QString> Location::identifiers() const
176{
177 return d->ids;
178}
179
180// keep this sorted by key
181struct {
182 const char *key;
183 const char *value;
184} static const name_normalization_map[] = {
185 { "bahnhof", nullptr },
186 { "bhf", nullptr },
187 { "centraal", "central" },
188 { "cs", "central" },
189 { "de", nullptr },
190 { "flughafen", "airport" },
191 { "gare", nullptr },
192 { "hbf", "hauptbahnhof" },
193 { "rer", nullptr },
194 { "st", "saint" },
195 { "str", "strasse" },
196};
197
198static QStringList splitAndNormalizeName(const QString &name)
199{
200 static const QRegularExpression splitRegExp(uR"([, \‍(\)-/\.\[\]])"_s);
201 auto l = name.split(splitRegExp, Qt::SkipEmptyParts);
202
203 for (auto it = l.begin(); it != l.end();) {
204 // ignore single-letter fragments, with the exception of the 'H' used in Denmark
205 // this seem to be mostly transport mode abbreviations (such as 'S' and 'U' in Germany)
206 if ((*it).size() == 1) {
207 it = l.erase(it);
208 continue;
209 }
210
211 *it = (*it).toCaseFolded();
212 const auto b = (*it).toUtf8();
213 const auto entry = std::lower_bound(std::begin(name_normalization_map), std::end(name_normalization_map), b.constData(), [](const auto &lhs, const auto rhs) {
214 return strcmp(lhs.key, rhs) < 0;
215 });
216 if (entry != std::end(name_normalization_map) && strcmp((*entry).key, b.constData()) == 0) {
217 if (!(*entry).value) {
218 it = l.erase(it);
219 continue;
220 }
221 *it = QString::fromUtf8((*entry).value);
222 }
223 ++it;
224 }
225
226 l.removeDuplicates();
227 std::sort(l.begin(), l.end());
228 return l;
229}
230
231static QString stripDiacritics(const QString &s)
232{
233 QString res;
234 res.reserve(s.size());
235
236 // if the character has a canonical decomposition use that and skip the combining diacritic markers following it
237 // see https://en.wikipedia.org/wiki/Unicode_equivalence
238 // see https://en.wikipedia.org/wiki/Combining_character
239 for (const auto &c : s) {
240 if (c.decompositionTag() == QChar::Canonical) {
241 res.push_back(c.decomposition().at(0));
242 } else {
243 res.push_back(c);
244 }
245 }
246
247 return res;
248}
249
250// keep this ordered (see https://en.wikipedia.org/wiki/List_of_Unicode_characters)
251struct {
252 ushort key;
253 const char* replacement;
254} static const transliteration_map[] = {
255 { 0x00DF, "ss" }, // ß
256 { 0x00E4, "ae" }, // ä
257 { 0x00F6, "oe" }, // ö
258 { 0x00F8, "oe" }, // ø
259 { 0x00FC, "ue" }, // ü
260};
261
262static QString applyTransliterations(const QString &s)
263{
264 QString res;
265 res.reserve(s.size());
266
267 for (const auto c : s) {
268 const auto it = std::lower_bound(std::begin(transliteration_map), std::end(transliteration_map), c, [](const auto &lhs, const auto rhs) {
269 return QChar(lhs.key) < rhs;
270 });
271 if (it != std::end(transliteration_map) && QChar((*it).key) == c) {
272 res += QString::fromUtf8((*it).replacement);
273 continue;
274 }
275
276 if (c.decompositionTag() == QChar::Canonical) { // see above
277 res += c.decomposition().at(0);
278 } else {
279 res += c;
280 }
281 }
282
283 return res;
284}
285
286static bool isCompatibleLocationType(Location::Type lhs, Location::Type rhs)
287{
288 return lhs == rhs
289 || (lhs == Location::Place && rhs == Location::Stop)
290 || (rhs == Location::Place && lhs == Location::Stop);
291}
292
293static int isSameDistanceThreshold(Location::Type type)
294{
295 switch (type) {
296 case Location::Place:
297 case Location::Stop:
299 return 25; // meter
301 return 10;
303 return 5;
306 return 3;
307 }
308 Q_UNREACHABLE();
309}
310
311bool Location::isSame(const Location &lhs, const Location &rhs)
312{
313 const auto dist = Location::distance(lhs.latitude(), lhs.longitude(), rhs.latitude(), rhs.longitude());
314 // further than 1km apart is certainly not the same
315 if (lhs.hasCoordinate() && rhs.hasCoordinate() && dist > 1000) {
316 return false;
317 }
318 // incompatible types are also unmergable
319 if (!isCompatibleLocationType(lhs.type(), rhs.type())) {
320 return false;
321 }
322
323 // ids - IFOPT takes priority here due to its special hierarchical handling, but only for stations
324 const auto lhsIfopt = lhs.identifier(IfoptUtil::identifierType());
325 const auto rhsIfopt = rhs.identifier(IfoptUtil::identifierType());
326 if (!lhsIfopt.isEmpty() && !rhsIfopt.isEmpty() && (lhs.type() == Location::Stop || rhs.type() == Location::Stop)) {
327 return IfoptUtil::isSameStopPlace(lhsIfopt, rhsIfopt);
328 }
329
330 const auto lhsIds = lhs.identifiers();
331 bool foundEqualId = false;
332 for (auto it = lhsIds.constBegin(); it != lhsIds.constEnd(); ++it) {
333 const auto rhsId = rhs.identifier(it.key());
334 if (it.value().isEmpty() || rhsId.isEmpty()) {
335 continue;
336 }
337 if (it.value() != rhsId) {
338 return false;
339 } else if (it.value() == rhsId) {
340 foundEqualId = true;
341 }
342 }
343 if (foundEqualId) {
344 return true;
345 }
346
349 return false;
350 }
351 if (lhs.type() == Location::Equipment && lhs.equipment().type() != rhs.equipment().type()) {
352 return false;
353 }
354
355 // name
356 if (isSameName(lhs.name(), rhs.name())) {
357 return true;
358 }
359
360 // TODO consider the address properties here?
361
362 // anything sufficiently close together is assumed to be the same
363 if (lhs.hasCoordinate() && rhs.hasCoordinate() && dist < std::min(isSameDistanceThreshold(lhs.type()), isSameDistanceThreshold(rhs.type()))) {
364 return true;
365 }
366
367 return false;
368}
369
370bool Location::isSameName(const QString &lhs, const QString &rhs)
371{
372 // simple prefix test, before we do the expensive fragment-based comparison below
373 if (lhs.startsWith(rhs, Qt::CaseInsensitive) || rhs.startsWith(lhs, Qt::CaseSensitive)) {
374 return true;
375 }
376
377 const auto lhsNameFragments = splitAndNormalizeName(lhs);
378 const auto rhsNameFragments = splitAndNormalizeName(rhs);
379
380 // first try with stripping diacritics
381 std::vector<QString> lhsNormalized;
382 lhsNormalized.reserve(lhsNameFragments.size());
383 std::transform(lhsNameFragments.begin(), lhsNameFragments.end(), std::back_inserter(lhsNormalized), stripDiacritics);
384 std::sort(lhsNormalized.begin(), lhsNormalized.end());
385 lhsNormalized.erase(std::unique(lhsNormalized.begin(), lhsNormalized.end()), lhsNormalized.end());
386
387 std::vector<QString> rhsNormalized;
388 rhsNormalized.reserve(rhsNameFragments.size());
389 std::transform(rhsNameFragments.begin(), rhsNameFragments.end(), std::back_inserter(rhsNormalized), stripDiacritics);
390 std::sort(rhsNormalized.begin(), rhsNormalized.end());
391 rhsNormalized.erase(std::unique(rhsNormalized.begin(), rhsNormalized.end()), rhsNormalized.end());
392
393 if (lhsNormalized == rhsNormalized) {
394 return true;
395 }
396
397 // if that didn't help, try to apply alternative transliterations of diacritics
398 lhsNormalized.clear();
399 std::transform(lhsNameFragments.begin(), lhsNameFragments.end(), std::back_inserter(lhsNormalized), applyTransliterations);
400 rhsNormalized.clear();
401 std::transform(rhsNameFragments.begin(), rhsNameFragments.end(), std::back_inserter(rhsNormalized), applyTransliterations);
402 return lhsNormalized == rhsNormalized;
403}
404
405static double mergeCoordinate(double lhs, double rhs)
406{
407 if (std::isnan(lhs)) {
408 return rhs;
409 }
410 if (std::isnan(rhs)) {
411 return lhs;
412 }
413
414 return (lhs + rhs) / 2.0;
415}
416
418{
419 Location l(lhs);
420 l.setType(std::max(lhs.type(), rhs.type()));
421
422 // merge identifiers
423 const auto rhsIds = rhs.identifiers();
424 for (auto it = rhsIds.constBegin(); it != rhsIds.constEnd(); ++it) {
425 if (it.key() == IfoptUtil::identifierType()) {
427 continue;
428 }
429 if (lhs.identifier(it.key()).isEmpty()) {
430 l.setIdentifier(it.key(), it.value());
431 }
432 }
433
434 if (!lhs.hasCoordinate()) {
435 l.setCoordinate(rhs.latitude(), rhs.longitude());
436 }
437
438 l.setName(MergeUtil::mergeString(lhs.name(), rhs.name()));
439
440 if (!lhs.d->timeZone.isValid()) {
441 l.setTimeZone(rhs.d->timeZone);
442 }
443
444 l.setLatitude(mergeCoordinate(lhs.latitude(), rhs.latitude()));
445 l.setLongitude(mergeCoordinate(lhs.longitude(), rhs.longitude()));
446
447 l.setStreetAddress(MergeUtil::mergeString(lhs.streetAddress(), rhs.streetAddress()));
448 l.setPostalCode(MergeUtil::mergeString(lhs.postalCode(), rhs.postalCode()));
449 l.setLocality(MergeUtil::mergeString(lhs.locality(), rhs.locality()));
450 l.setRegion(MergeUtil::mergeString(lhs.region(), rhs.region()));
451 l.setCountry(MergeUtil::mergeString(lhs.country(), rhs.country()));
452
453 switch (l.type()) {
454 case Place:
456 case Stop:
457 case Address:
458 break;
460 l.setData(RentalVehicleUtil::merge(lhs.rentalVehicleStation(), rhs.rentalVehicleStation()));
461 break;
462 case RentedVehicle:
463 l.setData(RentalVehicleUtil::merge(lhs.rentalVehicle(), rhs.rentalVehicle()));
464 break;
465 case Equipment:
466 l.setData(EquipmentUtil::merge(lhs.equipment(), rhs.equipment()));
467 break;
468 }
469
470 return l;
471}
472
473// see https://en.wikipedia.org/wiki/Haversine_formula
474double Location::distance(double lat1, double lon1, double lat2, double lon2)
475{
476 const auto degToRad = M_PI / 180.0;
477 const auto earthRadius = 6371000.0; // in meters
478
479 const auto d_lat = (lat1 - lat2) * degToRad;
480 const auto d_lon = (lon1 - lon2) * degToRad;
481
482 const auto a = pow(sin(d_lat / 2.0), 2) + cos(lat1 * degToRad) * cos(lat2 * degToRad) * pow(sin(d_lon / 2.0), 2);
483 return 2.0 * earthRadius * atan2(sqrt(a), sqrt(1.0 - a));
484}
485
486double Location::distance(const Location &lhs, const Location &rhs)
487{
488 if (!lhs.hasCoordinate() || !rhs.hasCoordinate()) {
489 return NAN;
490 }
491 return Location::distance(lhs.latitude(), lhs.longitude(), rhs.latitude(), rhs.longitude());
492}
493
495{
496 auto obj = Json::toJson(loc);
497 if (loc.d->timeZone.isValid()) {
498 obj.insert("timezone"_L1, QString::fromUtf8(loc.d->timeZone.id()));
499 }
500 if (!loc.hasFloorLevel()) {
501 obj.remove("floorLevel"_L1);
502 }
503
504 if (!loc.d->ids.isEmpty()) {
505 QJsonObject ids;
506 for (auto it = loc.d->ids.constBegin(); it != loc.d->ids.constEnd(); ++it) {
507 ids.insert(it.key(), it.value());
508 }
509 obj.insert("identifier"_L1, ids);
510 }
511
512 switch (loc.type()) {
513 case Place:
514 obj.remove("type"_L1);
515 [[fallthrough]];
516 case Address:
517 case Stop:
519 break;
521 obj.insert("rentalVehicleStation"_L1, RentalVehicleStation::toJson(loc.rentalVehicleStation()));
522 break;
523 case RentedVehicle:
524 obj.insert("rentalVehicle"_L1, RentalVehicle::toJson(loc.rentalVehicle()));
525 break;
526 case Equipment:
527 obj.insert("equipment"_L1, Equipment::toJson(loc.equipment()));
528 break;
529 }
530
531 return obj;
532}
533
534QJsonArray Location::toJson(const std::vector<Location> &locs)
535{
536 return Json::toJson(locs);
537}
538
540{
541 switch (d->type) {
542 case Location::Stop:
543 return u"qrc:///org.kde.kpublictransport/assets/images/transport-stop.svg"_s;
549 return equipment().iconName();
551 return u"qrc:///org.kde.kpublictransport/assets/images/transport-mode-car.svg"_s;
553 case Location::Place:
554 break;
555 }
556 return u"map-symbolic"_s;
557}
558
560{
561 auto loc = Json::fromJson<Location>(obj);
562 const auto tz = obj.value("timezone"_L1).toString();
563 if (!tz.isEmpty()) {
564 loc.setTimeZone(QTimeZone(tz.toUtf8()));
565 }
566
567 const auto ids = obj.value("identifier"_L1).toObject();
568 for (auto it = ids.begin(); it != ids.end(); ++it) {
569 loc.setIdentifier(it.key(), it.value().toString());
570 }
571
572 switch (loc.type()) {
573 case Place:
574 case Address:
575 case Stop:
577 break;
579 loc.setData(RentalVehicleStation::fromJson(obj.value("rentalVehicleStation"_L1).toObject()));
580 break;
581 case RentedVehicle:
582 loc.setData(RentalVehicle::fromJson(obj.value("rentalVehicle"_L1).toObject()));
583 break;
584 case Equipment:
585 loc.setData(Equipment::fromJson(obj.value("equipment"_L1).toObject()));
586 break;
587 }
588
589 return loc;
590}
591
592std::vector<Location> Location::fromJson(const QJsonArray &a)
593{
594 return Json::fromJson<Location>(a);
595}
596
597#include "moc_location.cpp"
static KCountrySubdivision fromLocation(float latitude, float longitude)
static KCountry fromLocation(float latitude, float longitude)
Status information about equipment such as elevators or escalators.
Definition equipment.h:25
QString iconName
An icon representing the equipment type.
Definition equipment.h:45
static QJsonObject toJson(const Equipment &equipment)
Serializes one object to JSON.
Definition equipment.cpp:58
static Equipment fromJson(const QJsonObject &obj)
Deserialize an object from JSON.
Definition equipment.cpp:63
bool hasFloorLevel
Indicates whether the floor level is set.
Definition location.h:76
Q_INVOKABLE QString identifier(const QString &identifierType) const
Location identifiers.
Definition location.cpp:144
KPublicTransport::Equipment equipment
Equipment information, if applicable for this location.
Definition location.h:88
KPublicTransport::RentalVehicle rentalVehicle
Rental vehicle information, if applicable for this location.
Definition location.h:86
double longitude
Longitude of the location, in degree, NaN if unknown.
Definition location.h:56
static Location fromJson(const QJsonObject &obj)
Deserialize a Location object from JSON.
Definition location.cpp:559
QTimeZone timeZone() const
The timezone this location is in, if known.
Definition location.cpp:125
KPublicTransport::RentalVehicleStation rentalVehicleStation
Rental vehicle dock information, if applicable for this location.
Definition location.h:84
QString region
Region (as in ISO 3166-2) of the location, if known.
Definition location.h:65
static bool isSameName(const QString &lhs, const QString &rhs)
Checks if two location names refer to the same location.
Definition location.cpp:370
Type
Type of location.
Definition location.h:35
@ RentedVehicleStation
a pick-up/drop-off point for dock-based rental bike/scooter systems
Definition location.h:38
@ Place
a location that isn't of any specific type
Definition location.h:36
@ RentedVehicle
a free-floating rental bike/scooter
Definition location.h:39
@ Equipment
elevator/escalator
Definition location.h:40
@ Address
postal addresses
Definition location.h:42
@ Stop
a public transport stop (train station, bus stop, etc)
Definition location.h:37
@ CarpoolPickupDropoff
a pickup or dropoff location for a carpool trip
Definition location.h:41
static QJsonObject toJson(const Location &loc)
Serializes one Location object to JSON.
Definition location.cpp:494
QString iconName
Icon representing the location type.
Definition location.h:93
static double distance(double lat1, double lon1, double lat2, double lon2)
Compute the distance between two geo coordinates, in meters.
Definition location.cpp:474
static Location merge(const Location &lhs, const Location &rhs)
Merge two departure instances.
Definition location.cpp:417
double latitude
Latitude of the location, in degree, NaN if unknown.
Definition location.h:54
QString streetAddress
Street address of the location, if known.
Definition location.h:59
QString country
Country of the location as ISO 3166-1 alpha 2 code, if known.
Definition location.h:67
QString locality
Locality/city of the location, if known.
Definition location.h:63
QString name
Human-readable name of the location.
Definition location.h:52
QString postalCode
Postal code of the location, if known.
Definition location.h:61
bool isEmpty() const
Returns true if this is an default-constructed location object not specifying any location.
Definition location.cpp:120
Type type
Location type.
Definition location.h:49
static bool isSame(const Location &lhs, const Location &rhs)
Checks if to instances refer to the same location (which does not necessarily mean they are exactly e...
Definition location.cpp:311
Additional information for a vehicle renting station, attached to Location objects.
static RentalVehicleStation fromJson(const QJsonObject &obj)
Deserialize an object from JSON.
QString iconName
Icon representing this rental vehicle station.
static bool isSame(const RentalVehicleStation &lhs, const RentalVehicleStation &rhs)
Checks if two instances refer to the same station.
bool isValid
Not an empty/default constructed object.
static QJsonObject toJson(const RentalVehicleStation &station)
Serializes one object to JSON.
An individual rental vehicle used on a JourneySection, ie.
static RentalVehicle fromJson(const QJsonObject &obj)
Deserialize an object from JSON.
static QJsonObject toJson(const RentalVehicle &vehicle)
Serializes one object to JSON.
QString vehicleTypeIconName
Icon representing the vehicle type.
bool isSameStopPlace(QStringView lhs, QStringView rhs)
Checks whether two valid IFOPT ids refer to the same stop place.
Definition ifoptutil.cpp:58
QString identifierType()
The identifier type for use in Location::identifer for IFOPT ids.
Definition ifoptutil.cpp:83
QStringView merge(QStringView lhs, QStringView rhs)
Merge two IFOPT ids that refer to the same stop place while retaining the maximum level of detail.
Definition ifoptutil.cpp:63
QStringView countryCode(QStringView coachNumber)
Returns the UIC country code from coachNumber.
Query operations and data types for accessing realtime public transport information from online servi...
KI18NLOCALEDATA_EXPORT const char * fromLocation(float latitude, float longitude)
iterator insert(QLatin1StringView key, const QJsonValue &value)
QJsonValue value(QLatin1StringView key) const const
QJsonObject toObject() const const
QString toString() const const
const QChar at(qsizetype position) const const
QString fromUtf8(QByteArrayView str)
QString & insert(qsizetype position, QChar ch)
bool isEmpty() const const
void push_back(QChar ch)
void reserve(qsizetype size)
qsizetype size() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString toString() const const
CaseInsensitive
SkipEmptyParts
QByteArray id() const const
bool isValid() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Nov 29 2024 11:57:19 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.