Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add export to KML support #21

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Widget.html
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,11 @@ <h2>${nls.listDrawTitle}</h2>
<span title="${nls.copyLabel}" class="copy blue-button" data-dojo-attach-event="onclick:copy">&nbsp;</span>
<span title="${nls.deleteAllLabel}" class="clear red-button" data-dojo-attach-event="onclick:clear">&nbsp;</span>
<span title="${nls.exportLabel}" class="export blue-button" data-dojo-attach-event="onclick:exportSelectionInFile">&nbsp;</span>
<span title="${nls.exportLabel} as KML" class="export orange-button kml-label" data-dojo-attach-event="onclick:exportKmlFile">KML</span>
</div>
<span title="${nls.importTitle}" class="import-button blue-button" data-dojo-attach-event="onclick:launchImportFile">&nbsp;</span>
</div>
</div>
</div>
</div>
</div>
</div>
33 changes: 31 additions & 2 deletions Widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,16 @@ define([
'jimu/dijit/DrawBox',
'jimu/dijit/Message',
'jimu/utils',
'jimu/symbolUtils'
'jimu/symbolUtils',
'./keyhole'
],
function(
declare, _WidgetsInTemplateMixin, BaseWidget,
Deferred, aspect, lang, on, html, has, Color, array, domConstruct, dom, Select, NumberSpinner, localStore,
esriConfig, InfoTemplate, Graphic, graphicsUtils, GraphicsLayer, Edit,
esriUnits, SpatialReference, Polyline, Polygon, geometryEngine, projection,
SimpleMarkerSymbol, SimpleLineSymbol, SimpleFillSymbol, TextSymbol, Font,
exportUtils, ViewStack, SymbolChooser, DrawBox, Message, jimuUtils, jimuSymbolUtils
exportUtils, ViewStack, SymbolChooser, DrawBox, Message, jimuUtils, jimuSymbolUtils, keyhole
) {

/*jshint unused: false*/
Expand Down Expand Up @@ -388,6 +389,21 @@ function(
return content;
},

drawingsGetKml: function(asString, onlyChecked) {
var graphics = (onlyChecked) ? this.getCheckedGraphics(false) : this.drawBox.drawLayer.graphics;

if (!graphics.length)
return asString ? "" : false;

var doc = keyhole.graphicsToKml(graphics);

if (asString) {
return new XMLSerializer().serializeToString(doc);
}

return doc;
},

///////////////////////// MENU METHODS ///////////////////////////////////////////////////////////
menuOnClickAdd: function() {
this.setMode("add1");
Expand Down Expand Up @@ -1178,6 +1194,19 @@ function(
this.launchExport(true);
},

exportKmlFile: function(evt) {
if (evt && evt.preventDefault)
evt.preventDefault();
var text = this.drawingsGetKml(true, true);
if (!text) {
this.showMessage(this.nls.importWarningNoExport0Draw, 'warning');
return false;
}
var filename = (this.config.exportFileName ? this.config.exportFileName : "myDrawings") + '.kml';
var blob = new Blob([text], {type: 'application/vnd.google-earth.kml+xml;charset=utf-8'});
saveAs(blob, filename, true);
},

launchExport: function(only_graphics_checked) {
var drawing_json = this.drawingsGetJson(false, only_graphics_checked);

Expand Down
11 changes: 10 additions & 1 deletion css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,15 @@
.jimu-widget-edraw .grey-button:hover {
background-color: rgba(153, 153, 153, 1);
}
.jimu-widget-edraw .orange-button {
background-color: rgba(220, 160, 0, 0.7);
}
.jimu-widget-edraw .orange-button:hover {
background-color: rgba(220, 160, 0, 1);
}
.jimu-widget-edraw span.kml-label {
font-size: 8px;
}
.jimu-widget-edraw .symbol-set-table {
width: 100%;
}
Expand Down Expand Up @@ -481,4 +490,4 @@
}
.eDraw-import-message:hover .eDraw-import-draganddrop-message{
display:block;
}
}
244 changes: 244 additions & 0 deletions keyhole.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/*
* BSD 3-Clause License
*
* Copyright (c) 2021, 8minute Solar Energy LLC
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

/* jshint esversion: 6 */

define([
'esri/SpatialReference',
'esri/geometry/projection',
],
function(SpatialReference, projection) {

class Node {
/* Class to simplify creation of XML documents.
*
* Eliminates repetitive code that makes it difficult to read.
* Methods and getters return Node objects to allow call chaining.
*/

constructor(elem) {
// elem should be an XML DOM Element object
this.elem = elem;
}

// Helpers to return the parent (enclosing) node
get parent() { return new Node(this.elem.parentNode); }
get end() { return new Node(this.elem.parentNode); }

using(func) {
// Call function with the current node
func(this);
return this;
}

forEach(items, func) {
// Call func(node, item) for each item in array it
items.forEach(function(item) { func(this, item); }, this);
return this;
}

add(name) {
// Append a new child element to the node
//
// name is the tag name to append. Namespace tags are supported using
// `ns:name`, but the namespace must already exist on the document.

let doc = this.elem.ownerDocument;
let index = name.indexOf(':');
let xmlns = index == -1 ? doc.documentElement.namespaceURI :
doc.documentElement.getAttribute(`xmlns:${name.substring(0, 2)}`);
let elem = doc.createElementNS(xmlns, name);
this.elem.appendChild(elem);
return new Node(elem);
}

dropIfEmpty() {
// Drop the node if no child nodes were added
let parent = this.elem.parentNode;
if (this.elem.firstChild === null) {
parent.removeChild(this.elem);
}
return new Node(parent);
}

dropUnused() {
// Drop a container node if there are insufficient child nodes
let parent = this.elem.parentNode;
let children = this.elem.childNodes;
if (children.length < 2) {
for (let child of children) {
this.elem.removeChild(child);
parent.appendChild(child);
}
parent.removeChild(this.elem);
}
return new Node(parent);
}

text(text) {
// Add and return a text node
this.elem.appendChild(this.elem.ownerDocument.createTextNode(text));
return this;
}

cdata(text) {
// Add and return a CDATA node
this.elem.appendChild(this.elem.ownerDocument.createCDATASection(text));
return this;
}

attr(name, value) {
// Set an attribute on the node
this.elem.setAttribute(name, value);
return this;
}
}


function colorToHex(color) {
// Convert ArcGIS color to KML hex color
return [~~(color.a * 255), color.b, color.g, color.r].map(function(value) {
let s = value.toString(16);
return '0'.repeat(2 - s.length) + s;
}).join('');
}


function symbolStyle(node, symbol) {
// Add style to node to match symbol as best as possible
switch (symbol.type) {
case 'simplelinesymbol':
node.add('LineStyle').
add('color').text(colorToHex(symbol.color)).end.
add('width').text(symbol.width * 4 / 3);
break;

case 'simplefillsymbol':
node.add('PolyStyle').
add('color').text(colorToHex(symbol.color)).end.
add('fill').text('1').end.
add('outline').text('1');
symbolStyle(node, symbol.outline);
break;

case 'picturemarkersymbol':
node.add('IconStyle').
add('Icon').
add('href').text(symbol.imageData || symbol.url);
break;

case 'simplemarkersymbol':
node.add('IconStyle').
add('color').text(colorToHex(symbol.color));
break;

case 'textsymbol':
node.add('LabelStyle').
add('color').text(colorToHex(symbol.color));
break;

case 'picturefillsymbol':
default:
break;
}
}


return {
graphicsToKml: function(graphics) {
// Convert a list of graphics objects to an KML document and return XMLDocument object.
//
// Returns false if no graphics are given.

if (!graphics.length)
return null;

const wgs84 = SpatialReference({wkid: 4326});

let doc = (new DOMParser()).parseFromString(`<?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"></kml>`, 'application/xml');
let root = new Node(doc.documentElement);

root.add('Document').
add('name').text('Drawings').end.
forEach(graphics, function(node, graphic) {
let geometry = graphic.geometry;
if (geometry.spatialReference.wkid != wgs84.wkid) {
Promise.resolve(projection.load());
geometry = projection.project(geometry, wgs84);
}

node.add('Placemark').
add('name').text(graphic.attributes.name).end.
add('description').cdata(graphic.attributes.description).end.
add('Style').using(function(node) { symbolStyle(node, graphic.symbol); }).dropIfEmpty().
add('MultiGeometry').
using(function(node) {
switch (geometry.type) {
case 'point':
node.add('Point').
add('coordinates').text(`${geometry.x},${geometry.y},${geometry.hasZ ? geometry.z || 0 : 0}`);
break;

case 'polyline':
node.forEach(geometry.paths, function(node, path) {
node.add('LineString').
add('extrude').text('0').end.
add('altitudeMode').text('clampToGround').end.
add('coordinates').text(
path.map(function([x, y, z=0]) { return `${x},${y},${geometry.hasZ ? z : 0}`; }).join(' '));
});
break;

case 'polygon':
node.forEach(geometry.rings, function(node, ring) {
node.add('Polygon').
add('extrude').text('0').end.
add('altitudeMode').text('clampToGround').end.
add('outerBoundaryIs').
add('LinearRing').
add('coordinates').text(
ring.map(function([x, y, z=0]) { return `${x},${y},${geometry.hasZ ? z : 0}`; }).join(' '));
});
break;

default:
break;
}
}).dropUnused();
});

return doc;
},

};
});