Skip to content

Instantly share code, notes, and snippets.

@kentstanton
Created July 19, 2015 17:06
Show Gist options
  • Save kentstanton/3441cc368d3c52621b19 to your computer and use it in GitHub Desktop.
Save kentstanton/3441cc368d3c52621b19 to your computer and use it in GitHub Desktop.
Powershell 5.0 script to parse a KML file and pull out nodes. Useful for deconstructing an out-of-control Google Earth myplaces file.
<#
.Synopsis
Script to assist with untangling an out-of-control Google Earth MyPlaces file
Selects document, folder or placemark top-level nodes in your myplaces file
and writes new files containing just the elements of that type.
More information available at:
http://spastatepark.blogspot.com/2015/07/using-google-earth-for-geographic.html
Requires Powershell 5.x - because I wanted to get familiar with the new PS 5.0 OOP features.
.DESCRIPTION
Parse a KML file and produce new files containing selected kml objects.
Specify "all" to create new files for everything in your MyPlaces file.
Make a copy of your MyPlaces file and set the global path variables below to run the script.
IMPORTANT: This version of the script requires Powershell 5.0. See my github site for information
on versions that work with older versions of Powershell.
Developer Notes:
Using Powersheel 5.x classes to structure the code using composition and the factory pattern.
Execution starts with call to main at the end of the script.
.EXAMPLE
parse-kml -sourceFileName myplaces_copy.kml -NodeTypeToSelect folder
Parse the KML file named myplaces_copy.kml in the current folder and outputs all top level folder nodes
to new files in a sub-folder of the source file location.
Overwrite defaults to false so if files exist in the output location, the process is halted.
.EXAMPLE
parse-kml -sourceFileName myplaces_copy.kml -NodeTypeToSelect all -overwrite true
Parse the KML file named myplaces_copy.kml in the current folder and output all top level nodes
to new files in a sub-folder of the source file location.
Overwrite existing output files if any are found.
.EXAMPLE
parse-kml -sourceFileName myplaces_copy.kml -path "c:\temp" -NodeTypeToSelect all -overwrite true
Parse the KML file named myplaces_copy.kml in the current folder and output all top level nodes
to new files in a sub-folder of the source file location.
Overwrite existing output files if any are found.
#>
param(
[Parameter(Mandatory=$true)]
$SourceFileName,
[Parameter(Mandatory=$true)]
$NodeTypeToSelect,
[Parameter(Mandatory=$false)]
$AllowOverwrite = $false,
[Parameter(Mandatory=$false)]
$path = $(Convert-Path("."))
)
#####
# Execution starts with the call to main() at the end of the script
#####
# PS 5.x is required.
#Requires –Version 5
Set-StrictMode -Version latest
# Stop on any error. Change this if you want to hanlde errors in a more granular way.
$ErrorActionPreference = "Stop"
CLS
# Using standard input parameter names but keeping these internal names.
# You can override the input parameters by changing these assignments to use values approprite for your working environment.
# The KML files created by the script go into a subfolder as named below.
$myPlacesFileName = $SourceFileName
$myPlacesFilePath = $path
$OutputSubFolder = "kmlfiles"
<# Build the paths for the input file and for output. Path validation is done here.
Checking if overwriting is an issue is done here.
#>
class EnvironmentData {
[string] $sourceFileName;
[string] $sourcePath;
[string] $sourceFullPath;
[string] $outputPath;
EnvironmentData([string] $SourcefileName, [string] $SourcePath, [string] $OutputSubFolder, [boolean]$AllowOverwrite) {
try {
$This.sourceFileName = $SourcefileName;
if ($SourcePath.EndsWith("\")) {
$This.sourcePath = $SourcePath;
} else {
$This.sourcePath = "$($SourcePath)\";
}
$This.sourceFullPath = "$($This.SourcePath)$($This.SourceFileName)";
$This.outputPath = "$($This.sourcePath)$($OutputSubFolder)"
# If the output folder does not exist, create it
if ($(test-path $This.outputPath) -eq $false) {
new-item -itemtype directory -force -Path $This.outputPath
}
$This.MakeOutputfolder($AllowOverwrite)
$This.ReportPathErrors()
} catch {
"Unhandled Exception: EnvironmentData - Error building the input and/or output paths."
}
}
MakeOutputfolder ($AllowOverwrite) {
$dateForName = $((Get-Date).ToShortDateString());
$dateForName = $dateForName.Replace("/", "_");
$pathWithSubFolder = "$($this.outputPath)\$($dateForName)"
# brute
if ($(test-path $pathWithSubFolder) -eq $true) {
$countFilesInOutputPath = @( Get-ChildItem $pathWithSubFolder).Count;
#= Get-ChildItem -Path $pathWithSubFolder -Include *.kml
if ( $($countFilesInOutputPath -ne 0) -and $($AllowOverwrite -eq $false)) {
Write-host "Error: You must pass $true for $AllowOverwrite OR the output folder must be empty. No files were written." -ForegroundColor red -BackgroundColor Yellow
Exit 4
}
} else {
New-Item -Path $pathWithSubFolder -type directory -ErrorAction SilentlyContinue
$This.outputPath = $pathWithSubFolder
}
$This.outputPath = $pathWithSubFolder
}
ReportPathErrors() {
if ($(test-path $This.SourceFullPath) -eq $false) {
write-host -foregroundcolor yellow -backgroundcolor red "Terminating Error: The Source path is invalid. $($This.sourceFullPath)"
Exit 2
}
if ($(test-path $This.outputPath) -eq $false) {
write-host -foregroundcolor yellow -backgroundcolor red "Terminating Error: The Output path is invalid. $($This.outputPath)"
Exit 2
}
}
}
class KmlPoint {
[float] $latitude;
[float] $longitude;
[float] $elevation;
KmlPlacemarkEX([xml.xmlElement] $kmlDocElement) {
$this.kmlStyleIdentifier = $kmlDocElement.styleUrl;
}
}
class KmlPlacemark {
[string] $kmlStyleIdentifier;
[string] $kmlTypeIdentifier;
KmlPlacemark([xml.xmlElement] $kmlDocElement) {
$this.kmlStyleIdentifier = $kmlDocElement.styleUrl;
$this.kmlTypeIdentifier = "placemark";
}
}
class KmlFolder {
[string] $kmlStyleIdentifier;
[string] $kmlTypeIdentifier;
KmlFolder([xml.xmlElement] $kmlDocElement) {
$this.kmlStyleIdentifier = "";
$this.kmlTypeIdentifier = "folder";
}
}
class KmlDocument {
[string] $kmlStyleIdentifier;
[string] $kmlTypeIdentifier;
KmlDocument([xml.xmlElement] $kmlDocElement) {
$this.kmlStyleIdentifier = "";
$this.kmlTypeIdentifier = "document";
}
}
# Node Factory using Powershell 5
class KmlNodeFactory {
[xml.xmlElement] $xml;
[string] $kmlNodeName;
[object] $TypedNode;
[xml.xmlElement] $parentNode;
[Boolean]$selectedNode = $false;
KmlNodeFactory([xml.xmlElement] $xmlElement, [string] $nodeTypeName, [string] $NodeTypeToSelect) {
try {
$this.xml = $xmlElement;
# need a better way to handle this...
if ($this.xml.name.GetType().Name -eq "XmlElement") {
$this.kmlNodeName = $this.xml.name.'#text'
} else {
$this.kmlNodeName = $this.xml.name;
}
$this.parentNode = $xmlElement.ParentNode;
$this.SelectedNode
Switch ($nodeTypeName) {
"Placemark" {$this.TypedNode = [KmlPlacemark]::new($this.xml)}
"Folder" {$this.TypedNode = [KmlFolder]::new($this.xml)}
"Document" {$this.TypedNode = [KmlDocument]::new($this.xml);}
}
if ($NodeTypeToSelect.ToLower() -eq "all") {
$this.selectedNode = $true;
} elseif ($NodeTypeToSelect -ne "" -and ($this.TypedNode.kmlTypeIdentifier.ToLower() -eq $NodeTypeToSelect.ToLower()) ) {
$this.selectedNode = $true;
}
$AllNodesList += $this;
} catch {
Write-host "Unhandled exception in AddContentToKmlTemplate" -BackgroundColor Red -ForegroundColor Yellow
$_.Exception.Message;
Exit 3;
}
}
}
# Brute force approach to building up the output XML
# Todo: wrap this in a kmlwriter class
function AddContentToKmlTemplate($selectedNodes) {
$kmlNamespaces = @'
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
'@
try {
$nodeTypeStartTag = "<$($selectedNodes.TypedNode.kmlTypeIdentifier)>"
$nodeTypeEndTag = "</$($selectedNodes.TypedNode.kmlTypeIdentifier)>"
$templatedKml = "$($kmlNamespaces)$($nodeTypeStartTag)$($selectedNodes.xml.Innerxml)$($nodeTypeEndTag)</kml>"
return $templatedKml
} catch {
Write-host "Unhandled exception in AddContentToKmlTemplate" -BackgroundColor Red -ForegroundColor Yellow
$_.Exception.Message;
Exit 4;
}
}
<#
Create a sub-folder using today's date as the name. Nodes are written as files in this folder.
# Todo: wrap this in a kmlwriter class
#>
function WriteKml($selectedNodes, $env) {
try {
foreach($nodeToWrite in $selectedNodes) {
if ($nodeToWrite.kmlNodeName -eq "") {
write-host "Warning: No name found for the node to write. We really ought to just stop here but we'll try the next one."
} else {
# not going to allow spaces in file names
$kmlPlaceFileName = ($nodeToWrite.kmlNodeName).replace(" ", "_");
}
$fullOutputPath = "$($env.outputpath)\$($kmlPlaceFileName).kml";
foreach($node in $nodeToWrite) {
$templatedKmlContent = AddContentToKmlTemplate $node
$templatedKmlContent | Set-Content $fullOutputPath
}
}
} catch {
Write-host "Unhandled exception in WriteKml" -BackgroundColor Red -ForegroundColor Yellow
$_.Exception.Message;
Exit 5;
}
}
<#
This should/could be refactored to incorporate the parsing
into the factory class. My intent is to rewrite this
with the core functionality provided by CmdLets written in C#.
Time permitting...
#>
Function Main($envData) {
[xml]$kmlFileContent = get-content $envData.sourceFullPath -ErrorAction Continue
$KmlDocumentsList = @()
$KmlFoldersList = @()
$KmlPlacemarksList = @()
$AllNodesList = @()
$SelectedNodes = @()
$kmlPlacemarks = $kmlFileContent.kml.Document.folder.Placemark
Write-host "`nProcessing Placemarks" -BackgroundColor DarkRed
foreach ($KmlPlacemark in $kmlPlacemarks) {
$PlaceMarkNode = [KmlNodeFactory]::new($KmlPlacemark, "Placemark", $NodeTypeToSelect);
$KmlPlacemarksList += $PlaceMarkNode
$AllNodesList += $PlaceMarkNode
if ($PlaceMarkNode.SelectedNode) {
$SelectedNodes += $PlaceMarkNode;
}
}
$kmlDocuments = $kmlFileContent.kml.Document.folder.Document;
Write-host "`nProcessing Documents" -BackgroundColor DarkRed
foreach ($KmlDocument in $kmlDocuments) {
$DocumentNode = [KmlNodeFactory]::new($KmlDocument, "Document", $NodeTypeToSelect);
$KmlDocumentsList += $DocumentNode
$AllNodesList += $DocumentNode
if ($DocumentNode.SelectedNode) {
$SelectedNodes += $DocumentNode;
}
}
$kmlFolders = $kmlFileContent.kml.Document.Folder.Folder;
Write-host "`nProcessing Folders" -BackgroundColor DarkRed
foreach ($kmlFolder in $kmlFolders) {
$FolderNode = [KmlNodeFactory]::new($KmlFolder, "Folder",$NodeTypeToSelect);
$KmlFoldersList += $FolderNode
$AllNodesList += $FolderNode
if ($FolderNode.SelectedNode) {
$SelectedNodes += $FolderNode;
}
}
# Write the output; Display results
if ($SelectedNodes.Count -eq 0) {
$AllNodesList | Out-GridView
} else {
WriteKml $SelectedNodes $envData
$SelectedNodes | Out-GridView
}
}
# load and validate the paths used by the script
$Configuration = [EnvironmentData]::new($myPlacesFileName,$myPlacesFilePath,$OutputSubFolder, $AllowOverwrite);
Main $Configuration $overWrite
@lhsupply
Copy link

I would love to be able to use this script but fail to even start it.
The examples start with "parse-kml" but nowhere I can find where it refers to: "The term 'parse-kml' is not recognized as the name of a cmdlet, function, script file, or operable program."
Can you help?
Thanks!
Leo

@Merlin844
Copy link

Put the entire script inside function.

Function Parse-KML() {
PS Code Above
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment