Snippets

Tuomas Hietanen KML / Google Maps / Oculus Wander

Created by Tuomas Hietanen
/// Set of little F# (FSharp) functions as script tools to work with 
/// - KML files (Keyhole Markup Language) used by Google Earth and Google Maps
/// - Google Maps Streetview API,
/// - Oculus Wander Application (by Parkline Interactive, LLC) bookmarks: Wander_Favorites.json

/// You have to first set your Google API key to environment variables as api-key and enable
/// Streetview API for that
// System.Environment.SetEnvironmentVariable("api-key", "...")

#r "nuget: FSharp.Data"
#r "System.Xml.Linq"

open System
open FSharp.Data
open System.Xml.Linq

[<Literal>]
let kmlschemaPath = __SOURCE_DIRECTORY__ + "/sample.kml"

type StreetviewMeta = FSharp.Data.JsonProvider<"""[{"copyright" : "© TH","date" : "2017-08","location" : {
    "lat" : 60.1697536,"lng" : 24.9332589},"pano_id" : "asdfasdfasdf","status" : "OK"},{"status":"ZERO_RESULTS"}]""", SampleIsList=true>
type KmlSchema = FSharp.Data.XmlProvider<kmlschemaPath, SampleIsList = false>

type PlaceId = 
| Location of string
| PanoId of string
| Unknown
    override this.ToString() =
        match this with
        | Location x -> x
        | PanoId p -> p
        | Unknown -> ""

type StreetviewRequestType =
| Picture of string //filename
| Metadata

type WanderSchema = FSharp.Data.JsonProvider<"""[
     [{"title": "FolderName","isFolder": true,"folderContents": [{"panoid": "1Eo78UUvk1O-KWRDO-egtg",
       "title": "Placename","isFolder": false,"timeStamp": 637791743005323710},
      {"title": "FolderName","isFolder": true,"folderContents": [{"panoid": "1Eo78UUvk1O-KWRDO-egtg",
       "title": "Placename","isFolder": false,"timeStamp": 637791743005323710}],"timeStamp": 637791743005323710}
     ],"timeStamp": 637794221079717650}],
     [{"panoid": "1Eo78UUvk1O-KWRDO-egtg","title": "Placename","isFolder": false,"timeStamp": 637791743005323710}]
    ]""", SampleIsList=true>

/// Given Google Maps StreetView URL, parse Panoid
let parse_panoid (uri:string) = 
    let start = uri.IndexOf("!1s")
    if start = -1 || not (uri.Contains ".google.") then Unknown
    else PanoId (uri.Substring(start+3, uri.IndexOf("!2e") - 3 - start).Replace("%2F","/"))

/// Read Google API key from environment variables
/// You can do in Console: set api-key ...
let google_api_key = 
    match Environment.GetEnvironmentVariable "api-key" with
    | null -> failwith "Set environment variable api-key to point your Google Maps API key. Enable Streetview API for the key."
    | x -> x

let googleMapsStreetviewBaseUrl = "https://maps.googleapis.com/maps/api/streetview"

/// Request uri to fetch google data
let getStreetUri reqtype place =
  let apireq = 
      match reqtype with
      | Picture _ -> "?"
      | Metadata -> "/metadata?"
  match place with
  | PanoId panoid -> googleMapsStreetviewBaseUrl + apireq + "pano=" + 
                        panoid + "&size=500x500&key=" + google_api_key
  | Location place -> googleMapsStreetviewBaseUrl + apireq + "location=" + 
                        (System.Web.HttpUtility.UrlEncode place) + "&size=500x500&key=" + google_api_key
  | Unknown -> ""

/// Parse kml: Address name, location
let parseKmlAddresses (filePath:string) =
    //let filePath = @"C:\Users\...\Downloads\myMap.kml"
    if not (System.IO.File.Exists filePath) then
        failwith ("File not found: " + filePath)
    else
    let text = System.IO.File.ReadAllText filePath
    let data = KmlSchema.Parse text
    let places1 =
        data.Document.Placemarks 
        |> Array.map(fun p -> 
            p.Name,
            match p.Address with
            | Some x -> x
            | None ->
                match p.Point with
                | Some point -> point.Coordinates.Replace("\r","").Replace("\n","").Trim()
                | None -> p.Name)
    let places2 = 
        data.Document.Folders
        |> Array.map(fun f ->
            f.Placemarks 
            |> Array.map(fun p -> 
                p.Name,
                match p.Address with
                | Some x -> x
                | None -> 
                    match p.Point with
                    | Some point -> point.Coordinates.Replace("\r","").Replace("\n","").Trim()
                    | None -> p.Name))
    Array.concat [|places1; Array.concat places2|]

/// Read place names from a KLM file and export them to CSV file
/// File is generated to __SOURCE_DIRECTORY__
let kmlToCsv kmlPath = 
    let csvPath = "exportPlaceNames.csv"
    let places = parseKmlAddresses kmlPath
    System.IO.File.WriteAllText(csvPath, "Continent;Country;Place;Location\r\n", Text.Encoding.Unicode)
    System.IO.File.AppendAllLines(csvPath, places |> Seq.map(fun (p,l) -> p + ";" + l), Text.Encoding.Unicode)
    csvPath

/// Ask Google StreetView pictures or panoid metadata from place
let getLocationData reqtype place =
    let uri = getStreetUri reqtype place
    use cli = new System.Net.WebClient()
    match reqtype with
    | Metadata ->
        let res = cli.DownloadString uri
        let r = StreetviewMeta.Parse res
        (match r.PanoId with Some v -> v | None -> ""), 
        (match r.Location with Some v -> v.Lat, v.Lng | None -> 0m,0m)
    | Picture name ->
        let file = "streetview-" + 
                    (System.Web.HttpUtility.UrlEncode (name + "_" + place.ToString())) + 
                    "_" + DateTime.UtcNow.ToString("yyMMddhhmmss") + ".jpg"
        cli.DownloadFile(uri, file)
        file, (0m,0m)
        
/// Creates a single Wander bookmark based on place name
let wanderBookmarkItem place =
    let panoid, _ = getLocationData Metadata place
    if String.IsNullOrEmpty panoid then None
    else Some (WanderSchema.FolderContent(Some panoid, 
                place.ToString(), 
                false, DateTime.UtcNow.ToBinary(), [||]))

/// Creates new Wander_Favourites with places from the place names array
let wanderFavourites (placeNames:string[]) =
    let favouritesFile = "Wander_Favorites.json"
    let folderContent = 
        placeNames 
        |> Array.map(fun placeName -> wanderBookmarkItem (Location placeName))
        |> Array.filter(fun place -> place.IsSome)
        |> Array.map(fun place -> place.Value)
    let wanderContent = 
            "[" + 
            WanderSchema.Root("Import", true, folderContent, DateTime.UtcNow.ToBinary(), None)
                .JsonValue.ToString(FSharp.Data.JsonSaveOptions.DisableFormatting)
                .Replace(",\"folderContents\":[]","") + "]"
    System.IO.File.WriteAllText(favouritesFile,wanderContent)
    favouritesFile

/// Export all places from KLM file as Wander bookmark file
/// Requests n times Google API.
let kmlToWander kmlPath =
    let favouritesFile = "Wander_Favorites.json"
    let places = parseKmlAddresses kmlPath
    let folderContent = 
        places 
        |> Array.map(fun (p,_) -> wanderBookmarkItem (Location p))
        |> Array.filter(fun p -> p.IsSome)
        |> Array.map(fun p -> p.Value)
    let wanderContent = 
            "[" +
            WanderSchema.Root("Import", true, folderContent, DateTime.UtcNow.ToBinary(), None)
                            .JsonValue.ToString(FSharp.Data.JsonSaveOptions.DisableFormatting)
                            .Replace(",\"folderContents\":[]","") + "]"
    System.IO.File.WriteAllText(favouritesFile,wanderContent)
    favouritesFile

/// Fetch pictures for Wander bookmarks.
/// Requests n times Google API.
let extractWanderPictures (wanderBookmarks:string) =
    //let wanderBookmarks = @"C:\Users\...\Documents\Wander_Favorites.json"
    //from: \Quest 2\Internal shared storage\Android\data\com.parkline.wander\files
    let wss = WanderSchema.Load wanderBookmarks
    wss |> Array.iter(fun ws ->
        match ws.IsFolder with
        | false ->
            match ws.Panoid with
            | Some panoid -> 
                let file,_ = getLocationData (Picture ws.Title) (PanoId panoid)
                ()
            | None -> ()
        | true ->
            ws.FolderContents 
            |> Array.iter(fun b -> 
                match b.IsFolder with
                | false ->
                    match b.Panoid with
                    | Some panoid -> 
                        let files,_ = getLocationData (Picture b.Title) (PanoId panoid)
                        ()
                    | None -> ()
                | true -> // let's support only 2 nested folders for now
                    b.FolderContents
                    |> Array.filter(fun bb -> not bb.IsFolder)
                    |> Array.iter(fun bb -> 
                        let files,_ = getLocationData (Picture bb.Title) (PanoId bb.Panoid)
                        ())
            )
    )

let wanderToKlm (wanderBookmarks:string) =
    let kmlFile = "klm_Import.kml"
    let wss = WanderSchema.Load wanderBookmarks
    let places =
        seq {
            for ws in wss do
                match ws.IsFolder with
                | false ->
                    yield ws.Title
                | true ->
                    for b in ws.FolderContents do
                        match b.IsFolder with
                        | false ->
                            yield b.Title
                        | true -> // let's support only 2 nested folders for now
                            for bb in
                                b.FolderContents
                                |> Array.filter(fun bb -> not bb.IsFolder) do
                                yield bb.Title
        } |> Seq.toArray
    let places_and_ponts = 
            places |> Array.map(fun place -> 
                let panoid, (lat,lon) = getLocationData Metadata (Location place)
                place, (lon,lat).ToString().Replace("(","").Replace(")","")
            ) |> Array.filter(fun (p,_) -> not(String.IsNullOrEmpty p))
    let placemarks = places_and_ponts 
                     |> Array.map(fun (p,lonlat) -> 
                           KmlSchema.Placemark(p, Some p, None, None, None, Some(KmlSchema.Point lonlat)))
    let kml = KmlSchema.Kml(
                KmlSchema.Document("Imported", 
                                   KmlSchema.Description(), Array.empty, Array.empty, placemarks, Array.empty))
    System.IO.File.WriteAllLines(kmlFile, [|"""<?xml version="1.0" encoding="UTF-8"?>"""|])
    System.IO.File.AppendAllText(kmlFile, kml.ToString())
    kmlFile
<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <name>asdfasdf</name>
    <description/>	
    <Style id="icon-959-DB4436-normal">
      <IconStyle>
        <color>ff3644db</color>
        <scale>1.1</scale>
        <Icon>
          <href>https://asdf.com/asdf.png</href>
        </Icon>
      </IconStyle>
      <LabelStyle>
        <scale>0</scale>
      </LabelStyle>
    </Style>
    <Style id="icon-959-DB4436-normal">
      <IconStyle>
        <color>ff3644db</color>
        <scale>1.1</scale>
        <Icon>
          <href>https://asdf.com/asdf.png</href>
        </Icon>
      </IconStyle>
      <LabelStyle>
        <scale>0</scale>
      </LabelStyle>
      <BalloonStyle>
        <text><![CDATA[<h3>$[name]</h3>]]></text>
      </BalloonStyle>	  
    </Style>
    <StyleMap id="icon-959-DB4436">
      <Pair>
        <key>normal</key>
        <styleUrl>#icon-959-DB4436-normal</styleUrl>
      </Pair>
      <Pair>
        <key>highlight</key>
        <styleUrl>#icon-959-DB4436-highlight</styleUrl>
      </Pair>
    </StyleMap>
    <StyleMap id="icon-959-DB4436">
      <Pair>
        <key>highlight</key>
        <styleUrl>#icon-959-DB4436-highlight</styleUrl>
      </Pair>
    </StyleMap>
    <Placemark>
      <name>Africa;South Africa;Western Cape: Cape Point;</name>
      <address>Africa;South Africa;Western Cape: Cape Point;</address>
      <description><![CDATA[<img src="https://lh3.asdf.com" height="200" width="auto" />]]></description>
      <styleUrl>#icon-959-DB4436</styleUrl>
      <ExtendedData>
        <Data name="gx_media_links">
          <value>asdf https://asdf.com</value>
        </Data>
      </ExtendedData>
    </Placemark>
    <Placemark>
      <name>Africa;South Africa;Western Cape: Cape Town Central;</name>
      <address>Africa;South Africa;Western Cape: Cape Town Central;</address>
      <styleUrl>#icon-959-DB4436-nodesc</styleUrl>
    </Placemark>
    <Placemark>
      <name><![CDATA[Africa;South Africa;Western Cape: Simon's Town;]]></name>
      <address><![CDATA[Africa;South Africa;Western Cape: Simon's Town;]]></address>
      <styleUrl>#icon-959-DB4436-nodesc</styleUrl>
    </Placemark>
    <Placemark>
      <name>Asia;Japan;Tokyo Prefecture: Chiyoda;</name>
    </Placemark>
    <Placemark>
        <name>Piste 1</name>
        <styleUrl>#icon-1899-0288D1-nodesc</styleUrl>
        <Point>
          <coordinates>
            24.974672,60.1915652,0
          </coordinates>
        </Point>
    </Placemark>
    <Placemark>
      <name>Asia;Malaysia;Sabah: Kota Kinabalu;</name>
      <address>Asia;Malaysia;Sabah: Kota Kinabalu;</address>
      <description><![CDATA[<img src="https://asdf.com" height="200" width="auto" /><br><br><img src="https://asdf.com" height="200" width="auto" /><br><br><img src="asdf.com" height="200" width="auto" /><br><br><img src="https://asdf.com" height="200" width="auto" />]]></description>
      <styleUrl>#icon-959-DB4436</styleUrl>
      <ExtendedData>
        <Data name="gx_media_links">
          <value>asdf https://asdf.com https://asdf.com https://asdf.com https://asdf.com</value>
        </Data>
      </ExtendedData>
    </Placemark>
    <Folder>
      <name>asdf</name>
	  <Placemark>
		  <name>Asia;Japan;Tokyo Prefecture: Chiyoda;</name>
		  <address>Africa;South Africa;Western Cape: Cape Point;</address>
	  </Placemark>
	  <Placemark>
		  <name>Asia;Japan;Tokyo Prefecture: Chiyoda;</name>
	  </Placemark>
    </Folder>
    <Folder>
      <name>asdf</name>
      <Placemark>
        <name>Piste 1</name>
        <styleUrl>#icon-1899-0288D1-nodesc</styleUrl>
        <Point>
          <coordinates>
            24.974672,60.1915652,0
          </coordinates>
        </Point>
      </Placemark>
    </Folder>
  </Document>
</kml>

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.