diff --git a/CHANGELOG.md b/CHANGELOG.md index f9a93687..a3b9c635 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +v0.6.42 +* Added "+ add field" button to web ui inspector in attribute editing mode. + + v0.6.41 * Bug fixes diff --git a/package-lock.json b/package-lock.json index 0de1367d..0c8789e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mapshaper", - "version": "0.6.41", + "version": "0.6.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mapshaper", - "version": "0.6.41", + "version": "0.6.42", "license": "MPL-2.0", "dependencies": { "@placemarkio/tokml": "^0.3.3", diff --git a/package.json b/package.json index 43c41cfa..deb9718f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mapshaper", - "version": "0.6.41", + "version": "0.6.42", "description": "A tool for editing vector datasets for mapping and GIS.", "keywords": [ "shapefile", diff --git a/src/cli/mapshaper-command-utils.mjs b/src/cli/mapshaper-command-utils.mjs index 83209929..2bc97fd0 100644 --- a/src/cli/mapshaper-command-utils.mjs +++ b/src/cli/mapshaper-command-utils.mjs @@ -17,6 +17,16 @@ export function applyCommandToEachLayer(func, targetLayers) { }, []); } +export function applyCommandToEachTarget(func, targets) { + var args = utils.toArray(arguments).slice(2); + targets.forEach(function(target) { + var result = func.apply(null, [target].concat(args)); + if (result) { + error('Unexpected output from command'); + } + }); +} + function isLayer(arg) { return arg && (Array.isArray(arg.shapes) || !!arg.data || !!arg.geometry_type); } diff --git a/src/cli/mapshaper-run-command.mjs b/src/cli/mapshaper-run-command.mjs index e220fbc6..d7b6b986 100644 --- a/src/cli/mapshaper-run-command.mjs +++ b/src/cli/mapshaper-run-command.mjs @@ -13,7 +13,7 @@ import { stop, error, UserError, verbose } from '../utils/mapshaper-logging'; import utils from '../utils/mapshaper-utils'; import cmd from '../mapshaper-cmd'; import { stashVar, clearStash } from '../mapshaper-stash'; -import { applyCommandToEachLayer } from '../cli/mapshaper-command-utils'; +import { applyCommandToEachLayer, applyCommandToEachTarget } from '../cli/mapshaper-command-utils'; import '../commands/mapshaper-add-shape'; import '../commands/mapshaper-affine'; @@ -92,7 +92,7 @@ import '../commands/mapshaper-subdivide'; function commandAcceptsMultipleTargetDatasets(name) { return name == 'rotate' || name == 'info' || name == 'proj' || name == 'require' || name == 'drop' || name == 'target' || name == 'if' || name == 'elif' || - name == 'else' || name == 'endif' || name == 'run' || name == 'i'; + name == 'else' || name == 'endif' || name == 'run' || name == 'i' || name == 'snap'; } function commandAcceptsEmptyTarget(name) { @@ -422,7 +422,8 @@ export async function runCommand(command, job) { outputLayers = cmd.sliceLayers(targetLayers, source, targetDataset, opts); } else if (name == 'snap') { - cmd.snap(targetDataset, opts); + // cmd.snap(targetDataset, opts); + applyCommandToEachTarget(targets, opts); } else if (name == 'sort') { applyCommandToEachLayer(cmd.sortFeatures, targetLayers, arcs, opts); diff --git a/src/commands/mapshaper-snap.mjs b/src/commands/mapshaper-snap.mjs index 9dc77cf6..fad15286 100644 --- a/src/commands/mapshaper-snap.mjs +++ b/src/commands/mapshaper-snap.mjs @@ -7,9 +7,10 @@ import { stop, message } from '../utils/mapshaper-logging'; import cmd from '../mapshaper-cmd'; import utils from '../utils/mapshaper-utils'; -cmd.snap = function(dataset, opts) { +cmd.snap = function(target, opts) { var interval = 0; var snapCount = 0; + var dataset = target.dataset; var arcs = dataset.arcs; var arcBounds = arcs && arcs.getBounds(); if (!arcBounds || !arcBounds.hasBounds()) { diff --git a/src/gui/gui-alert.mjs b/src/gui/gui-alert.mjs index b9d9242f..0a757ec4 100644 --- a/src/gui/gui-alert.mjs +++ b/src/gui/gui-alert.mjs @@ -4,21 +4,26 @@ export function showPopupAlert(msg, title) { var self = {}, html = ''; var _cancel, _close; var warningRxp = /^Warning: /; - var el = El('div').appendTo('body').addClass('error-wrapper'); - var infoBox = El('div').appendTo(el).addClass('error-box info-box selectable'); + var el = El('div').appendTo('body').addClass('alert-wrapper'); + 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(); + }); + var container = El('div').appendTo(infoBox); if (!title && warningRxp.test(msg)) { title = 'Warning'; msg = msg.replace(warningRxp, ''); } if (title) { - html += `
${title}
`; + El('div').addClass('alert-title').text(title).appendTo(container); } - html += `

${msg}

`; - El('div').appendTo(infoBox).addClass('close2-btn').on('click', function() { - if (_cancel) _cancel(); - self.close(); - }); - El('div').appendTo(infoBox).addClass('error-content').html(html); + var content = El('div').appendTo(infoBox); + if (msg) { + content.html(`

${msg}

`); + } + + self.container = function() { return content; }; self.onCancel = function(cb) { _cancel = cb; diff --git a/src/gui/gui-popup.mjs b/src/gui/gui-popup.mjs index 583313fd..6a1ce4ad 100644 --- a/src/gui/gui-popup.mjs +++ b/src/gui/gui-popup.mjs @@ -3,6 +3,7 @@ import { utils, internal } from './gui-core'; import { El } from './gui-el'; import { GUI } from './gui-lib'; import { ClickText2 } from './gui-elements'; +import { showPopupAlert } from './gui-alert'; // toNext, toPrev: trigger functions for switching between multiple records export function Popup(gui, toNext, toPrev) { @@ -35,8 +36,7 @@ export function Popup(gui, toNext, toPrev) { // stash a function for refreshing the current popup when data changes // while the popup is being displayed (e.g. while dragging a label) refresh = function() { - var rec = table && (editable ? table.getRecordAt(id) : table.getReadOnlyRecordAt(id)) || {}; - render(content, rec, table, editable); + render(content, id, table, editable); }; refresh(); if (ids && ids.length > 1) { @@ -75,7 +75,8 @@ export function Popup(gui, toNext, toPrev) { tab.show(); } - function render(el, rec, table, editable) { + function render(el, recId, table, editable) { + var rec = table && (editable ? table.getRecordAt(recId) : table.getReadOnlyRecordAt(recId)) || {}; var tableEl = El('table').addClass('selectable'), rows = 0; // self.hide(); // clean up if panel is already open @@ -90,11 +91,6 @@ export function Popup(gui, toNext, toPrev) { } }); - // add new field form - if (editable) { - renderNewRow(tableEl, rec, table); - } - tableEl.appendTo(el); if (rows > 0) { // tableEl.appendTo(el); @@ -119,15 +115,58 @@ export function Popup(gui, toNext, toPrev) { } if (editable) { + // render "add field" button var line = El('div').appendTo(el); El('span').addClass('save-menu-btn').appendTo(line).on('click', async function(e) { - - }).text('add field'); + // show "add field" dialog + renderAddFieldPopup(recId, table); + }).text('+ add field'); } } - function renderNewRow(el, rec, table) { + function renderAddFieldPopup(recId, table) { + var popup = showPopupAlert('', 'Add field'); + var el = popup.container(); + el.addClass('option-menu'); + var html = `
+
+
Apply
assign value to all records`; + el.html(html); + + var name = el.findChild('.field-name'); + name.node().focus(); + var val = el.findChild('.field-value'); + var box = el.findChild('.all'); + var btn = el.findChild('.btn').on('click', function() { + var all = box.node().checked; + var nameStr = name.node().value.trim(); + if (!nameStr) return; + if (table.fieldExists(nameStr)) { + name.node().value = ''; + return; + } + var valStr = val.node().value.trim(); + var value = parseUnknownType(valStr); + // table.addField(nameStr, function(d) { + // // parse each time to avoid multiple references to objects + // return (all || d == rec) ? parseUnknownType(valStr) : null; + // }); + + var cmdStr = `-each "d['${nameStr}'] = `; + if (!all) { + cmdStr += `this.id != ${recId} ? null : `; + } + valStr = JSON.stringify(JSON.stringify(value)); + cmdStr = valStr.replace('"', cmdStr); + gui.console.runMapshaperCommands(cmdStr, function(err) { + if (!err) { + popup.close(); + } else { + console.error(err); + } + }); + }); } function renderRow(table, rec, key, type, editable) { @@ -163,12 +202,10 @@ export function Popup(gui, toNext, toPrev) { input.on('change', function(e) { var val2 = parser(input.value()), strval2 = formatInspectorValue(val2, type); - if (strval == strval2) { - // contents unchanged - } else if (val2 === null && type != 'object') { // allow null objects + if (val2 === null && type != 'object') { // allow null objects // invalid value; revert to previous value input.value(strval); - } else { + } else if (strval != strval2) { // field content has changed strval = strval2; gui.dispatchEvent('data_preupdate', {FID: currId}); // for undo/redo @@ -233,6 +270,14 @@ var inputParsers = { } }; +function parseUnknownType(str) { + var val = inputParsers.number(str); + if (val !== null) return val; + val = inputParsers.object(str); + if (val !== null) return val; + return str; +} + function getInputParser(type) { return inputParsers[type || 'multiple']; } diff --git a/www/index.html b/www/index.html index 3c1a668a..1afb3425 100644 --- a/www/index.html +++ b/www/index.html @@ -172,7 +172,7 @@

File format

Export
- choose directory + diff --git a/www/page.css b/www/page.css index 02534cd6..ea315600 100644 --- a/www/page.css +++ b/www/page.css @@ -256,7 +256,7 @@ body { /* --- Error message ---------- */ -.error-wrapper { +.alert-wrapper { z-index: 120; text-align: center; position: absolute; @@ -266,29 +266,33 @@ body { left: 0; } -.error-wrapper p.error-message { +.alert-wrapper p.error-message { margin: 1px 0 0 0; } -.error-wrapper .dialog-btn { - margin-top: 8px; -} - -.error-title { +.alert-title { line-height: 1.1; font-weight: bold; margin-bottom: 5px; } -div.error-box { +div.alert-title, div.error-message { + font-size: 16px; +} + +.alert-wrapper .alert-btn { + margin-top: 8px; +} + +div.alert-box { margin-top: 42px; /* 55px; */ overflow: auto; max-height: 70%; max-width: 400px; - font-size: 16px; } + /* --- Splash screen -------------- */ .splash-screen #splash-screen { @@ -513,7 +517,7 @@ body.dragover #import-options-drop-area .drop-area { text-align: left; margin-top: 12px; margin-right: 20px; - padding: 12px 16px 12px 18px; + padding: 11px 16px 12px 18px; vertical-align: top; display: inline-block; /* border: 1px solid #aaa; */ @@ -553,6 +557,7 @@ body.dragover #import-options-drop-area .drop-area { top: 1px; width: 12px; height: 12px; + cursor: pointer; } .info-box .tip-button { @@ -800,7 +805,7 @@ img.close-btn { float: right; height: 18px; width: 18px; - margin-top: 1px; + /* margin-top: 1px; */ margin-right: -3px; cursor: pointer; border-radius: 4px; @@ -1227,13 +1232,16 @@ div.basemap-style-btn.active img { top: 23px; } -#save-preference { - display: none; +.inline-checkbox { position: relative; top: 1px; left: 5px; } +#save-preference { + /* display: none; */ +} + .nav-sub-menu { position: absolute; z-index: -1; @@ -1316,25 +1324,32 @@ div.basemap-style-btn.active img { background: white; } -.save-menu-btn { +/*.save-menu-btn { display: inline-block; border-radius: 4px; border: 1px solid #aaa; font-size: 12px; margin-left: 2px; padding: 1px 2px 3px 2px; -} +}*/ -.save-menu-link, .save-menu-btn { - cursor: pointer; + display: inline-block; + font-size: 12px; + margin-top: 2px; } .save-menu-btn:hover { - background: #e6f7ff; color: black; } +.save-menu-link, +.save-menu-btn { + cursor: pointer; +} + + + .save-menu-link:hover { /* background: #e6f7ff; */ font-weight: bold;