diff --git a/.gitignore b/.gitignore index 1c050d7..7bfa204 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ sampleOutput.kml /KML_Samples_output.kml *_out.kml + +work diff --git a/src/it/scala/com/phasmidsoftware/kmldoc/KMLEditorFuncSpec.scala b/src/it/scala/com/phasmidsoftware/kmldoc/KMLEditorFuncSpec.scala new file mode 100644 index 0000000..8d25e92 --- /dev/null +++ b/src/it/scala/com/phasmidsoftware/kmldoc/KMLEditorFuncSpec.scala @@ -0,0 +1,74 @@ +package com.phasmidsoftware.kmldoc + +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should +import scala.util._ + +class KMLEditorFuncSpec extends AnyFlatSpec with should.Matchers { + + behavior of "KMLEditor" + + val placemark = "Placemark" + private val triedFilename: Success[String] = Success("src/main/resources/com/phasmidsoftware/kmldoc/placemarks.kml") + + it should "processKMLs join" in { + val editor = KMLEditor(Seq(KmlEdit(KmlEdit.JOIN, 2, Element(placemark, "Salem & Lowell RR (#1)"), Some(Element(placemark, "Salem & Lowell RR (#2)"))), KmlEdit(KmlEdit.DELETE, 1, Element(placemark, "Salem & Lowell RR (#2)"), None))) + val ksi: IO[Seq[KML]] = for { + ks <- KMLCompanion.loadKML(triedFilename) + ks2 = editor.processKMLs(ks) + } yield ks2 + val result = ksi.unsafeRunSync() + val feature: Feature = result.head.features.head + feature match { + case Document(fs) => + val folder = fs.head + folder match { + case Folder(features) => + features.size shouldBe 1 + val p1 = features.head + p1 match { + case p@Placemark(g) => + p.featureData.name.$.toString shouldBe "Salem & Lowell RR (#1)Salem & Lowell RR (#2)" + p.featureData.name.matches("Salem & Lowell RR (#1)Salem & Lowell RR (#2)") shouldBe true + val lineString = g.head + lineString match { + case LineString(_, cs) => + cs.head.coordinates.size shouldBe 163 + // TODO check the ordering of the Coordinate values. + } + } + } + } + } + + it should "processKMLs joinX" in { + val editor = KMLEditor(Seq(KmlEdit(KmlEdit.JOINX, 2, Element(placemark, "Salem & Lowell RR (#1)"), Some(Element(placemark, "Salem & Lowell RR (#2)"))), KmlEdit(KmlEdit.DELETE, 1, Element(placemark, "Salem & Lowell RR (#2)"), None))) + val ksi: IO[Seq[KML]] = for { + ks <- KMLCompanion.loadKML(triedFilename) + ks2 = editor.processKMLs(ks) + } yield ks2 + val result = ksi.unsafeRunSync() + val feature: Feature = result.head.features.head + feature match { + case Document(fs) => + val folder = fs.head + folder match { + case Folder(features) => + features.size shouldBe 1 + val p1 = features.head + p1 match { + case p@Placemark(g) => + p.featureData.name.matches("Salem & Lowell RR (#1)") shouldBe true + val lineString = g.head + lineString match { + case LineString(_, cs) => + cs.head.coordinates.size shouldBe 163 + // TODO check the ordering of the Coordinate values. + } + } + } + } + } +} diff --git a/src/main/scala/com/phasmidsoftware/core/XML.scala b/src/main/scala/com/phasmidsoftware/core/XML.scala index da3921c..8033f61 100644 --- a/src/main/scala/com/phasmidsoftware/core/XML.scala +++ b/src/main/scala/com/phasmidsoftware/core/XML.scala @@ -16,7 +16,7 @@ case class Text($: CharSequence) extends Mergeable[Text] { * @param t the object to be merged with this. * @return the merged value of T. */ - def merge(t: Text): Option[Text] = ($, t.$) match { + def merge(t: Text, mergeName: Boolean = true): Option[Text] = ($, t.$) match { case (c1: CDATA, c2: CDATA) => c1 merge c2 map (Text(_)) case _ => Some(Text($.toString + " " + t.$.toString)) } @@ -85,7 +85,7 @@ case class CDATA(content: String, pre: String, post: String) extends CharSequenc * @param t the object to be merged with this. * @return the merged value of T. */ - def merge(t: CDATA): Option[CDATA] = Some(CDATA(content + separator(post, t.pre) + t.content, pre, t.post)) + def merge(t: CDATA, mergeName: Boolean = true): Option[CDATA] = Some(CDATA(content + separator(post, t.pre) + t.content, pre, t.post)) private def separator(a: String, b: String): String = (a + b).replaceAll("\n", "
") } diff --git a/src/main/scala/com/phasmidsoftware/kmldoc/KML.scala b/src/main/scala/com/phasmidsoftware/kmldoc/KML.scala index cdff36d..0bab074 100644 --- a/src/main/scala/com/phasmidsoftware/kmldoc/KML.scala +++ b/src/main/scala/com/phasmidsoftware/kmldoc/KML.scala @@ -6,7 +6,7 @@ import com.phasmidsoftware.core.FP.tryNotNull import com.phasmidsoftware.core._ import com.phasmidsoftware.kmldoc.HasFeatures.editHasFeaturesToOption import com.phasmidsoftware.kmldoc.KMLCompanion.renderKMLToPrintStream -import com.phasmidsoftware.kmldoc.KmlEdit.editFeatures +import com.phasmidsoftware.kmldoc.KmlEdit.{JOIN, JOINX, editFeatures} import com.phasmidsoftware.kmldoc.KmlRenderers.sequenceRendererFormatted import com.phasmidsoftware.kmldoc.Mergeable.{mergeOptions, mergeOptionsBiased, mergeSequence, mergeStringsDelimited} import com.phasmidsoftware.render._ @@ -51,7 +51,7 @@ case class KmlData(__id: Option[String]) extends Mergeable[KmlData] { * @param k a KmlData object. * @return the merged value of KmlData. */ - def merge(k: KmlData): Option[KmlData] = Some(KmlData(mergeStringsDelimited(__id, k.__id)("#"))) + def merge(k: KmlData, mergeName: Boolean = true): Option[KmlData] = Some(KmlData(mergeStringsDelimited(__id, k.__id)("#"))) } /** @@ -125,10 +125,10 @@ case class FeatureData(name: Text, maybeDescription: Option[Text], maybeStyleUrl * @param f a FeatureData object. * @return the merged value of FeatureData. */ - def merge(f: FeatureData): Option[FeatureData] = { + def merge(f: FeatureData, mergeName: Boolean = true): Option[FeatureData] = { // TODO warn if styles are not the same. for { - n <- name merge f.name + n <- if (mergeName) name merge f.name else Some(name) d = mergeOptions(maybeDescription, f.maybeDescription)((t1, t2) => t1 merge t2) z <- kmlData merge f.kmlData } yield FeatureData(n, d, maybeStyleUrl, maybeOpen, maybeVisibility, StyleSelectors, abstractView)(z) // TODO: not all fields are properly merged @@ -190,7 +190,7 @@ trait Geometry extends KmlObject with Mergeable[Geometry] { * @param t the object to be merged with this. * @return the merged value of T. */ - def merge(t: Geometry): Option[Geometry] = throw KmlException(s"merge not implemented for this class: ${t.getClass}") + def merge(t: Geometry, mergeName: Boolean = true): Option[Geometry] = throw KmlException(s"merge not implemented for this class: ${t.getClass}") } /** @@ -211,7 +211,7 @@ object Geometry extends Extractors with Renderers { * @param kmlData source of properties. */ case class GeometryData(maybeExtrude: Option[Extrude], maybeAltitudeMode: Option[AltitudeMode])(val kmlData: KmlData) extends Mergeable[GeometryData] { - def merge(g: GeometryData): Option[GeometryData] = + def merge(g: GeometryData, mergeName: Boolean = true): Option[GeometryData] = for { k <- kmlData merge g.kmlData } yield GeometryData(mergeOptionsBiased(maybeExtrude, g.maybeExtrude), mergeOptionsBiased(maybeAltitudeMode, g.maybeAltitudeMode))(k) @@ -405,14 +405,14 @@ case class Placemark(Geometry: Seq[Geometry])(val featureData: FeatureData) exte * @param t the object to be merged with this. * @return the merged value of T. */ - def merge(t: Placemark): Option[Placemark] = { - println(s"joinPlacemarks: $name, ${t.name}") + def merge(t: Placemark, mergeName: Boolean = true): Option[Placemark] = { + logger.info(s"joinPlacemarks: $name, ${t.name} with mergeName=$mergeName") val gps: Seq[Geometry] = this.Geometry val gqs = t.Geometry val los: Seq[Option[Geometry]] = for (gp <- gps; gq <- gqs) yield gp.merge(gq) val z: Seq[Geometry] = los filter (_.isDefined) map (_.get) for { - xx <- this.featureData merge t.featureData + xx <- featureData.merge(t.featureData, mergeName) } yield Placemark(z)(xx) } @@ -441,7 +441,7 @@ case class Placemark(Geometry: Seq[Geometry])(val featureData: FeatureData) exte private def editMatching1(e: KmlEdit) = (name, e) match { case (name, KmlEdit(KmlEdit.DELETE, _, Element(_, nameToMatch), None)) if name.matches(nameToMatch) => - System.err.println(s"delete: $nameToMatch") // TODO generate a log message + logger.info(s"delete: $nameToMatch") Some(None) case (_, KmlEdit(KmlEdit.DELETE, _, _, _)) => Some(Some(this)) @@ -457,10 +457,10 @@ case class Placemark(Geometry: Seq[Geometry])(val featureData: FeatureData) exte * @return an optional optional Feature. */ private def editMatchingPlacemark2(e: KmlEdit, fs: Seq[Feature]) = - (name, e) match { - case (name, KmlEdit(KmlEdit.JOIN, _, Element("Placemark", nameToMatch1), Some(Element("Placemark", nameToMatch2)))) + e match { + case KmlEdit(command@(JOIN | JOINX), _, Element("Placemark", nameToMatch1), Some(Element("Placemark", nameToMatch2))) if name.matches(nameToMatch1) => - Some(joinMatchedPlacemarks(fs, nameToMatch2)) + Some(joinMatchedPlacemarks(fs, nameToMatch2, command == JOIN)) case _ => None } @@ -472,17 +472,15 @@ case class Placemark(Geometry: Seq[Geometry])(val featureData: FeatureData) exte * @param nameToMatch the name of the feature to be joined, as defined by the edit. * @return an optional Feature which, if defined, is the new Placemark to be used instead of p. */ - private def joinMatchedPlacemarks(fs: Seq[Feature], nameToMatch: String) = { - System.err.println(s"join: $name with $nameToMatch") // TODO generate a log message - val zz = for (f <- fs if f != this) yield joinMatchingPlacemarks(nameToMatch, f) + private def joinMatchedPlacemarks(fs: Seq[Feature], nameToMatch: String, mergeName: Boolean) = { + val zz = for (f <- fs if f != this) yield joinMatchingPlacemarks(nameToMatch, f, mergeName) for (z <- zz.find(_.isDefined); q <- z) yield q } - private def joinMatchingPlacemarks(name: String, feature: Feature) = - feature match { - case q: Placemark if q.name.matches(name) => this merge q - case _ => None - } + private def joinMatchingPlacemarks(name: String, feature: Feature, mergeName: Boolean) = feature match { + case q: Placemark if q.name.matches(name) => merge(q, mergeName) + case _ => None + } } /** @@ -559,7 +557,7 @@ case class LineString(tessellate: Tessellate, coordinates: Seq[Coordinates])(val import Coordinates.empty - override def merge(g: Geometry): Option[Geometry] = g match { + override def merge(g: Geometry, mergeName: Boolean = true): Option[Geometry] = g match { case l@LineString(_, _) => for { t <- tessellate merge l.tessellate @@ -900,7 +898,7 @@ object LabelStyle extends Extractors with Renderers { * @param $ the value. */ case class Tessellate($: CharSequence) extends Mergeable[Tessellate] { - def merge(t: Tessellate): Option[Tessellate] = ($, t.$) match { + def merge(t: Tessellate, mergeName: Boolean = true): Option[Tessellate] = ($, t.$) match { case (a, b) if a == b => Some(Tessellate(a)) case _ => None } @@ -982,7 +980,7 @@ case class Coordinates(coordinates: Seq[Coordinate]) extends Mergeable[Coordinat def gap(other: Coordinates): Option[Double] = gapInternal(coordinates, other.coordinates) - def merge(other: Coordinates): Option[Coordinates] = { + def merge(other: Coordinates, mergeName: Boolean = true): Option[Coordinates] = { val xo = vector val yo: Option[Cartesian] = other.vector val zo: Option[Double] = for (x <- xo; y <- yo) yield x dotProduct y @@ -1562,7 +1560,7 @@ trait Mergeable[T] { * @param t the object to be merged with this. * @return the merged value of T. */ - def merge(t: T): Option[T] + def merge(t: T, mergeName: Boolean = true): Option[T] } /** diff --git a/src/main/scala/com/phasmidsoftware/kmldoc/KMLEdit.scala b/src/main/scala/com/phasmidsoftware/kmldoc/KMLEdit.scala index 102805d..d4f9cb6 100644 --- a/src/main/scala/com/phasmidsoftware/kmldoc/KMLEdit.scala +++ b/src/main/scala/com/phasmidsoftware/kmldoc/KMLEdit.scala @@ -19,8 +19,8 @@ case class KmlEdit(command: String, operands: Int, op1: Element, maybeOp2: Optio object KmlEdit { def operands(command: String): Int = command match { - case KmlEdit.JOIN => 2 - case KmlEdit.DELETE => 1 + case JOIN | JOINX => 2 + case DELETE => 1 case _ => 0 } @@ -61,7 +61,19 @@ object KmlEdit { */ def parseLines(ws: Iterator[String]): IO[Seq[KmlEdit]] = (for (w <- ws) yield parse(w)).toSeq.sequence + /** + * Join two elements (typically using the merge method of Mergeable KML objects). + */ val JOIN = "join" + + /** + * like JOIN but retains the first name (i.e. it excludes the second name). + */ + val JOINX = "joinX" + + /** + * delete an element. + */ val DELETE = "delete" } diff --git a/src/main/scala/com/phasmidsoftware/kmldoc/KMLEditor.scala b/src/main/scala/com/phasmidsoftware/kmldoc/KMLEditor.scala index 91ac45a..b9ee31b 100644 --- a/src/main/scala/com/phasmidsoftware/kmldoc/KMLEditor.scala +++ b/src/main/scala/com/phasmidsoftware/kmldoc/KMLEditor.scala @@ -8,6 +8,7 @@ import com.phasmidsoftware.kmldoc.KMLCompanion.renderKMLs import com.phasmidsoftware.kmldoc.KMLEditor.{addExtension, write} import com.phasmidsoftware.render.FormatXML import java.io.{BufferedWriter, File, FileWriter, Writer} +import org.slf4j.{Logger, LoggerFactory} import scala.annotation.tailrec import scala.io.Source import scala.util._ @@ -19,7 +20,7 @@ import scala.util._ */ case class KMLEditor(edits: Seq[KmlEdit]) { - System.err.println(s"KMLEditor: ${edits.mkString}") // TODO generate log message + KMLEditor.logger.info(s"KMLEditor: ${edits.mkString}") /** * Method to process the file defined by baseFilename by parsing it, editing it, and writing it out. @@ -34,8 +35,8 @@ case class KMLEditor(edits: Seq[KmlEdit]) { val inputFile = addExtension(baseFilename, kml) val outExt = "_out" + kml val outputFile = addExtension(baseFilename, outExt) - inputFile foreach (f => System.err.println(s"KMLEditor.process from $f")) // TODO generate a log message - outputFile foreach (f => System.err.println(s"KMLEditor.process to $f")) // TODO generate a log message + inputFile foreach (f => KMLEditor.logger.info(s"KMLEditor.process from $f")) + outputFile foreach (f => KMLEditor.logger.info(s"KMLEditor.process to $f")) processFromTo(inputFile, outputFile) } @@ -50,7 +51,7 @@ case class KMLEditor(edits: Seq[KmlEdit]) { private def processFromTo(inputFile: Try[String], outputFile: Try[String]): IO[Unit] = { val qsi: IO[Seq[Writer]] = for { w <- IO.fromTry(outputFile) - _ = println(w) +// _ = println(w) f <- IO(new File(w)) bW = new BufferedWriter(new FileWriter(f, false)) ks <- KMLCompanion.loadKML(inputFile) @@ -89,6 +90,8 @@ case class KMLEditor(edits: Seq[KmlEdit]) { } object KMLEditor { + val logger: Logger = LoggerFactory.getLogger(KMLEditor.getClass) + /** * Method to construct a KMLEditor from a filename. * diff --git a/src/test/scala/com/phasmidsoftware/kmldoc/KMLEditorSpec.scala b/src/test/scala/com/phasmidsoftware/kmldoc/KMLEditorSpec.scala index 90ff14a..8dd9f99 100644 --- a/src/test/scala/com/phasmidsoftware/kmldoc/KMLEditorSpec.scala +++ b/src/test/scala/com/phasmidsoftware/kmldoc/KMLEditorSpec.scala @@ -11,39 +11,6 @@ class KMLEditorSpec extends AnyFlatSpec with should.Matchers { behavior of "KMLEditor" val placemark = "Placemark" - private val triedFilename: Success[String] = Success("src/main/resources/com/phasmidsoftware/kmldoc/placemarks.kml") - - it should "processKMLs" in { - val editor = KMLEditor(Seq(KmlEdit(KmlEdit.JOIN, 2, Element(placemark, "Salem & Lowell RR (#1)"), Some(Element(placemark, "Salem & Lowell RR (#2)"))), KmlEdit(KmlEdit.DELETE, 1, Element(placemark, "Salem & Lowell RR (#2)"), None))) - val ksi: IO[Seq[KML]] = for { - ks <- KMLCompanion.loadKML(triedFilename) - ks2 = editor.processKMLs(ks) - } yield ks2 - val result = ksi.unsafeRunSync() - val feature: Feature = result.head.features.head - feature match { - case Document(fs) => - val folder = fs.head - folder match { - case Folder(features) => - features.size shouldBe 1 - val p1 = features.head - p1 match { - case Placemark(g) => - val lineString = g.head - lineString match { - case LineString(_, cs) => - cs.head.coordinates.size shouldBe 163 - // TODO check the ordering of the Coordinate values. - } - } - } - } - } - - it should "edits" in { - - } it should "parse" in { val result: IO[KMLEditor] = KMLEditor.parse(Success("src/main/resources/com/phasmidsoftware/kmldoc/sampleEdits.txt"))