001/*
002 * Java GPX Library (jpx-3.1.0).
003 * Copyright (c) 2016-2023 Franz Wilhelmstötter
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 *
017 * Author:
018 *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019 */
020package io.jenetics.jpx;
021
022import static java.lang.String.format;
023import static java.util.Objects.hash;
024import static java.util.Objects.requireNonNull;
025import static io.jenetics.jpx.Format.toIntString;
026import static io.jenetics.jpx.Lists.copyOf;
027import static io.jenetics.jpx.Lists.copyTo;
028
029import java.io.DataInput;
030import java.io.DataOutput;
031import java.io.IOException;
032import java.io.InvalidObjectException;
033import java.io.ObjectInputStream;
034import java.io.Serial;
035import java.io.Serializable;
036import java.net.URI;
037import java.util.ArrayList;
038import java.util.Iterator;
039import java.util.List;
040import java.util.Objects;
041import java.util.Optional;
042import java.util.function.Consumer;
043import java.util.function.Function;
044import java.util.function.Predicate;
045import java.util.stream.Stream;
046
047import org.w3c.dom.Document;
048
049import io.jenetics.jpx.GPX.Version;
050
051/**
052 * Represents a route - an ordered list of way-points representing a series of
053 * turn points leading to a destination.
054 * <p>
055 * Create a new route via the builder:
056 * <pre>{@code
057 * final Route route = Route.builder()
058 *     .name("Route 1")
059 *     .description("Fancy mountain-bike tour.")
060 *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
061 *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
062 *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
063 *     .build();
064 * }</pre>
065 *
066 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
067 * @version 1.5
068 * @since 1.0
069 */
070public final class Route implements Iterable<WayPoint>, Serializable {
071
072        @Serial
073        private static final long serialVersionUID = 2L;
074
075        private final String _name;
076        private final String _comment;
077        private final String _description;
078        private final String _source;
079        private final List<Link> _links;
080        private final UInt _number;
081        private final String _type;
082        private final Document _extensions;
083        private final List<WayPoint> _points;
084
085        /**
086         * Create a new {@code Route} with the given parameters and way-points.
087         *
088         * @param name the GPS name of the route
089         * @param comment the GPS comment of the route
090         * @param description the Text description of route for user. Not sent to GPS.
091         * @param source the source of data. Included to give user some idea of
092         *        reliability and accuracy of data.
093         * @param links the links to external information about the route
094         * @param number the GPS route number
095         * @param type the type (classification) of the route
096         * @param extensions the extensions document
097         * @param points the sequence of route points
098         */
099        private Route(
100                final String name,
101                final String comment,
102                final String description,
103                final String source,
104                final List<Link> links,
105                final UInt number,
106                final String type,
107                final Document extensions,
108                final List<WayPoint> points
109        ) {
110                _name = name;
111                _comment = comment;
112                _description = description;
113                _source = source;
114                _links = copyOf(links);
115                _number = number;
116                _type = type;
117                _extensions = extensions;
118                _points = copyOf(points);
119        }
120
121        /**
122         * Return the route name.
123         *
124         * @return the route name
125         */
126        public Optional<String> getName() {
127                return Optional.ofNullable(_name);
128        }
129
130        /**
131         * Return the GPS comment of the route.
132         *
133         * @return the GPS comment of the route
134         */
135        public Optional<String> getComment() {
136                return Optional.ofNullable(_comment);
137        }
138
139        /**
140         * Return the Text description of route for user. Not sent to GPS.
141         *
142         * @return the Text description of route for user. Not sent to GPS
143         */
144        public Optional<String> getDescription() {
145                return Optional.ofNullable(_description);
146        }
147
148        /**
149         * Return the source of data. Included to give user some idea of reliability
150         * and accuracy of data.
151         *
152         * @return the source of data
153         */
154        public Optional<String> getSource() {
155                return Optional.ofNullable(_source);
156        }
157
158        /**
159         * Return the links to external information about the route.
160         *
161         * @return the links to external information about the route
162         */
163        public List<Link> getLinks() {
164                return _links;
165        }
166
167        /**
168         * Return the GPS route number.
169         *
170         * @return the GPS route number
171         */
172        public Optional<UInt> getNumber() {
173                return Optional.ofNullable(_number);
174        }
175
176        /**
177         * Return the type (classification) of the route.
178         *
179         * @return the type (classification) of the route
180         */
181        public Optional<String> getType() {
182                return Optional.ofNullable(_type);
183        }
184
185        /**
186         * Return the sequence of route points.
187         *
188         * @return the sequence of route points
189         */
190        public List<WayPoint> getPoints() {
191                return _points;
192        }
193
194        /**
195         * Return a stream of {@link WayPoint} objects this route contains.
196         *
197         * @return a stream of {@link WayPoint} objects this route contains
198         */
199        public Stream<WayPoint> points() {
200                return _points.stream();
201        }
202
203        /**
204         * Return the (cloned) extensions document. The root element of the returned
205         * document has the name {@code extensions}.
206         * <pre>{@code
207         * <extensions>
208         *     ...
209         * </extensions>
210         * }</pre>
211         *
212         * @since 1.5
213         *
214         * @return the extensions document
215         * @throws org.w3c.dom.DOMException if the document could not be cloned,
216         *         because of an erroneous XML configuration
217         */
218        public Optional<Document> getExtensions() {
219                return Optional.ofNullable(_extensions).map(XML::clone);
220        }
221
222        @Override
223        public Iterator<WayPoint> iterator() {
224                return _points.iterator();
225        }
226
227        /**
228         * Convert the <em>immutable</em> route object into a <em>mutable</em>
229         * builder initialized with the current route values.
230         *
231         * @since 1.1
232         *
233         * @return a new route builder initialized with the values of {@code this}
234         *         route
235         */
236        public Builder toBuilder() {
237                return builder()
238                        .name(_name)
239                        .cmt(_comment)
240                        .desc(_description)
241                        .src(_source)
242                        .links(_links)
243                        .number(_number)
244                        .extensions(_extensions)
245                        .points(_points);
246        }
247
248        /**
249         * Return {@code true} if all route properties are {@code null} or empty.
250         *
251         * @return {@code true} if all route properties are {@code null} or empty
252         */
253        public boolean isEmpty() {
254                return _name == null &&
255                        _comment == null &&
256                        _description == null &&
257                        _source == null &&
258                        _links.isEmpty() &&
259                        _number == null &&
260                        _extensions == null &&
261                        _points.isEmpty();
262        }
263
264        /**
265         * Return {@code true} if not all route properties are {@code null} or empty.
266         *
267         * @since 1.1
268         *
269         * @return {@code true} if not all route properties are {@code null} or empty
270         */
271        public boolean nonEmpty() {
272                return !isEmpty();
273        }
274
275        @Override
276        public int hashCode() {
277                return hash(
278                        _name,
279                        _comment,
280                        _description,
281                        _source,
282                        _type,
283                        Lists.hashCode(_links),
284                        _number,
285                        _points
286                );
287        }
288
289        @Override
290        public boolean equals(final Object obj) {
291                return obj == this ||
292                        obj instanceof Route route &&
293                        Objects.equals(route._name, _name) &&
294                        Objects.equals(route._comment, _comment) &&
295                        Objects.equals(route._description, _description) &&
296                        Objects.equals(route._source, _source) &&
297                        Objects.equals(route._type, _type) &&
298                        Lists.equals(route._links, _links) &&
299                        Objects.equals(route._number, _number) &&
300                        Objects.equals(route._points, _points);
301        }
302
303        @Override
304        public String toString() {
305                return format("Route[name=%s, points=%s]", _name, _points.size());
306        }
307
308        /* *************************************************************************
309         *  Static object creation methods
310         * ************************************************************************/
311
312        /**
313         * Return a new {@code Route} builder object.
314         *
315         * @return a new {@code Route} builder object
316         */
317        public static Builder builder() {
318                return new Builder();
319        }
320
321        /**
322         * Builder class for building {@code Route} objects.
323         * <pre>{@code
324         * final Route route = Route.builder()
325         *     .name("Route 1")
326         *     .description("Fancy mountain-bike tour.")
327         *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(160))
328         *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(161))
329         *     .addPoint(p -> p.lat(48.2081743).lon(16.3738189).ele(162))))
330         *     .build();
331         * }</pre>
332         */
333        public static final class Builder implements Filter<WayPoint, Route> {
334
335                private String _name;
336                private String _comment;
337                private String _description;
338                private String _source;
339                private final List<Link> _links = new ArrayList<>();
340                private UInt _number;
341                private String _type;
342                private Document _extensions;
343                private final List<WayPoint> _points = new ArrayList<>();
344
345                private Builder() {
346                }
347
348                /**
349                 * Set the route name.
350                 *
351                 * @param name the route name.
352                 * @return {@code this} {@code Builder} for method chaining
353                 */
354                public Builder name(final String name) {
355                        _name = name;
356                        return this;
357                }
358
359                /**
360                 * Return the current name value.
361                 *
362                 * @since 1.1
363                 *
364                 * @return the current name value
365                 */
366                public Optional<String> name() {
367                        return Optional.ofNullable(_name);
368                }
369
370                /**
371                 * Set the route comment.
372                 *
373                 * @param comment the route comment
374                 * @return {@code this} {@code Builder} for method chaining
375                 */
376                public Builder cmt(final String comment) {
377                        _comment = comment;
378                        return this;
379                }
380
381                /**
382                 * Return the current comment value.
383                 *
384                 * @since 1.1
385                 *
386                 * @return the current comment value
387                 */
388                public Optional<String> cmt() {
389                        return Optional.ofNullable(_comment);
390                }
391
392                /**
393                 * Set the route description.
394                 *
395                 * @param description the route description
396                 * @return {@code this} {@code Builder} for method chaining
397                 */
398                public Builder desc(final String description) {
399                        _description = description;
400                        return this;
401                }
402
403                /**
404                 * Return the current description value.
405                 *
406                 * @since 1.1
407                 *
408                 * @return the current description value
409                 */
410                public Optional<String> desc() {
411                        return Optional.ofNullable(_description);
412                }
413
414                /**
415                 * Set the source of the data. Included to give user some idea of
416                 * reliability and accuracy of data.
417                 *
418                 * @param source the source of the data
419                 * @return {@code this} {@code Builder} for method chaining
420                 */
421                public Builder src(final String source) {
422                        _source = source;
423                        return this;
424                }
425
426                /**
427                 * Return the current source value.
428                 *
429                 * @since 1.1
430                 *
431                 * @return the current source value
432                 */
433                public Optional<String> src() {
434                        return Optional.ofNullable(_source);
435                }
436
437                /**
438                 * Set the links to additional information about the route. The link
439                 * list may be {@code null}.
440                 *
441                 * @param links the links to additional information about the route
442                 * @return {@code this} {@code Builder} for method chaining
443                 * @throws NullPointerException if one of the links in the list is
444                 *         {@code null}
445                 */
446                public Builder links(final List<Link> links) {
447                        copyTo(links, _links);
448                        return this;
449                }
450
451                /**
452                 * Set the links to external information about the route.
453                 *
454                 * @param link the links to external information about the route.
455                 * @return {@code this} {@code Builder} for method chaining
456                 * @throws NullPointerException if the given {@code link} is {@code null}
457                 */
458                public Builder addLink(final Link link) {
459                        _links.add(requireNonNull(link));
460                        return this;
461                }
462
463                /**
464                 * Set the links to external information about the route.
465                 *
466                 * @param href the links to external information about the route.
467                 * @return {@code this} {@code Builder} for method chaining
468                 * @throws NullPointerException if the given {@code href} is
469                 *         {@code null}
470                 * @throws IllegalArgumentException if the given {@code href} is not a
471                 *         valid URL
472                 */
473                public Builder addLink(final String href) {
474                        _links.add(Link.of(href));
475                        return this;
476                }
477
478                /**
479                 * Return the current links. The returned link list is mutable.
480                 *
481                 * @since 1.1
482                 *
483                 * @return the current links
484                 */
485                public List<Link> links() {
486                        return new NonNullList<>(_links);
487                }
488
489                /**
490                 * Set the GPS route number.
491                 *
492                 * @param number the GPS route number
493                 * @return {@code this} {@code Builder} for method chaining
494                 */
495                public Builder number(final UInt number) {
496                        _number = number;
497                        return this;
498                }
499
500                /**
501                 * Set the GPS route number.
502                 *
503                 * @param number the GPS route number
504                 * @return {@code this} {@code Builder} for method chaining
505                 */
506                public Builder number(final int number) {
507                        _number = UInt.of(number);
508                        return this;
509                }
510
511                /**
512                 * Return the current number value.
513                 *
514                 * @since 1.1
515                 *
516                 * @return the current number value
517                 */
518                public Optional<UInt> number() {
519                        return Optional.ofNullable(_number);
520                }
521
522                /**
523                 * Set the type (classification) of the route.
524                 *
525                 * @param type the type (classification) of the route.
526                 * @return {@code this} {@code Builder} for method chaining
527                 */
528                public Builder type(final String type) {
529                        _type = type;
530                        return this;
531                }
532
533                /**
534                 * Return the current type value.
535                 *
536                 * @since 1.1
537                 *
538                 * @return the current type value
539                 */
540                public Optional<String> type() {
541                        return Optional.ofNullable(_type);
542                }
543
544                /**
545                 * Sets the extensions object, which may be {@code null}. The root
546                 * element of the extensions document must be {@code extensions}.
547                 * <pre>{@code
548                 * <extensions>
549                 *     ...
550                 * </extensions>
551                 * }</pre>
552                 *
553                 * @since 1.5
554                 *
555                 * @param extensions the document
556                 * @return {@code this} {@code Builder} for method chaining
557                 * @throws IllegalArgumentException if the root element is not the
558                 *         an {@code extensions} node
559                 */
560                public Builder extensions(final Document extensions) {
561                        _extensions = XML.checkExtensions(extensions);
562                        return this;
563                }
564
565                /**
566                 * Return the current extensions
567                 *
568                 * @since 1.5
569                 *
570                 * @return the extensions document
571                 */
572                public Optional<Document> extensions() {
573                        return Optional.ofNullable(_extensions);
574                }
575
576                /**
577                 * Sets the way-points of the route. The way-point list may be
578                 * {@code null}.
579                 *
580                 * @param points the way-points
581                 * @return {@code this} {@code Builder} for method chaining
582                 * @throws NullPointerException if one of the way-points is {@code null}
583                 */
584                public Builder points(final List<WayPoint> points) {
585                        copyTo(points, _points);
586                        return this;
587                }
588
589                /**
590                 * Adds a way-point to the route.
591                 *
592                 * @param point the way-point which is added to the route
593                 * @return {@code this} {@code Builder} for method chaining
594                 * @throws NullPointerException if the {@code point} is {@code null}
595                 */
596                public Builder addPoint(final WayPoint point) {
597                        _points.add(requireNonNull(point));
598                        return this;
599                }
600
601                /**
602                 * Add a new way-point via the given {@code WayPoint.Builder} class.
603                 *
604                 * @param point the way-point builder
605                 * @return {@code this} {@code Builder} for method chaining
606                 */
607                public Builder addPoint(final Consumer<? super WayPoint.Builder> point) {
608                        final WayPoint.Builder builder = WayPoint.builder();
609                        point.accept(builder);
610                        return addPoint(builder.build());
611                }
612
613                /**
614                 * Return the current way-points. The returned list is mutable.
615                 *
616                 * @since 1.1
617                 *
618                 * @return the current, mutable way-point list
619                 */
620                public List<WayPoint> points() {
621                        return new NonNullList<>(_points);
622                }
623
624                @Override
625                public Builder filter(final Predicate<? super WayPoint> predicate) {
626                        points(_points.stream().filter(predicate).toList());
627                        return this;
628                }
629
630                @Override
631                public Builder map(
632                        final Function<? super WayPoint, ? extends WayPoint> mapper
633                ) {
634                        points(
635                                _points.stream()
636                                        .map(mapper)
637                                        .map(WayPoint.class::cast)
638                                        .toList()
639                        );
640                        return this;
641                }
642
643                @Override
644                public Builder flatMap(
645                        final Function<
646                                ? super WayPoint,
647                                ? extends List<WayPoint>> mapper
648                ) {
649                        points(
650                                _points.stream()
651                                        .flatMap(wp -> mapper.apply(wp).stream())
652                                        .toList()
653                        );
654                        return this;
655                }
656
657                @Override
658                public Builder listMap(
659                        final Function<
660                                ? super List<WayPoint>,
661                                ? extends List<WayPoint>> mapper
662                ) {
663                        points(mapper.apply(_points));
664                        return this;
665                }
666
667                /**
668                 * Create a new {@code Route} object with the set values.
669                 *
670                 * @return a new {@code Route} object with the set values
671                 */
672                @Override
673                public Route build() {
674                        return of(
675                                _name,
676                                _comment,
677                                _description,
678                                _source,
679                                _links,
680                                _number,
681                                _type,
682                                _extensions,
683                                _points
684                        );
685                }
686
687        }
688
689        /* *************************************************************************
690         *  Static object creation methods
691         * ************************************************************************/
692
693        /**
694         * Create a new {@code Route} with the given parameters and way-points.
695         *
696         * @since 1.5
697         *
698         * @param name the GPS name of the route
699         * @param comment the GPS comment of the route
700         * @param description the Text description of route for user. Not sent to GPS.
701         * @param source the source of data. Included to give user some idea of
702         *        reliability and accuracy of data.
703         * @param links the links to external information about the route
704         * @param number the GPS route number
705         * @param type the type (classification) of the route
706         * @param extensions the extensions document
707         * @param points the sequence of route points
708         * @return a new route object with the given parameters
709         */
710        public static Route of(
711                final String name,
712                final String comment,
713                final String description,
714                final String source,
715                final List<Link> links,
716                final UInt number,
717                final String type,
718                final Document extensions,
719                final List<WayPoint> points
720        ) {
721                return new Route(
722                        name,
723                        comment,
724                        description,
725                        source,
726                        links,
727                        number,
728                        type,
729                        XML.extensions(XML.clone(extensions)),
730                        points
731                );
732        }
733
734        /**
735         * Create a new {@code Route} with the given parameters and way-points.
736         *
737         * @param name the GPS name of the route
738         * @param comment the GPS comment of the route
739         * @param description the Text description of route for user. Not sent to GPS.
740         * @param source the source of data. Included to give user some idea of
741         *        reliability and accuracy of data.
742         * @param links the links to external information about the route
743         * @param number the GPS route number
744         * @param type the type (classification) of the route
745         * @param points the sequence of route points
746         * @return a new route object with the given parameters
747         */
748        public static Route of(
749                final String name,
750                final String comment,
751                final String description,
752                final String source,
753                final List<Link> links,
754                final UInt number,
755                final String type,
756                final List<WayPoint> points
757        ) {
758                return of(
759                        name,
760                        comment,
761                        description,
762                        source,
763                        links,
764                        number,
765                        type,
766                        null,
767                        points
768                );
769        }
770
771        /**
772         * Create a new {@code Route} with the given parameters and way-points.
773         *
774         * @param name the GPS name of the route
775         * @param points the sequence of route points
776         * @return a new route object with the given parameters
777         */
778        public static Route of(
779                final String name,
780                final List<WayPoint> points
781        ) {
782                return of(
783                        name,
784                        null,
785                        null,
786                        null,
787                        null,
788                        null,
789                        null,
790                        null,
791                        points
792                );
793        }
794
795        /**
796         * Create a new {@code Route} with the given parameters and way-points.
797         *
798         * @param points the sequence of route points
799         * @return a new route object with the given parameters
800         */
801        public static Route of(final List<WayPoint> points) {
802                return of(
803                        null,
804                        null,
805                        null,
806                        null,
807                        null,
808                        null,
809                        null,
810                        null,
811                        points
812                );
813        }
814
815
816        /* *************************************************************************
817         *  Java object serialization
818         * ************************************************************************/
819
820        @Serial
821        private Object writeReplace() {
822                return new SerialProxy(SerialProxy.ROUTE, this);
823        }
824
825        @Serial
826        private void readObject(final ObjectInputStream stream)
827                throws InvalidObjectException
828        {
829                throw new InvalidObjectException("Serialization proxy required.");
830        }
831
832        void write(final DataOutput out) throws IOException {
833                IO.writeNullableString(_name, out);
834                IO.writeNullableString(_comment, out);
835                IO.writeNullableString(_description, out);
836                IO.writeNullableString(_source, out);
837                IO.writes(_links, Link::write, out);
838                IO.writeNullable(_number, UInt::write, out);
839                IO.writeNullableString(_type, out);
840                IO.writeNullable(_extensions, IO::write, out);
841                IO.writes(_points, WayPoint::write, out);
842        }
843
844        static Route read(final DataInput in) throws IOException {
845                return new Route(
846                        IO.readNullableString(in),
847                        IO.readNullableString(in),
848                        IO.readNullableString(in),
849                        IO.readNullableString(in),
850                        IO.reads(Link::read, in),
851                        IO.readNullable(UInt::read, in),
852                        IO.readNullableString(in),
853                        IO.readNullable(IO::readDoc, in),
854                        IO.reads(WayPoint::read, in)
855                );
856        }
857
858        /* *************************************************************************
859         *  XML stream object serialization
860         * ************************************************************************/
861
862        private static String url(final Route route) {
863                return route.getLinks().isEmpty()
864                        ? null
865                        : route.getLinks().get(0).getHref().toString();
866        }
867
868        private static String urlname(final Route route) {
869                return route.getLinks().isEmpty()
870                        ? null
871                        : route.getLinks().get(0).getText().orElse(null);
872        }
873
874        // Define the needed writers for the different versions.
875        private static XMLWriters<Route>
876        writers(final Function<? super Number, String> formatter) {
877                return new XMLWriters<Route>()
878                        .v00(XMLWriter.elem("name").map(r -> r._name))
879                        .v00(XMLWriter.elem("cmt").map(r -> r._comment))
880                        .v00(XMLWriter.elem("desc").map(r -> r._description))
881                        .v00(XMLWriter.elem("src").map(r -> r._source))
882                        .v11(XMLWriter.elems(Link.WRITER).map(r -> r._links))
883                        .v10(XMLWriter.elem("url").map(Route::url))
884                        .v10(XMLWriter.elem("urlname").map(Route::urlname))
885                        .v00(XMLWriter.elem("number").map(r -> toIntString(r._number)))
886                        .v00(XMLWriter.elem("type").map(r -> r._type))
887                        .v00(XMLWriter.doc("extensions").map(gpx -> gpx._extensions))
888                        .v10(XMLWriter.elems(WayPoint.xmlWriter(Version.V10, "rtept", formatter)).map(r -> r._points))
889                        .v11(XMLWriter.elems(WayPoint.xmlWriter(Version.V11, "rtept", formatter)).map(r -> r._points));
890        }
891
892
893        // Define the needed readers for the different versions.
894        private static XMLReaders
895        readers(final Function<? super String, Length> lengthParser) {
896                return new XMLReaders()
897                        .v00(XMLReader.elem("name"))
898                        .v00(XMLReader.elem("cmt"))
899                        .v00(XMLReader.elem("desc"))
900                        .v00(XMLReader.elem("src"))
901                        .v11(XMLReader.elems(Link.READER))
902                        .v10(XMLReader.elem("url").map(Format::parseURI))
903                        .v10(XMLReader.elem("urlname"))
904                        .v00(XMLReader.elem("number").map(UInt::parse))
905                        .v00(XMLReader.elem("type"))
906                        .v00(XMLReader.doc("extensions"))
907                        .v10(XMLReader.elems(WayPoint.xmlReader(Version.V10, "rtept", lengthParser)))
908                        .v11(XMLReader.elems(WayPoint.xmlReader(Version.V11, "rtept", lengthParser)));
909        }
910
911        static XMLWriter<Route> xmlWriter(
912                final Version version,
913                final Function<? super Number, String> formatter
914        ) {
915                return XMLWriter.elem("rte", writers(formatter).writers(version));
916        }
917
918        static XMLReader<Route> xmlReader(
919                final Version version,
920                final Function<? super String, Length> lengthParser
921        ) {
922                return XMLReader.elem(
923                        version == Version.V10 ? Route::toRouteV10 : Route::toRouteV11,
924                        "rte",
925                        readers(lengthParser).readers(version)
926                );
927        }
928
929        @SuppressWarnings("unchecked")
930        private static Route toRouteV11(final Object[] v) {
931                return new Route(
932                        (String)v[0],
933                        (String)v[1],
934                        (String)v[2],
935                        (String)v[3],
936                        (List<Link>)v[4],
937                        (UInt)v[5],
938                        (String)v[6],
939                        XML.extensions((Document)v[7]),
940                        (List<WayPoint>)v[8]
941                );
942        }
943
944        @SuppressWarnings("unchecked")
945        private static Route toRouteV10(final Object[] v) {
946                return new Route(
947                        (String)v[0],
948                        (String)v[1],
949                        (String)v[2],
950                        (String)v[3],
951                        v[4] != null
952                                ? List.of(Link.of((URI)v[4], (String)v[5], null))
953                                : null,
954                        (UInt)v[6],
955                        (String)v[7],
956                        XML.extensions((Document)v[8]),
957                        (List<WayPoint>)v[9]
958                );
959        }
960
961}