diff --git a/lib/src/kml_reader.dart b/lib/src/kml_reader.dart index 3bb88b4..8dc0dcc 100644 --- a/lib/src/kml_reader.dart +++ b/lib/src/kml_reader.dart @@ -771,10 +771,10 @@ class KmlReader { await _readEnum(iterator, val.name, ColorMode.values); break; case KmlTag.fill: - polyStyle.fill = await _readInt(iterator, val.name, 16); + polyStyle.fill = await _readInt(iterator, val.name); break; case KmlTag.outline: - polyStyle.outline = await _readInt(iterator, val.name, 16); + polyStyle.outline = await _readInt(iterator, val.name); break; } } @@ -887,10 +887,12 @@ class KmlReader { if (val is XmlStartElementEvent) { switch (val.name) { case KmlTag.bgColor: - balloonStyle.bgColor = await _readInt(iterator, val.name, 16); + balloonStyle.bgColor = await _readInt(iterator, val.name, 16) + ?? balloonStyle.bgColor; break; case KmlTag.textColor: - balloonStyle.textColor = await _readInt(iterator, val.name, 16); + balloonStyle.textColor = await _readInt(iterator, val.name, 16) + ?? balloonStyle.textColor; break; case KmlTag.text: balloonStyle.text = await _readString(iterator, val.name) ?? ''; diff --git a/lib/src/kml_writer.dart b/lib/src/kml_writer.dart index a7cd6ad..35f2900 100644 --- a/lib/src/kml_writer.dart +++ b/lib/src/kml_writer.dart @@ -1,11 +1,13 @@ import 'package:xml/xml.dart'; import 'model/geo_object.dart'; +import 'model/geo_style.dart'; import 'model/geoxml.dart'; import 'model/gpx_tag.dart'; import 'model/kml_tag.dart'; import 'model/link.dart'; import 'model/metadata.dart'; +import 'model/polygon.dart'; import 'model/rte.dart'; import 'model/trk.dart'; import 'model/wpt.dart'; @@ -53,23 +55,27 @@ class KmlWriter { _writeMetadata(builder, geoXml.metadata!); } + for (final style in geoXml.styles) { + _writeStyle(builder, style); + } + for (final wpt in geoXml.wpts) { - _writePoint(builder, KmlTag.placemark, wpt); + _writePoint(builder, wpt, geoXml); } - // for (final polygon in gpx.polygons) { - // _writePolygon(builder, polygon); - // } + for (final polygon in geoXml.polygons) { + _writePolygon(builder, polygon, geoXml); + } for (final rte in geoXml.rtes) { - _writeTrackRoute(builder, rte); + _writeTrackRoute(builder, rte, geoXml); } for (final trk in geoXml.trks) { if (trk.trksegs .expand((trkseg) => trkseg.trkpts) .any((element) => element.time == null)) { - _writeTrackRoute(builder, trk); + _writeTrackRoute(builder, trk, geoXml); } else { _writeTrack(builder, trk); } @@ -113,12 +119,21 @@ class KmlWriter { }); } - void _writeTrackRoute(XmlBuilder builder, GeoObject item) { + void _writeTrackRoute(XmlBuilder builder, GeoObject item, GeoXml geoXml) { builder.element(KmlTag.placemark, nest: () { _writeElement(builder, GpxTag.name, item.name); _writeElement(builder, GpxTag.desc, item.desc); _writeAtomLinks(builder, item.links); + if (item.style != null) { + if (item.style!.id != null && item.style!.id!.isNotEmpty + && geoXml.styles.contains(item.style)) { + _writeElement(builder, KmlTag.styleUrl, '#${item.style!.id}'); + } else { + _writeStyle(builder, item.style!); + } + } + builder.element(KmlTag.extendedData, nest: () { _writeExtendedElement(builder, GpxTag.comment, item.cmt); _writeExtendedElement(builder, GpxTag.type, item.type); @@ -189,57 +204,136 @@ class KmlWriter { }); } - // void _writePolygon(XmlBuilder builder, Polygon polygon) { - // builder.element(KmlTag.placemark, nest: () { - // _writeElement(builder, KmlTag.name, polygon.name); - // _writeElement(builder, KmlTag.desc, polygon.desc); - // _writeAtomLinks(builder, polygon.links); - // - // // Style the polygon. - // builder.element(KmlTag.style, nest: () { - // builder.element(KmlTag.lineStyle, nest: () { - // _writeElement(builder, KmlTag.color, - // polygon.outlineColor.toRadixString(16)); - // _writeElement(builder, KmlTag.width, polygon.outlineWidth); - // }); - // builder.element(KmlTag.polyStyle, nest: () { - // _writeElement( - // builder, KmlTag.color, polygon.fillColor.toRadixString(16)); - // _writeElement(builder, KmlTag.outline, 0); - // }); - // }); - // - // builder.element(KmlTag.extendedData, nest: () { - // _writeExtendedElement(builder, GpxTag.comment, polygon.cmt); - // _writeExtendedElement(builder, GpxTag.type, polygon.type); - // - // _writeExtendedElement(builder, GpxTag.src, polygon.src); - // _writeExtendedElement(builder, GpxTag.number, polygon.number); - // }); - // - // builder.element(KmlTag.polygon, nest: () { - // builder.element(KmlTag.outerBoundaryIs, nest: () { - // builder.element(KmlTag.linearRing, nest: () { - // _writeElement( - // builder, - // KmlTag.coordinates, - // polygon.points - // .map((wpt) => [wpt.lon, wpt.lat].join(',')) - // .join('\n')); - // }); - // }); - // }); - // }); - // } - - void _writePoint(XmlBuilder builder, String tagName, Wpt wpt) { - builder.element(tagName, nest: () { + void _writePolygon(XmlBuilder builder, Polygon polygon, GeoXml geoXml) { + builder.element(KmlTag.placemark, nest: () { + _writeElement(builder, KmlTag.name, polygon.name); + _writeElement(builder, KmlTag.desc, polygon.desc); + _writeAtomLinks(builder, polygon.links); + + // Style the polygon. + if (polygon.style != null) { + if (polygon.style!.id != null && polygon.style!.id!.isNotEmpty + && geoXml.styles.contains(polygon.style)) { + _writeElement(builder, KmlTag.styleUrl, '#${polygon.style!.id}'); + } else { + _writeStyle(builder, polygon.style!); + } + } + + builder.element(KmlTag.extendedData, nest: () { + _writeExtendedElement(builder, GpxTag.comment, polygon.cmt); + _writeExtendedElement(builder, GpxTag.type, polygon.type); + + _writeExtendedElement(builder, GpxTag.src, polygon.src); + _writeExtendedElement(builder, GpxTag.number, polygon.number); + }); + + builder.element(KmlTag.polygon, nest: () { + builder.element(KmlTag.outerBoundaryIs, nest: () { + builder.element(KmlTag.linearRing, nest: () { + _writeElement( + builder, + KmlTag.coordinates, + polygon.points + .map((wpt) => [wpt.lon, wpt.lat].join(',')) + .join('\n')); + }); + }); + }); + }); + } + + void _writeStyle(XmlBuilder builder, GeoStyle style) { + builder.element(KmlTag.style, + attributes: style.id != null ? {KmlTag.id: style.id!} : {}, nest: () { + if (style.lineStyle != null) { + builder.element(KmlTag.lineStyle, nest: () { + _writeColorStyleElements(builder, style.lineStyle); + _writeElement(builder, KmlTag.width, style.lineStyle?.width); + }); + } + + if (style.polyStyle != null) { + builder.element(KmlTag.polyStyle, nest: () { + _writeColorStyleElements(builder, style.polyStyle); + _writeElement(builder, KmlTag.fill, style.polyStyle?.fill); + _writeElement(builder, KmlTag.outline, style.polyStyle?.outline); + }); + } + + if (style.iconStyle != null) { + builder.element(KmlTag.iconStyle, nest: () { + _writeColorStyleElements(builder, style.iconStyle); + if (style.iconStyle?.iconUrl != null) { + builder.element(KmlTag.icon, nest: () { + _writeElement(builder, KmlTag.href, style.iconStyle?.iconUrl); + }); + } + _writeElement(builder, KmlTag.scale, style.iconStyle?.scale); + _writeElement(builder, KmlTag.heading, style.iconStyle?.heading); + if (style.iconStyle?.x != null && + style.iconStyle?.y != null && + style.iconStyle?.xunit != null && + style.iconStyle?.yunit != null){ + builder.element(KmlTag.hotSpot, attributes: { + KmlTag.hotSpotX: style.iconStyle!.x!.toString(), + KmlTag.hotSpotY: style.iconStyle!.y!.toString(), + KmlTag.xunits: style.iconStyle!.xunit!.name, + KmlTag.yunits: style.iconStyle!.yunit!.name, + }); + } + }); + } + + if (style.labelStyle != null) { + builder.element(KmlTag.labelStyle, nest: () { + _writeColorStyleElements(builder, style.labelStyle); + _writeElement(builder, KmlTag.scale, style.labelStyle?.scale); + }); + } + + if (style.balloonStyle != null) { + builder.element(KmlTag.balloonStyle, nest: () { + _writeElement(builder, KmlTag.bgColor, + style.balloonStyle!.bgColor.toRadixString(16)); + _writeElement(builder, KmlTag.textColor, + style.balloonStyle!.textColor.toRadixString(16)); + _writeElement(builder, KmlTag.text, + style.balloonStyle!.text); + _writeElement(builder, KmlTag.displayMode, + style.balloonStyle!.show ? 'default' : 'hide'); + }); + } + }); + } + + void _writeColorStyleElements(XmlBuilder builder, ColorStyle? colorStyle) { + if (colorStyle == null) { + return; + } + _writeElement(builder, + KmlTag.color, colorStyle.color?.toRadixString(16)); + _writeElement(builder, + KmlTag.colorMode, colorStyle.colorMode?.name); + } + + void _writePoint(XmlBuilder builder, Wpt wpt, GeoXml geoXml) { + builder.element(KmlTag.placemark, nest: () { _writeElement(builder, KmlTag.name, wpt.name); _writeElement(builder, KmlTag.desc, wpt.desc); _writeElementWithTime(builder, wpt.time); _writeAtomLinks(builder, wpt.links); + + if (wpt.style != null) { + if (wpt.style!.id != null && wpt.style!.id!.isNotEmpty + && geoXml.styles.contains(wpt.style)) { + _writeElement(builder, KmlTag.styleUrl, '#${wpt.style!.id}'); + } else { + _writeStyle(builder, wpt.style!); + } + } builder.element(KmlTag.extendedData, nest: () { _writeExtendedElement(builder, GpxTag.magVar, wpt.magvar); diff --git a/lib/src/model/geo_style.dart b/lib/src/model/geo_style.dart index bfbeaff..71b98cf 100644 --- a/lib/src/model/geo_style.dart +++ b/lib/src/model/geo_style.dart @@ -294,10 +294,10 @@ class BalloonStyle { /// overlay, you would specify the following: 7fff0000, /// where alpha=0x7f, blue=0xff, green=0x00, and red=0x00. The default is /// opaque white (ffffffff). - int? bgColor; + int bgColor = 0xffffffff; /// Foreground color for text. The default is black (ff000000). - int? textColor = 0xff000000; + int textColor = 0xff000000; /// Text displayed in the balloon. If no text is specified, Google Earth /// draws the default balloon (with the Feature in boldface, the diff --git a/lib/src/model/geoxml.dart b/lib/src/model/geoxml.dart index a86f373..a65fc4f 100644 --- a/lib/src/model/geoxml.dart +++ b/lib/src/model/geoxml.dart @@ -7,6 +7,7 @@ import '../kml_reader.dart'; import '../kml_writer.dart'; import 'geo_style.dart'; import 'metadata.dart'; +import 'polygon.dart'; import 'rte.dart'; import 'trk.dart'; import 'wpt.dart'; @@ -35,6 +36,8 @@ class GeoXml { /// A list of tracks. List trks = []; + List polygons = []; + List styles = []; /// You can add extend GPX by adding your own elements from another schema @@ -51,6 +54,7 @@ class GeoXml { const ListEquality().equals(other.wpts, wpts) && const ListEquality().equals(other.rtes, rtes) && const ListEquality().equals(other.trks, trks) && + const ListEquality().equals(other.polygons, polygons) && const ListEquality().equals(other.styles, styles) && const MapEquality().equals(other.extensions, extensions); } @@ -66,6 +70,7 @@ class GeoXml { wpts, rtes, trks, + polygons, styles, extensions ].join(",")}]"; @@ -80,6 +85,7 @@ class GeoXml { ...trks, ...rtes, ...wpts, + ...polygons, ...styles ]); diff --git a/lib/src/model/polygon.dart b/lib/src/model/polygon.dart new file mode 100644 index 0000000..1fc765a --- /dev/null +++ b/lib/src/model/polygon.dart @@ -0,0 +1,111 @@ +import 'package:collection/collection.dart'; +import 'package:geoxml/src/model/geo_style.dart'; +import 'package:geoxml/src/model/kml_tag.dart'; +import 'package:quiver/core.dart'; + +import '../tools/color_converter.dart'; +import 'geo_object.dart'; +import 'link.dart'; +import 'trk.dart'; +import 'wpt.dart'; + +/// Polygon represents a simple polygon - +/// an ordered list of points describing a shape. +class Polygon implements GeoObject { + /// Name of Polygon. + @override + String? name; + + /// GPS comment for track. + @override + String? cmt; + + /// User description of track. + @override + String? desc; + + /// Source of data. Included to give user some idea of reliability and + /// accuracy of data. + @override + String? src; + + /// Links to external information about the track. + @override + List links; + + /// GPS track number. + @override + int? number; + + /// Type (classification) of track. + @override + String? type; + + /// You can add extend GPX by adding your own elements from another schema + /// here. + @override + Map extensions; + + // Element tag. + @override + String tag = KmlTag.polygon; + + /// A Track Segment holds a list of Track Points which are logically connected + /// in order. To represent a single GPS track where GPS reception was lost, or + /// the GPS receiver was turned off, start a new Track Segment for each + /// continuous span of track data. + List points; + + @override + GeoStyle? style; + + /// Construct a new [Trk] object. + Polygon( + {this.name, + this.cmt, + this.desc, + this.src, + List? links, + this.number, + this.type, + Map? extensions, + List? points}) + : links = links ?? [], + extensions = extensions ?? {}, + points = points ?? []; + + @override + // ignore: type_annotate_public_apis + bool operator ==(other) { + if (other is Polygon) { + return other.name == name && + other.cmt == cmt && + other.desc == desc && + other.src == src && + const ListEquality().equals(other.links, links) && + other.number == number && + other.type == type && + const MapEquality().equals(other.extensions, extensions) && + const ListEquality().equals(other.points, points); + } + + return false; + } + + @override + String toString() => "Polygon[${[name, type, extensions, points].join(",")}]"; + + @override + int get hashCode => hashObjects([ + name, + cmt, + desc, + src, + number, + type, + ...links, + ...extensions.keys, + ...extensions.values, + ...points + ]); +} \ No newline at end of file diff --git a/test/tests/kml_writer_test.dart b/test/tests/kml_writer_test.dart index 7be158e..144ae38 100644 --- a/test/tests/kml_writer_test.dart +++ b/test/tests/kml_writer_test.dart @@ -64,6 +64,15 @@ void main() { xml); }); + // test('write styled kml', () async { + // final kmlString = await File('test/assets/style_test.kml').readAsString(); + // final kml = await GeoXml.fromKmlString(kmlString); + // final kmlOutput = KmlWriter().asString(kml, pretty: true); + // print(kmlOutput); + // + // expectXml(kmlOutput, kmlString); + // }); + test('write large kml', () async { final gpx = await GpxReader() .fromString(await File('test/assets/large.gpx').readAsString());