mapshaper
Version:
A tool for editing vector datasets for mapping and GIS.
1,734 lines (1,531 loc) • 422 kB
JavaScript
(function () {
var utils = /*#__PURE__*/Object.freeze({
__proto__: null,
get addThousandsSep () { return addThousandsSep; },
get addslashes () { return addslashes; },
get arrayToIndex () { return arrayToIndex; },
get clamp () { return clamp; },
get cleanNumericString () { return cleanNumericString; },
get contains () { return contains; },
get copyElements () { return copyElements; },
get countValues () { return countValues; },
get createBuffer () { return createBuffer; },
get default () { return utils; },
get defaults () { return defaults; },
get difference () { return difference; },
get endsWith () { return endsWith; },
get every () { return every; },
get expandoBuffer () { return expandoBuffer; },
get extend () { return extend; },
get extendBuffer () { return extendBuffer; },
get find () { return find; },
get findMedian () { return findMedian; },
get findQuantile () { return findQuantile; },
get findRankByValue () { return findRankByValue; },
get findStringPrefix () { return findStringPrefix; },
get findValueByPct () { return findValueByPct; },
get findValueByRank () { return findValueByRank; },
get forEach () { return forEach; },
get forEachProperty () { return forEachProperty; },
get format () { return format; },
get formatDateISO () { return formatDateISO; },
get formatIntlNumber () { return formatIntlNumber; },
get formatNumber () { return formatNumber; },
get formatNumberForDisplay () { return formatNumberForDisplay; },
get formatVersionedName () { return formatVersionedName; },
get formatter () { return formatter; },
get genericSort () { return genericSort; },
get getArrayBounds () { return getArrayBounds; },
get getGenericComparator () { return getGenericComparator; },
get getKeyComparator () { return getKeyComparator; },
get getSortedIds () { return getSortedIds; },
get getUniqueName () { return getUniqueName; },
get groupBy () { return groupBy; },
get htmlEscape () { return htmlEscape; },
get indexOf () { return indexOf; },
get indexOn () { return indexOn; },
get inherit () { return inherit; },
get initializeArray () { return initializeArray; },
get intersection () { return intersection; },
get isArray () { return isArray; },
get isArrayLike () { return isArrayLike; },
get isBoolean () { return isBoolean; },
get isDate () { return isDate; },
get isEven () { return isEven; },
get isFiniteNumber () { return isFiniteNumber; },
get isFunction () { return isFunction; },
get isInteger () { return isInteger; },
get isNonNegNumber () { return isNonNegNumber; },
get isNumber () { return isNumber; },
get isObject () { return isObject; },
get isOdd () { return isOdd; },
get isPromise () { return isPromise; },
get isString () { return isString; },
get isValidNumber () { return isValidNumber; },
get lpad () { return lpad; },
get ltrim () { return ltrim; },
get mean () { return mean; },
get merge () { return merge; },
get mergeNames () { return mergeNames; },
get numToStr () { return numToStr; },
get parseIntlNumber () { return parseIntlNumber; },
get parseNumber () { return parseNumber; },
get parsePercent () { return parsePercent; },
get parseString () { return parseString; },
get pickOne () { return pickOne; },
get pluck () { return pluck; },
get pluralSuffix () { return pluralSuffix; },
get promisify () { return promisify; },
get quicksort () { return quicksort; },
get quicksortPartition () { return quicksortPartition; },
get range () { return range; },
get reduceAsync () { return reduceAsync; },
get regexEscape () { return regexEscape; },
get reorderArray () { return reorderArray; },
get repeat () { return repeat; },
get repeatString () { return repeatString; },
get replaceArray () { return replaceArray; },
get rpad () { return rpad; },
get rtrim () { return rtrim; },
get shuffle () { return shuffle; },
get some () { return some; },
get sortArrayIndex () { return sortArrayIndex; },
get sortOn () { return sortOn; },
get splitLines () { return splitLines; },
get sum () { return sum; },
get toArray () { return toArray; },
get toBuffer () { return toBuffer; },
get trim () { return trim; },
get trimQuotes () { return trimQuotes; },
get uniq () { return uniq; },
get uniqifyNames () { return uniqifyNames; },
get wildcardToRegExp () { return wildcardToRegExp; }
});
var api = window.mapshaper; // assuming mapshaper is in global scope
var mapshaper = api,
utils$1 = api.utils,
cli = api.cli,
geom = api.geom,
internal = api.internal,
Bounds = internal.Bounds,
UserError$1 = internal.UserError,
message$1 = internal.message, // stop, error and message are overridden in gui-proxy.js
stop$1 = internal.stop,
error$1 = internal.error;
api.enableLogging();
function CatalogControl(gui, catalog, onSelect) {
var self = this,
container = gui.container.findChild('.file-catalog'),
cols = catalog.cols,
enabled = true,
items = catalog.items,
n = items.length,
row = 0,
html, rows;
this.reset = function() {
enabled = true;
container.removeClass('downloading');
this.progress(-1);
};
this.progress = function() {}; // set by click handler
if (n > 0 === false) {
console.error("Catalog is missing array of items");
return;
}
gui.container.addClass('catalog-mode');
if (!cols) {
cols = Math.ceil(Math.sqrt(n));
}
rows = Math.ceil(n / cols);
html = '<table>';
if (catalog.title) {
html += utils$1.format('<tr><th colspan="%d"><h4>%s</h4></th></tr>', cols, catalog.title);
}
while (row < rows) {
html += renderRow(items.slice(row * cols, row * cols + cols));
row++;
}
html += '</table>';
container.node().innerHTML = html;
gui.container.findChildren('.file-catalog td').forEach(function(el, i) {
el.on('click', function() {
selectItem(el, i);
});
});
// Generate onprogress callback to show a progress indicator
function getProgressFunction(el) {
var visible = false,
i = 0;
return function(pct) {
i++;
if (i == 2 && pct < 0.5) {
// only show progress bar if file will take a while to load
visible = true;
}
if (pct == -1) {
// kludge to reset progress bar
el.removeClass('downloading');
pct = 0;
}
if (visible) {
el.css('background-size', (Math.round(pct * 100) + '% 100%'));
}
};
}
function renderRow(items) {
var tds = items.map(function(o, col) {
var i = row * cols + col;
return renderCell(o, i);
});
return '<tr>' + tds.join('') + '</tr>';
}
function selectItem(el,i) {
var pageUrl = window.location.href.toString().replace(/[?#].*/, '').replace(/\/$/, '') + '/';
var item = items[i];
var urls = item.files.map(function(file) {
var url = (item.url || '') + file;
if (/^http/.test(url) === false) {
// assume relative url
url = pageUrl + '/' + url;
}
return url;
});
if (enabled) { // only respond to first click
self.progress = getProgressFunction(el);
el.addClass('downloading');
container.addClass('downloading');
enabled = false;
onSelect(urls);
}
}
function renderCell(item, i) {
var template = '<td data-id="%d"><h4 class="title">%s</h4><div class="subtitle">%s</div></td>';
return utils$1.format(template, i, item.title, item.subtitle || '');
}
}
function Handler(type, target, callback, listener, priority) {
this.type = type;
this.callback = callback;
this.listener = listener || null;
this.priority = priority || 0;
this.target = target;
}
Handler.prototype.trigger = function(evt) {
if (!evt) {
evt = new EventData(this.type);
evt.target = this.target;
} else if (evt.target != this.target || evt.type != this.type) {
error$1("[Handler] event target/type have changed.");
}
this.callback.call(this.listener, evt);
};
function EventData(type, target, data) {
this.type = type;
this.target = target;
if (data) {
utils$1.defaults(this, data);
this.data = data;
}
}
EventData.prototype.stopPropagation = function() {
this.__stop__ = true;
};
// Base class for objects that dispatch events
function EventDispatcher() {}
// @obj (optional) data object, gets mixed into event
// @listener (optional) dispatch event only to this object
EventDispatcher.prototype.dispatchEvent = function(type, obj, listener) {
var evt;
// TODO: check for bugs if handlers are removed elsewhere while firing
var handlers = this._handlers;
if (handlers) {
for (var i = 0, len = handlers.length; i < len; i++) {
var handler = handlers[i];
if (handler.type == type && (!listener || listener == handler.listener)) {
if (!evt) {
evt = new EventData(type, this, obj);
}
else if (evt.__stop__) {
break;
}
handler.trigger(evt);
}
}
}
};
EventDispatcher.prototype.addEventListener =
EventDispatcher.prototype.on = function(type, callback, context, priority) {
context = context || this;
priority = priority || 0;
var handler = new Handler(type, this, callback, context, priority);
// Insert the new event in the array of handlers according to its priority.
var handlers = this._handlers || (this._handlers = []);
var i = handlers.length;
while (--i >= 0 && handlers[i].priority < handler.priority) {}
handlers.splice(i+1, 0, handler);
return this;
};
// Remove an event handler.
// @param {string} type Event type to match.
// @param {function(BoundEvent)} callback Event handler function to match.
// @param {*=} context Execution context of the event handler to match.
// @return {number} Returns number of handlers removed (expect 0 or 1).
EventDispatcher.prototype.removeEventListener = function(type, callback, context) {
context = context || this;
var count = this.removeEventListeners(type, callback, context);
return count;
};
// Remove event handlers; passing arguments can limit which listeners to remove
// Returns nmber of handlers removed.
EventDispatcher.prototype.removeEventListeners = function(type, callback, context) {
var handlers = this._handlers;
var newArr = [];
var count = 0;
for (var i = 0; handlers && i < handlers.length; i++) {
var evt = handlers[i];
if ((!type || type == evt.type) &&
(!callback || callback == evt.callback) &&
(!context || context == evt.listener)) {
count += 1;
}
else {
newArr.push(evt);
}
}
this._handlers = newArr;
return count;
};
EventDispatcher.prototype.countEventListeners = function(type) {
var handlers = this._handlers,
len = handlers && handlers.length || 0,
count = 0;
if (!type) return len;
for (var i = 0; i < len; i++) {
if (handlers[i].type === type) count++;
}
return count;
};
function getPageXY(el) {
var x = 0, y = 0;
if (el.getBoundingClientRect) {
var box = el.getBoundingClientRect();
x = box.left - pageXToViewportX(0);
y = box.top - pageYToViewportY(0);
}
else {
var fixed = elementIsFixed(el);
while (el) {
x += el.offsetLeft || 0;
y += el.offsetTop || 0;
el = el.offsetParent;
}
if (fixed) {
var offsX = -pageXToViewportX(0);
var offsY = -pageYToViewportY(0);
x += offsX;
y += offsY;
}
}
var obj = {x:x, y:y};
return obj;
}
function elementIsFixed(el) {
// get top-level offsetParent that isn't body (cf. Firefox)
var body = document.body;
var parent;
while (el && el != body) {
parent = el;
el = el.offsetParent;
}
// Look for position:fixed in the computed style of the top offsetParent.
// var styleObj = parent && (parent.currentStyle || window.getComputedStyle && window.getComputedStyle(parent, '')) || {};
var styleObj = parent && getElementStyle(parent) || {};
return styleObj.position == 'fixed';
}
function pageXToViewportX(x) {
return x - window.pageXOffset;
}
function pageYToViewportY(y) {
return y - window.pageYOffset;
}
function getElementStyle(el) {
return el.currentStyle || window.getComputedStyle && window.getComputedStyle(el, '') || {};
}
function getClassNameRxp(cname) {
return new RegExp("(^|\\s)" + cname + "(\\s|$)");
}
function hasClass(el, cname) {
var rxp = getClassNameRxp(cname);
return el && rxp.test(el.className);
}
function addClass(el, cname) {
var classes = el.className;
if (!classes) {
classes = cname;
}
else if (!hasClass(el, cname)) {
classes = classes + ' ' + cname;
}
el.className = classes;
}
function removeClass(el, cname) {
var rxp = getClassNameRxp(cname);
el.className = el.className.replace(rxp, "$2");
}
function replaceClass(el, c1, c2) {
var r1 = getClassNameRxp(c1);
el.className = el.className.replace(r1, '$1' + c2 + '$2');
}
var cssDiv = document.createElement('div');
function mergeCSS(s1, s2) {
cssDiv.style.cssText = s1 + ";" + s2; // extra ';' for ie, which may leave off final ';'
return cssDiv.style.cssText;
}
function addCSS(el, css) {
// console.error(css);
el.style.cssText = mergeCSS(el.style.cssText, css);
}
// Return: HTML node reference or null
// Receive: node reference or id or "#" + id
function getElement(ref) {
var el;
if (typeof ref == 'string') {
if (ref.charAt(0) == '#') {
ref = ref.substr(1);
}
if (ref == 'body') {
el = document.getElementsByTagName('body')[0];
}
else {
el = document.getElementById(ref);
}
}
else if (ref && ref.nodeType !== void 0) {
el = ref;
}
return el || null;
}
function undraggable(el) {
el.ondragstart = function(){return false;};
el.draggable = false;
}
function onload(handler) {
if (document.readyState == 'complete') {
handler();
} else {
window.addEventListener('load', handler);
}
}
var tagOrIdSelectorRE = /^#?[\w-]+$/;
El.__select = function(selector, root) {
root = root || document;
var els;
if (document.querySelectorAll) {
try {
els = root.querySelectorAll(selector);
} catch (e) {
error$1("Invalid selector:", selector);
}
} else {
error$1("This browser doesn't support CSS query selectors");
}
return utils$1.toArray(els);
};
// Converts dash-separated names (e.g. background-color) to camelCase (e.g. backgroundColor)
// Doesn't change names that are already camelCase
//
El.toCamelCase = function(str) {
var cc = str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); });
return cc;
};
El.fromCamelCase = function(str) {
var dashed = str.replace(/([A-Z])/g, "-$1").toLowerCase();
return dashed;
};
El.setStyle = function(el, name, val) {
var jsName = El.toCamelCase(name);
if (el.style[jsName] == void 0) {
return;
}
var cssVal = val;
if (isFinite(val) && val !== null) {
cssVal = String(val); // problem if converted to scientific notation
if (jsName != 'opacity' && jsName != 'zIndex') {
cssVal += "px";
}
}
el.style[jsName] = cssVal;
};
El.findAll = function(sel, root) {
return El.__select(sel, root);
};
function El(ref) {
if (!ref) error$1("Element() needs a reference");
if (ref instanceof El) {
return ref;
}
else if (this instanceof El === false) {
return new El(ref);
}
var node;
if (utils$1.isString(ref)) {
if (ref[0] == '<') {
var parent = El('div').html(ref).node();
node = parent.childNodes.length == 1 ? parent.childNodes[0] : parent;
} else if (tagOrIdSelectorRE.test(ref)) {
node = getElement(ref) || document.createElement(ref); // TODO: detect type of argument
} else {
node = El.__select(ref)[0];
}
} else if (ref.tagName) {
node = ref;
}
if (!node) error$1("Unmatched element selector:", ref);
this.el = node;
}
utils$1.inherit(El, EventDispatcher);
utils$1.extend(El.prototype, {
clone: function() {
var el = this.el.cloneNode(true);
if (el.nodeName == 'SCRIPT') {
// Assume scripts are templates and convert to divs, so children
// can ...
el = El('div').addClass(el.className).html(el.innerHTML).node();
}
el.id = utils$1.getUniqueName();
this.el = el;
return this;
},
node: function() {
return this.el;
},
width: function() {
return this.el.offsetWidth;
},
height: function() {
return this.el.offsetHeight;
},
top: function() {
return this.el.offsetTop;
},
left: function() {
return this.el.offsetLeft;
},
// Apply inline css styles to this Element, either as string or object.
css: function(css, val) {
if (utils$1.isObject(css)) {
utils$1.forEachProperty(css, function(val, key) {
El.setStyle(this.el, key, val);
}, this);
} else if (val === void 0) {
addCSS(this.el, css);
} else {
El.setStyle(this.el, css, val);
}
return this;
},
attr: function(obj, value) {
if (utils$1.isString(obj)) {
if (arguments.length == 1) {
return this.el.getAttribute(obj);
}
if (value === null) {
this.el.removeAttribute(obj);
} else {
this.el.setAttribute(obj, value);
}
}
return this;
},
remove: function(sel) {
if (this.el.parentNode) this.el.parentNode.removeChild(this.el);
return this;
},
addClass: function(className) {
addClass(this.el, className);
return this;
},
removeClass: function(className) {
removeClass(this.el, className);
return this;
},
classed: function(className, b) {
this[b ? 'addClass' : 'removeClass'](className);
return this;
},
hasClass: function(className) {
return hasClass(this.el, className);
},
toggleClass: function(cname) {
if (this.hasClass(cname)) {
this.removeClass(cname);
} else {
this.addClass(cname);
}
},
computedStyle: function() {
return getElementStyle(this.el);
},
visible: function() {
if (this._hidden !== undefined) {
return !this._hidden;
}
var style = this.computedStyle();
return style.display != 'none' && style.visibility != 'hidden';
},
hide: function(css) {
if (this.visible()) {
this.css('display:none;');
this._hidden = true;
}
return this;
},
show: function(css) {
var tag = this.el && this.el.tagName;
if (!this.visible()) {
this.css('display', tag == 'SPAN' ? 'inline-block' : 'block');
this._hidden = false;
}
return this;
},
html: function(html) {
if (arguments.length == 0) {
return this.el.innerHTML;
} else {
this.el.innerHTML = html;
return this;
}
},
text: function(str) {
this.html(utils$1.htmlEscape(str));
return this;
},
// Shorthand for attr('id', <name>)
id: function(id) {
if (id) {
this.el.id = id;
return this;
}
return this.el.id;
},
findChild: function(sel) {
var node = El.__select(sel, this.el)[0];
return node ? new El(node) : null;
},
findChildren: function(sel) {
return El.__select(sel, this.el).map(El);
},
appendTo: function(ref) {
var parent = ref instanceof El ? ref.el : getElement(ref);
if (this._sibs) {
for (var i=0, len=this._sibs.length; i<len; i++) {
parent.appendChild(this._sibs[i]);
}
}
parent.appendChild(this.el);
return this;
},
nextSibling: function() {
return this.el.nextSibling ? new El(this.el.nextSibling) : null;
},
firstChild: function() {
var ch = this.el.firstChild;
while (ch.nodeType != 1) { // skip text nodes
ch = ch.nextSibling;
}
return new El(ch);
},
appendChild: function(ref) {
var el = El(ref);
this.el.appendChild(el.el);
return this;
},
newChild: function(tagName) {
var ch = document.createElement(tagName);
this.el.appendChild(ch);
return new El(ch);
},
// Traverse to parent node
parent: function() {
var p = this.el && this.el.parentNode;
return p ? new El(p) : null;
},
findParent: function(tagName) {
var p = this.el && this.el.parentNode;
if (tagName) {
tagName = tagName.toUpperCase();
while (p && p.tagName != tagName) {
p = p.parentNode;
}
}
return p ? new El(p) : null;
},
// Remove all children of this element
empty: function() {
this.el.innerHTML = '';
return this;
}
});
// use DOM handler for certain events
// TODO: find a better way distinguising DOM events and other events registered on El
// e.g. different methods
//
//El.prototype.__domevents = utils.arrayToIndex("click,mousedown,mousemove,mouseup".split(','));
El.prototype.__on = El.prototype.on;
El.prototype.on = function(type, func) {
if (this.constructor == El) {
this.el.addEventListener(type, func);
} else {
this.__on.apply(this, arguments);
}
return this;
};
El.prototype.__removeEventListener = El.prototype.removeEventListener;
El.prototype.removeEventListener = function(type, func) {
if (this.constructor == El) {
this.el.removeEventListener(type, func);
} else {
this.__removeEventListener.apply(this, arguments);
}
return this;
};
var GUI = {};
GUI.isActiveInstance = function(gui) {
return gui == GUI.__active;
};
GUI.getPixelRatio = function() {
var deviceRatio = window.devicePixelRatio || window.webkitDevicePixelRatio || 1;
return deviceRatio > 1 ? 2 : 1;
};
GUI.browserIsSupported = function() {
return typeof ArrayBuffer != 'undefined' &&
typeof Blob != 'undefined' && typeof File != 'undefined';
};
GUI.exportIsSupported = function() {
return typeof URL != 'undefined' && URL.createObjectURL &&
typeof document.createElement("a").download != "undefined" ||
!!window.navigator.msSaveBlob;
};
// TODO: make this relative to a single GUI instance
GUI.canSaveToServer = function() {
return !!(mapshaper.manifest && mapshaper.manifest.allow_saving) && typeof fetch == 'function';
};
GUI.setSavedValue = function(name, val) {
try {
window.localStorage.setItem(name, JSON.stringify(val));
} catch(e) {}
};
GUI.getSavedValue = function(name) {
try {
return JSON.parse(window.localStorage.getItem(name));
} catch(e) {}
return null;
};
GUI.getUrlVars = function() {
var q = window.location.search.substring(1);
return q.split('&').reduce(function(memo, chunk) {
var pair = chunk.split('=');
var key = decodeURIComponent(pair[0]);
memo[key] = parseVal(pair[1]);
return memo;
}, {});
function parseVal(val) {
var str = val ? decodeURIComponent(val) : 'true';
if (str == 'true' || str == 'false') return JSON.parse(str);
return str;
}
};
// Assumes that URL path ends with a filename
GUI.getUrlFilename = function(url) {
var path = /\/\/([^#?]+)/.exec(url);
var file = path ? path[1].split('/').pop() : '';
return file;
};
GUI.formatMessageArgs = function(args) {
// .replace(/^\[[^\]]+\] ?/, ''); // remove cli annotation (if present)
return internal.formatLogArgs(args);
};
GUI.handleDirectEvent = function(cb) {
return function(e) {
if (e.target == this) cb();
};
};
GUI.getInputElement = function() {
var el = document.activeElement;
return (el && (el.tagName == 'INPUT' || el.contentEditable == 'true')) ? el : null;
};
GUI.textIsSelected = function() {
return !!GUI.getInputElement();
};
GUI.selectElement = function(el) {
var range = document.createRange(),
sel = window.getSelection();
range.selectNodeContents(el);
sel.removeAllRanges();
sel.addRange(range);
};
GUI.blurActiveElement = function() {
var el = GUI.getInputElement();
if (el) el.blur();
};
// Filter out delayed click events, e.g. so users can highlight and copy text
// Filter out context menu clicks
GUI.onClick = function(el, cb) {
var time;
el.on('mousedown', function() {
time = +new Date();
});
el.on('mouseup', function(e) {
if (looksLikeContextClick(e)) {
return;
}
if (+new Date() - time < 300) {
cb(e);
}
});
};
GUI.onContextClick = function(el, cb) {
el.on('mouseup', function(e) {
if (looksLikeContextClick(e)) {
e.stopPropagation();
e.preventDefault();
cb(e);
}
});
};
function looksLikeContextClick(e) {
return e.button > 1 || e.ctrlKey;
}
// tests if filename is a type that can be used
// GUI.isReadableFileType = function(filename) {
// return !!internal.guessInputFileType(filename) || internal.couldBeDsvFile(filename) ||
// internal.isZipFile(filename);
// };
GUI.parseFreeformOptions = function(raw, cmd) {
var str = raw.trim(),
parsed;
if (!str) {
return {};
}
if (!/^-/.test(str)) {
str = '-' + cmd + ' ' + str;
}
parsed = internal.parseCommands(str);
if (!parsed.length || parsed[0].name != cmd) {
stop$1("Unable to parse command line options");
}
return parsed[0].options;
};
// Convert an options object to a command line options string
// (used by gui-import-control.js)
// TODO: handle options with irregular string <-> object conversion
GUI.formatCommandOptions = function(o) {
var arr = [];
Object.keys(o).forEach(function(key) {
var name = key.replace(/_/g, '-');
var val = o[key];
var str;
// TODO: quote values that contain spaces
if (Array.isArray(val)) {
str = name + '=' + val.join(',');
} else if (val === true) {
str = name;
} else if (val === false) {
return;
} else {
str = name + '=' + val;
}
arr.push(str);
});
return arr.join(' ');
};
// TODO: switch all ClickText to ClickText2
// @ref Reference to an element containing a text node
function ClickText2(ref) {
var self = this;
var selected = false;
var el = El(ref).on('mousedown', init);
function init() {
el.removeEventListener('mousedown', init);
el.attr('contentEditable', true)
.attr('spellcheck', false)
.attr('autocorrect', false)
.on('focus', function(e) {
el.addClass('editing');
selected = false;
}).on('blur', function(e) {
el.removeClass('editing');
self.dispatchEvent('change');
window.getSelection().removeAllRanges();
}).on('keydown', function(e) {
if (e.keyCode == 13) { // enter
e.stopPropagation();
e.preventDefault();
this.blur();
}
}).on('click', function(e) {
if (!selected && window.getSelection().isCollapsed) {
GUI.selectElement(el.node());
}
selected = true;
e.stopPropagation();
});
}
this.value = function(str) {
if (utils$1.isString(str)) {
el.node().textContent = str;
} else {
return el.node().textContent;
}
};
}
utils$1.inherit(ClickText2, EventDispatcher);
// @ref reference to a text input element
function ClickText(ref) {
var _el = El(ref);
var _self = this;
var _max = Infinity,
_min = -Infinity,
_formatter = function(v) {return String(v);},
_validator = function(v) {return !isNaN(v);},
_parser = function(s) {return parseFloat(s);},
_value = 0;
_el.on('blur', onblur);
_el.on('keydown', onpress);
function onpress(e) {
if (e.keyCode == 27) { // esc
_self.value(_value); // reset input field to current value
_el.el.blur();
} else if (e.keyCode == 13) { // enter
_el.el.blur();
}
}
// Validate input contents.
// Update internal value and fire 'change' if valid
//
function onblur() {
var val = _parser(_el.el.value);
if (val === _value) {
// return;
}
if (_validator(val)) {
_self.value(val);
_self.dispatchEvent('change', {value:_self.value()});
} else {
_self.value(_value);
_self.dispatchEvent('error'); // TODO: improve
}
}
this.bounds = function(min, max) {
_min = min;
_max = max;
return this;
};
this.validator = function(f) {
_validator = f;
return this;
};
this.formatter = function(f) {
_formatter = f;
return this;
};
this.parser = function(f) {
_parser = f;
return this;
};
this.text = function() {return _el.el.value;};
this.value = function(arg) {
if (arg == void 0) {
// var valStr = this.el.value;
// return _parser ? _parser(valStr) : parseFloat(valStr);
return _value;
}
var val = utils$1.clamp(arg, _min, _max);
if (!_validator(val)) {
error$1("ClickText#value() invalid value:", arg);
} else {
_value = val;
}
_el.el.value = _formatter(val);
return this;
};
}
utils$1.inherit(ClickText, EventDispatcher);
function Checkbox(ref) {
var _el = El(ref);
}
utils$1.inherit(Checkbox, EventDispatcher);
function SimpleButton(ref) {
var _el = El(ref),
_active = !_el.hasClass('disabled');
_el.active = function(a) {
if (a === void 0) return _active;
if (a !== _active) {
_active = a;
_el.toggleClass('disabled');
}
return _el;
};
// this.node = function() {return _el.node();};
function isVisible() {
var el = _el.node();
return el.offsetParent !== null;
}
return _el;
}
function filterLayerByIds(lyr, ids) {
var shapes;
if (lyr.shapes) {
shapes = ids.map(function(id) {
return lyr.shapes[id];
});
return utils$1.defaults({shapes: shapes, data: null}, lyr);
}
return lyr;
}
function formatLayerNameForDisplay(name) {
return name || '[unnamed]';
}
function cleanLayerName(raw) {
return raw.replace(/[\n\t/\\]/g, '')
.replace(/^[.\s]+/, '').replace(/[.\s]+$/, '');
}
function updateLayerStackOrder(layers) {
// 1. assign ascending ids to unassigned layers above the range of other layers
layers.forEach(function(o, i) {
if (!o.layer.menu_order) o.layer.menu_order = 1e6 + i;
});
// 2. sort in ascending order
layers.sort(function(a, b) {
return a.layer.menu_order - b.layer.menu_order;
});
// 3. assign consecutve ids
layers.forEach(function(o, i) {
o.layer.menu_order = i + 1;
});
return layers;
}
function sortLayersForMenuDisplay(layers) {
layers = updateLayerStackOrder(layers);
return layers.reverse();
}
function setLayerPinning(lyr, pinned) {
lyr.pinned = !!pinned;
}
function calcDotScale(layers, ext) {
var bbox = ext.getBounds().scale(1.3).toArray(); // add buffer
// var topTier = 50000; // can be a bottleneck
var topTier = 10000; // short-circuit counting here
var count = 0;
layers = layers.filter(function(lyr) {
return lyr.geometry_type == 'point' && lyr.gui.style?.dotSize > 0;
});
layers.forEach(function(lyr) {
count += countPoints(lyr.gui.displayLayer.shapes, topTier, bbox);
});
count = Math.min(topTier, count) || 1;
var k = Math.pow(5 - utils$1.clamp(Math.log10(count), 1, 4), 1.25);
// zoom adjustments
var mapScale = ext.scale();
if (mapScale < 0.5) {
k *= Math.pow(mapScale + 0.5, 0.35);
} else if (mapScale > 1) {
// scale faster at first
k *= Math.pow(Math.min(mapScale, 4), 0.25);
k *= Math.pow(mapScale, 0.02);
}
// scale down when map is small
var smallSide = Math.min(ext.width(), ext.height());
k *= utils$1.clamp(smallSide / 500, 0.5, 1);
return k;
}
function countPoints(shapes, max, bbox) {
var count = 0;
var shp, p;
// short-circuit point counting above top threshold
for (var i=0, n=shapes.length; i<n && count<max; i++) {
shp = shapes[i];
for (var j=0, m=(shp ? shp.length : 0); j<m; j++) {
p = shp[j];
if (p[0] > bbox[0] && p[0] < bbox[2] && p[1] > bbox[1] && p[1] < bbox[3]) {
count ++;
}
}
}
return count;
}
async function showPrompt(msg, title) {
var popup = showPopupAlert(msg, title);
return new Promise(function(resolve) {
popup.onCancel(function() {
resolve(false);
});
popup.button('Yes', function() {
resolve(true);
});
popup.button('No', function() {
resolve(false);
});
});
}
function showPopupAlert(msg, title, optsArg) {
var opts = optsArg || {};
var self = {}, html = '';
var _cancel, _close;
var warningRxp = /^Warning: /;
var el = El('div').appendTo('body').addClass('alert-wrapper')
.classed('non-blocking', opts.non_blocking);
var infoBox = El('div').appendTo(el).addClass('alert-box info-box selectable');
El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() {
if (_cancel) _cancel();
self.close();
});
if (opts.max_width) {
infoBox.node().style.maxWidth = opts.max_width;
}
var container = El('div').appendTo(infoBox);
if (!title && warningRxp.test(msg)) {
title = 'Warning';
msg = msg.replace(warningRxp, '');
}
if (title) {
El('div').addClass('alert-title').text(title).appendTo(container);
}
var content = El('div').appendTo(infoBox);
if (msg) {
content.html(`<p class="alert-message">${msg}</p>`);
}
self.container = function() { return content; };
self.onCancel = function(cb) {
_cancel = cb;
return self;
};
self.onClose = function(cb) {
_close = cb;
return self;
};
self.button = function(label, cb) {
El('div')
.addClass("btn dialog-btn alert-btn")
.appendTo(infoBox)
.html(label)
.on('click', function() {
self.close();
cb();
});
return self;
};
self.close = function(action) {
var ms = 0;
var _el = el;
if (action == 'fade' && _el) {
ms = 1000;
_el.addClass('fade-out');
}
if (_close) _close();
el = _cancel = _close = null;
setTimeout(function() {
if (_el) _el.remove();
}, ms);
};
return self;
}
function AlertControl(gui) {
var openAlert; // error popup
var openPopup; // any popup
var quiet = false;
gui.addMode('alert', function() {}, closePopup);
gui.alert = function(str, title) {
closePopup();
openAlert = openPopup = showPopupAlert(str, title);
openAlert.onClose(gui.clearMode);
gui.enterMode('alert');
};
gui.quiet = function(flag) {
quiet = !!flag;
};
gui.message = function(str, title) {
if (quiet) return;
if (openPopup) return; // don't stomp on another popup
openPopup = showPopupAlert(str, title);
openPopup.onClose(function() {openPopup = null;});
};
function closePopup() {
if (openPopup) openPopup.close();
openPopup = openAlert = null;
}
}
function saveZipFile(zipfileName, files, done) {
internal.zipAsync(files, function(err, buf) {
if (err) {
done(errorMessage(err));
} else {
saveBlobToLocalFile(zipfileName, new Blob([buf]), done);
}
});
function errorMessage(err) {
var str = "Error creating Zip file";
if (err.message) {
str += ": " + err.message;
}
return str;
}
}
function saveFilesToServer(paths, data, done) {
var i = -1;
next();
function next(err) {
i++;
if (err) return done(err);
if (i >= data.length) return done();
saveBlobToServer(paths[i], new Blob([data[i]]), next);
}
}
function saveBlobToServer(path, blob, done) {
var q = '?file=' + encodeURIComponent(path);
var url = window.location.origin + '/save' + q;
window.fetch(url, {
method: 'POST',
credentials: 'include',
body: blob
}).then(function(resp) {
if (resp.status == 400) {
return resp.text();
}
}).then(function(err) {
done(err);
}).catch(function(resp) {
done('connection to server was lost');
});
}
// save file to selected folder if supported, else to downloads
function saveBlobToLocalFile2(filename, blob) {
if (window.showSaveFilePicker) {
saveBlobToSelectedFile(filename, blob);
} else {
saveBlobToDownloadsFolder(filename, blob);
}
}
async function saveBlobToLocalFile(filename, blob, done) {
var chooseDir = GUI.getSavedValue('choose-save-dir');
done = done || function() {};
if (chooseDir) {
saveBlobToSelectedFile(filename, blob, done);
} else {
saveBlobToDownloadsFolder(filename, blob, done);
}
}
function showSaveDialog(filename, blob, done) {
var alert = showPopupAlert(`Save ${filename} to:`)
.button('selected folder', function() {
saveBlobToSelectedFile(filename, blob, done);
})
.button('downloads', function() {
saveBlobToDownloadsFolder(filename, blob, done);
})
.onCancel(done);
}
async function saveBlobToSelectedFile(filename, blob, done) {
// see: https://developer.chrome.com/articles/file-system-access/
// note: saving multiple files to a directory using showDirectoryPicker()
// does not work well (in Chrome). User is prompted for permission each time,
// and some directories (like Downloads and Documents) are blocked.
//
var options = getSaveFileOptions(filename);
var handle;
try {
handle = await window.showSaveFilePicker(options);
var writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
} catch(e) {
if (e.name == 'SecurityError') {
// assuming this is a timeout error, with message like:
// "Must be handling a user gesture to show a file picker."
showSaveDialog(filename, blob, done);
} else if (e.name == 'AbortError') {
// fired if user clicks a cancel button (normal, no error message)
// BUT: this kind of erro rmay also get fired when saving to a protected folder
// (should show error message)
done();
} else {
console.error(e.name, e.message, e);
done('Save failed for an unknown reason');
}
return;
}
done();
}
function getSaveFileOptions(filename) {
// see: https://wicg.github.io/file-system-access/#api-filepickeroptions
var type = internal.guessInputFileType(filename);
var ext = internal.getFileExtension(filename).toLowerCase();
var accept = {};
if (ext == 'kml') {
accept['application/vnd.google-earth.kml+xml'] = ['.kml'];
} else if (ext == 'svg') {
accept['image/svg+xml'] = ['.svg'];
} else if (ext == 'zip') {
accept['application/zip'] == ['.zip'];
} else if (type == 'text') {
accept['text/csv'] = ['.csv', '.tsv', '.tab', '.txt'];
} else if (type == 'json') {
accept['application/json'] = ['.json', '.geojson', '.topojson'];
} else {
accept['application/octet-stream'] = ['.' + ext];
}
return {
suggestedName: filename,
// If startIn is given, Chrome will always start there
// Default is to start in the previously selected dir (better)
// // startIn: 'downloads', // or: desktop, documents, [file handle], [directory handle]
types: [{
description: 'Files',
accept: accept
}]
};
}
function saveBlobToDownloadsFolder(filename, blob, done) {
var anchor, blobUrl;
done = done || function() {};
try {
blobUrl = URL.createObjectURL(blob);
} catch(e) {
done("Mapshaper can't export files from this browser. Try switching to Chrome or Firefox.");
return;
}
anchor = El('a').attr('href', '#').appendTo('body').node();
anchor.href = blobUrl;
anchor.download = filename;
var clickEvent = document.createEvent("MouseEvent");
clickEvent.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false,
false, false, false, 0, null);
anchor.dispatchEvent(clickEvent);
setTimeout(function() {
// Revoke blob url to release memory; timeout needed in firefox
URL.revokeObjectURL(blobUrl);
anchor.parentNode.removeChild(anchor);
done();
}, 400);
}
// Several dependencies are loaded via require()
var f;
if (typeof require == 'function') {
// Node.js context: native require() function
f = require;
} else if (typeof window == 'object' && window.modules) {
// running in web GUI
f = function(name) {
return window.modules[name];
};
} else {
// stub to avoid runtime error in a handful of tests
f = function() {};
}
var require$1 = f;
var idb = require$1('idb-keyval');
// https://github.com/jakearchibald/idb
// https://github.com/jakearchibald/idb-keyval
var sessionId = getUniqId('session');
var snapshotCount = 0;
function getUniqId(prefix) {
return prefix + '_' + (Math.random() + 1).toString(36).substring(2,8);
}
function isSnapshotId(str) {
return /^session_/.test(str);
}
function SessionSnapshots(gui) {
var _menuOpen = false;
var _menuTimeout;
var btn, menu;
init();
async function init() {
btn = gui.buttons.addButton('#ribbon-icon').addClass('menu-btn save-btn');
var enabled = await isStorageEnabled();
if (!enabled) {
btn.remove();
return;
}
menu = El('div').addClass('nav-sub-menu save-menu').appendTo(btn.node());
await initialCleanup();
window.addEventListener('beforeunload', async function() {
// delete snapshot data
// This is not ideal, because the data gets deleted even if the user
// cancels the page close... but there's no apparent good alternative
await finalCleanup();
});
btn.on('mouseenter', function() {
btn.addClass('hover');
clearTimeout(_menuTimeout); // prevent timed closing
if (!_menuOpen) {
openMenu();
}
});
btn.on('mouseleave', function() {
if (!_menuOpen) {
btn.removeClass('hover');
} else {
closeMenu(200);
}
});
}
async function renderMenu() {
var snapshots = await fetchSnapshotList();
menu.empty();
addMenuLink({
slug: 'stash',
// label: 'save data snapshot',
label: 'create a snapshot',
action: saveSnapshot
});
// var available = await getAvailableStorage();
// if (available) {
// El('div').addClass('save-menu-entry').text(available + ' available').appendTo(menu);
// }
// if (snapshots.length > 0) {
// El('div').addClass('save-menu-entry').text('snapshots').appendTo(menu);
// }
snapshots.forEach(function(item, i) {
var line = El('div').appendTo(menu).addClass('save-menu-item');
El('span').appendTo(line).html(`<span class="save-item-label">#${item.number}</span> `);
// show snapshot size
El('span').appendTo(line).html(` <span class="save-item-size">${item.display_size}</span>`);
El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
await restoreSnapshotById(item.id, gui);
closeMenu(100);
}).text('restore');
El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
var obj = await idb.get(item.id);
await internal.compressSnapshotForExport(obj);
var buf = internal.pack(obj);
var fileName = `snapshot-${String(item.number).padStart(2, '0')}.msx`;
// choose output filename and directory every time, if supported
saveBlobToLocalFile2(fileName, new Blob([buf]));
}).text('export');
El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) {
await removeSnapshotById(item.id);
closeMenu(300);
renderMenu();
}).text('remove');
});
}
function addMenuLink(item) {
var line = El('div').appendTo(menu);
var link = El('div').addClass('save-menu-link save-menu-entry').attr('data-name', item.slug).text(item.label).appendTo(line);
link.on('click', async function(e) {
await item.action(gui);
e.stopPropagation();
});
}
function openMenu() {
clearTimeout(_menuTimeout);
if (!_menuOpen) {
btn.addClass('open');
_menuOpen = true;
renderMenu();
}
}
function closeMenu(delay) {
if (!_menuOpen) return;
clearTimeout(_menuTimeout);
_menuTimeout = setTimeout(function() {
_menuOpen = false;
btn.removeClass('open');
btn.removeClass('hover');
}, delay || 0);
}
async function saveSnapshot(gui) {
var obj = await captureSnapshot(gui);
if (!obj) return;
// storing an unpacked object is usually a bit faster (~20%)
// note: we don't know the size of unpacked snapshot objects
// obj = internal.pack(obj);
var entryId = String(++snapshotCount).padStart(3, '0');
var snapshotId = sessionId + '_' + entryId; // e.g. session_d89fw_001
var size = obj.length;
var entry = {
created: Date.now(),
session: sessionId,
id: snapshotId,
name: snapshotCount + '.',
number: snapshotCount,
size: size,
display_size: formatSize(size)
};
await idb.set(entry.id, obj);
await addToIndex(entry);
renderMenu();
}
}
function formatSize(bytes) {
var kb = Math.round(bytes / 1000);
var mb = (bytes / 1e6).toFixed(1);
if (!kb) return '';
if (kb < 990) return kb + 'kB';
return mb + 'MB';
}
async function fetchSnapshotList() {
await removeMissingSnapshots();
var index = await fetchIndex();
var snapshots = index.snapshots;
snapshots = snapshots.filter(function(o) {return o.session == sessionId;});
return snapshots.sort(function(a, b) {b.created > a.created;});
}
async function removeSnapshotById(id, gui) {
await idb.del(id);
return updateIndex(function(index) {
index.snapshots = index.snapshots.filter(function(snap) {
return snap.id != id;
});
});
}
async function restoreSnapshotById(id, gui) {
var data;
try {
data = await internal.restoreSessionData(await idb.get(id));
} catch(e) {
console.error(e);
stop$1('Snapshot is not available');
}
gui.model.clear();
importDatasets(data.datasets, gui);
gui.clearMode();
}
// Add datasets to the current project
// TODO: figure out if interface data should be imported (e.g. should
// visibility flag of imported layers be imported)
async function importSessionData(buf, gui) {
if (buf instanceof ArrayBuffer) {
buf = new Uint8Array(buf);
}
var data = await internal.unpackSessionData(buf);
importDatasets(data.datasets, gui);
}
function importDatasets(datasets, gui) {
gui.model.addDatasets(datasets);
var target = findTargetLayer(datasets);
delete target.layers[0].active; // kludge, active flag only used in snapshots now
gui.model.setDefaultTarget(target.layers, target.dataset);
gui.model.updated({select: true, arc_count: true}); // arc_count to refresh display shapes
}
async function captureSnapshot(gui) {
var lyr = gui.model.getActiveLayer()?.layer;
if (!lyr) return null; // no data -- no snapshot
// compact: true applies compression to vector coordinates, for ~30% reduction
// in file size in a typical polygon or polyline file, but longer processing time
var opts = {compact: false, active_layer: lyr};
var datasets = gui.model.getDatasets();
var obj = await internal.exportDatasetsToPack(datasets, opts);
obj.gui = getGuiState(gui);
return obj;
}
// TODO: capture gui state information to allow restoring more of the UI
function getGuiState(gui) {
return null;
}
async function fetchIndex() {
var index = await idb.get('msx_index');
return index || {snapshots: []};
}
async function updateIndex(action) {
return idb.update('msx_index', function(in