Source: component/editor.js

/*! meta-admin/modules/editor */
/*jslint
    browser, long
*/
/*global
    Blob, confirm, L
*/
/**
 * editor component.
 *
 * @module meta-admin/modules/component/editor
 */
import _ from "underscore";
import {} from "leaflet";
import {} from "leaflet.markercluster";
import {saveAs} from "file-saver";
import dates from "util-web/modules/dates";
import ext from "util-web/modules/ext";
import i18next from "util-web/modules/i18next";
import keyval from "util-web/modules/keyval";
import ko from "knockout";
import Logger from "js-logger";
import logging from "meta-client/modules/logging";
import model from "meta-core/modules/model";
import nav from "util-web/modules/nav";
import objects from "meta-client/modules/objects";
import protocol from "meta-core/modules/protocol";
import template from "./editor.html";
import ui from "util-web/modules/ui";

const LOG = Logger.get("meta-admin/editor");
const EDITOR_OBJECT_QUERY_SELECTOR = "#editorObjectSearchQuery";
const LOG_SEARCH_QUERY_KEY = "admin-log-query";
const OBJECT_SEARCH_QUERY_KEY = "admin-obj-query";
const META_TYPES = Object.freeze([
    {
        id: model.META_TYPE_ACTION,
        name: i18next.pureComputedT("meta_type_" + model.META_TYPE_ACTION)
    },
    {
        id: model.META_TYPE_LOG,
        name: i18next.pureComputedT("meta_type_" + model.META_TYPE_LOG)
    },
    {
        id: model.META_TYPE_OBJECT,
        name: i18next.pureComputedT("meta_type_" + model.META_TYPE_OBJECT)
    },
    {
        id: model.META_TYPE_TYPE,
        name: i18next.pureComputedT("meta_type_" + model.META_TYPE_TYPE)
    }
]);

export default Object.freeze({
    template,
    viewModel: {
        createViewModel: function ({viewmodel}) {
            const getSelectedValue = () => ui.getSelectedText("#objectValue");
            const setSchemaRequired = function (schema) {
                if (_.isObject(schema)) {
                    const obj = {};
                    _.get(schema, model.TYPE_SCHEMA_REQUIRED, []).forEach(function (property) {
                        obj[property] = null; // TODO use schema.properties[property].type for initial value
                    });
                    return obj;
                }
            };
            const geoVisualizations = L.markerClusterGroup();
            const searchTimer = logging.newMetricTimer(viewmodel.client, "meta-admin.editor.search");
            const vm = {};

            vm.viewmodel = viewmodel;

            vm.model = model;
            vm.metaTypes = META_TYPES;

            vm.errorReply = ko.observable();
            vm.activeTab = ko.observable();

            vm.copyId = (meta) => navigator.clipboard.writeText(meta.id);

            vm.getSchemaForTypeId = function (typeId) {
                const predicate = {};
                predicate[model.META_ID] = typeId;
                return _.find(vm.viewmodel.types(), predicate)?.schema();
            };

            vm.newLog = () => vm.edit(model.metaLog({metaType: model.META_TYPE_OBJECT}));

            vm.newObject = function (typeId) {
                const schema = vm.getSchemaForTypeId(typeId);
                const value = setSchemaRequired(schema);
                vm.edit(model.metaObject({typeId, value}));
            };

            vm.viewmodel.metaCreateHook.subscribe(function (create) {
                if (_.isEmpty(create) === false) {
                    let tab;
                    return vm.viewmodel.navigateToEditor().then(function () {
                        if (model.META_TYPE_LOG === create) {
                            vm.newLog();
                            tab = vm.viewmodel.EDITOR_TABS.LOGS;
                        }
                        if (create.indexOf(model.META_TYPE_OBJECT) === 0) {
                            const typeId = create.split(";")[1];
                            vm.newObject(typeId);
                            tab = vm.viewmodel.EDITOR_TABS.OBJECTS;
                        }
                        vm.navigateToTab(tab);
                        vm.viewmodel.metaCreateHook(undefined);
                    });
                }
            });

            vm.logQuery = ko.observable();
            keyval.get(LOG_SEARCH_QUERY_KEY).then(function (query) {
                if (_.isEmpty(query) === false) {
                    vm.logQuery(query);
                }
            });
            vm.logFrom = ko.observable();
            vm.logTo = ko.observable();
            const logFromSub = dates.insertDateTimes({
                fromDateObservable: vm.logFrom,
                toDateObservable: vm.logTo
            });
            vm.logSearchLimit = ko.observable(10);
            vm.logSearchPurge = ko.observable();
            vm.metaLogs = objects({
                toVm: vm.viewmodel.metaToVm
            });

            vm.searchLogsInternal = function (limit, clearSearch) {
                const logQuery = vm.logQuery();
                const from = dates.parseDateTime(vm.logFrom());
                const to = dates.parseDateTime(vm.logTo());
                const queryParts = [model.lucene.META_LOG_QUERY];
                const purge = vm.logSearchPurge();
                LOG.debug(`searchLogsInternal, limit=${limit}, clearSearch=${clearSearch}`);
                if (clearSearch) {
                    vm.metaLogs.clear();
                }
                if (_.isEmpty(logQuery) === false) {
                    queryParts.push(logQuery);
                }
                if (from && to) {
                    queryParts.push(`${model.lucene.META_LOG_FIELD_TIMESTAMP}:${model.lucene.newDateTimeRangeQuery(from, to)}`);
                }
                let timerContext;
                return searchTimer.time().then(function (context) {
                    timerContext = context;
                    const searchRequest = protocol.searchRequest({
                        cursor: vm.metaLogs.searchCursor(),
                        limit,
                        purge: (
                            _.isEmpty(purge) === false
                            ? purge.split(",")
                            : undefined
                        ),
                        query: queryParts.join(" AND "),
                        sort: protocol.searchRequestSort({
                            field: model.lucene.LOGGING_FIELD_TIMESTAMP,
                            reverse: true
                        })
                    });
                    return vm.metaLogs.processSearch(
                        vm.viewmodel.client.search(searchRequest),
                        clearSearch
                    );
                }).then(function () {
                    vm.errorReply(undefined);
                    return keyval.set(LOG_SEARCH_QUERY_KEY, logQuery || "");
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    return timerContext.stop();
                });
            };

            vm.searchLogs = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                LOG.debug("searchLogs");
                return vm.searchLogsInternal(vm.logSearchLimit(), true).then(function () {
                    ui.resetForm(form);
                });
            };
            vm.loadMoreLogs = function () {
                LOG.debug("loadMoreLogs");
                return vm.searchLogsInternal(vm.logSearchLimit(), false);
            };
            vm.loadAllLogs = function () {
                LOG.debug("loadAllLogs");
                return vm.searchLogsInternal(0, false);
            };

            vm.searchTypeId = ko.observable();
            vm.objectQuery = ko.observable();
            keyval.get(OBJECT_SEARCH_QUERY_KEY).then(function (query) {
                if (_.isEmpty(query) === false) {
                    vm.objectQuery(query);
                }
            });
            vm.objectSearchLimit = ko.observable(100);
            vm.objectDistanceFrom = ko.observable();
            vm.objectDistanceMeters = ko.observable(500);
            vm.objectDistanceCircle = L.circle([0, 0], {
                color: "#800000",
                radius: vm.objectDistanceMeters()
            });
            const objectDistanceComputed = ko.computed(function () {
                const from = vm.objectDistanceFrom();
                const meters = vm.objectDistanceMeters();
                if (from) {
                    vm.objectDistanceCircle.setLatLng(from);
                    vm.objectDistanceCircle.setRadius(Number(meters));
                } else {
                    vm.objectDistanceCircle.setLatLng([0, 0]);
                }
            }).extend({rateLimit: 500});
            vm.sortableForType = ko.pureComputed(function () {
                const sortable = [model.NAME];
                const selectedType = vm.searchTypeId();
                if (selectedType) {
                    const schema = vm.getSchemaForTypeId(selectedType);
                    if (_.has(schema, model.TYPE_SCHEMA_SORTABLE)) {
                        return sortable.concat(schema[model.TYPE_SCHEMA_SORTABLE]);
                    }
                }
                return sortable;
            });
            vm.searchSort = ko.observable();
            vm.searchSortManual = ko.observable();
            vm.searchSortReverse = ko.observable(false);
            vm.objectSearchPurge = ko.observable();
            vm.metaObjects = objects({
                loadById: objects.loadByMetaId.bind(undefined, vm.viewmodel.client),
                onAdd: function (object) {
                    const mapApi = vm.viewmodel.mapApiProvider();
                    if (mapApi) {
                        mapApi.objectMarkers.push(object);
                    }
                },
                onRemove: function (object) {
                    const mapApi = vm.viewmodel.mapApiProvider();
                    if (mapApi) {
                        mapApi.objectMarkers.remove((meta) => meta.id === object.id);
                    }
                },
                toVm: vm.viewmodel.metaToVm,
                typeId: vm.searchTypeId
            });
            const eventHandle = vm.viewmodel.client.registerOnEvent(vm.metaObjects);
            vm.editedId = ko.pureComputed(function () {
                const edited = vm.viewmodel.edited();
                if (edited) {
                    return ko.unwrap(edited.id);
                }
            });

            vm.pickObjectDistanceFrom = function () {
                return vm.pickPositionInternal().then(function (position) {
                    vm.objectDistanceFrom(position);
                });
            };

            vm.clearObjectDistanceFrom = () => vm.objectDistanceFrom(undefined);

            vm.insertObjectSearchQueryText = function (valueProvider) {
                const selectedValue = ui.getSelectedText(EDITOR_OBJECT_QUERY_SELECTOR);
                ui.insertText(EDITOR_OBJECT_QUERY_SELECTOR, valueProvider(selectedValue));
                vm.objectQuery(vm.objectQuery());
            };

            vm.formatDateNumber = function () {
                vm.insertObjectSearchQueryText(function (value) {
                    const dateTime = dates.parseDateTime(value);
                    return model.lucene.toDateNumber(dateTime);
                });
            };

            vm.formatDateTimeNumber = function () {
                vm.insertObjectSearchQueryText(function (value) {
                    const dateTime = dates.parseDateTime(value);
                    return model.lucene.toDateTimeNumber(dateTime);
                });
            };

            vm.insertRangeQuery = function () {
                vm.insertObjectSearchQueryText(function (value) {
                    const index = value.indexOf(":") + 1;
                    const dateRange = dates.getDateRange(value.substring(index));
                    return value.substring(0, index) + model.lucene.newDateTimeRangeQuery(dateRange.from, dateRange.to);
                });
            };

            vm.searchObjectsInternal = function (limit, clearSearch) {
                const typeId = vm.searchTypeId();
                const query = vm.objectQuery();
                const forgedQuery = objects.newSearchQuery(typeId, [query]);
                const sortField = (
                    vm.searchTypeId()
                    ? vm.searchSort()
                    : vm.searchSortManual()
                );
                const purge = vm.objectSearchPurge();
                let spatial;
                LOG.debug(`searchObjectsInternal, limit=${limit}, clearSearch=${clearSearch}, query=${query}`);
                if (clearSearch) {
                    vm.metaObjects.clear();
                }
                if (vm.objectDistanceFrom()) {
                    spatial = [protocol.searchRequestSpatial({
                        distanceFrom: vm.objectDistanceFrom(),
                        distanceMeters: Number(vm.objectDistanceMeters()),
                        field: model.OBJECT_POSITION
                    })];
                }
                let timerContext;
                return searchTimer.time().then(function (context) {
                    timerContext = context;
                    const searchRequest = protocol.searchRequest({
                        cursor: vm.metaObjects.searchCursor(),
                        limit,
                        purge: (
                            _.isEmpty(purge) === false
                            ? purge.split(",")
                            : undefined
                        ),
                        query: forgedQuery,
                        sort: (
                            _.isEmpty(sortField)
                            ? undefined
                            : protocol.searchRequestSort({
                                field: sortField,
                                reverse: vm.searchSortReverse()
                            })
                        ),
                        spatial
                    });
                    return vm.metaObjects.processSearch(
                        vm.viewmodel.client.search(searchRequest)
                    );
                }).then(function () {
                    vm.errorReply(undefined);
                    return keyval.set(OBJECT_SEARCH_QUERY_KEY, query || "");
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    const mapApi = vm.viewmodel.mapApiProvider();
                    if (mapApi) {
                        mapApi.objectMarkers(vm.metaObjects.objects());
                    }
                    return timerContext.stop();
                });
            };

            vm.searchObjects = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                LOG.debug("searchObjects");
                return vm.searchObjectsInternal(Number(vm.objectSearchLimit()), true).then(function () {
                    ui.resetForm(form);
                });
            };

            vm.loadMoreObjects = function () {
                LOG.debug("loadMoreObjects");
                return vm.searchObjectsInternal(Number(vm.objectSearchLimit()), false);
            };

            vm.loadAllObjects = function () {
                LOG.debug("loadAllObjects");
                return vm.searchObjectsInternal(0, false);
            };

            vm.edit = (meta) => vm.editInternal(meta, false);

            vm.editInternal = function (meta, forceLoad = false) {
                let edited = meta;
                if (Boolean(edited) === false) {
                    geoVisualizations.clearLayers();
                    vm.viewmodel.edited(undefined);
                    return;
                }
                const metaId = _.get(edited, model.META_ID);
                LOG.debug(`edit, metaId=${metaId}, forceLoad=${forceLoad}`);
                if (forceLoad) {
                    return vm.viewmodel.client.search(protocol.searchRequest({
                        limit: 1,
                        query: metaId
                    })).then(function (searchReply) {
                        if (_.isEmpty(searchReply.metas)) {
                            LOG.warn("forceLoad failed, metaId=" + metaId);
                            return;
                        }
                        vm.edit(searchReply.metas[0]);
                    });
                }
                if (Boolean(ko.mapping.isMapped(edited)) === false) {
                    edited = vm.viewmodel.metaToVm(edited);
                }
                vm.viewmodel.edited(edited);
            };

            vm.loadLog = function () {
                vm.metaLogs.clear();
                vm.navigateToTab(vm.viewmodel.EDITOR_TABS.LOGS);
                vm.logQuery(`${model.lucene.META_LOG_FIELD_META_ID}:"${vm.viewmodel.edited().id}"`);
                return vm.searchLogs();
            };

            vm.writeChannel = ko.observable();
            vm.isSaving = ko.observable();
            vm.isSaveSuccess = ko.observable();

            vm.save = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                vm.isSaving(true);
                const edited = vm.viewmodel.edited();
                return vm.viewmodel.client.write(protocol.writeRequest({
                    action: (
                        edited.id
                        ? protocol.WRITE_ACTION.UPDATE
                        : protocol.WRITE_ACTION.CREATE
                    ),
                    channel: vm.writeChannel(),
                    meta: vm.viewmodel.vmToMeta(edited)
                })).then(function (writeReply) {
                    vm.errorReply(undefined);
                    vm.edit(writeReply.meta);
                    vm.isSaveSuccess(true);
                    ui.resetForm(form);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    vm.isSaving(false);
                });
            };

            vm.doDelete = function () {
                if (confirm(i18next.t("delete_meta_confirmation")) === false) {
                    return;
                }
                const edited = vm.viewmodel.edited();
                return vm.deleteMeta(edited).then(function () {
                    vm.cancel();
                });
            };

            vm.deleteMeta = function (edited) {
                if (Boolean(edited) === false) {
                    return Promise.resolve(undefined);
                }
                const meta = {};
                meta[model.META_TYPE_PROPERTY] = edited[model.META_TYPE_PROPERTY];
                meta.id = edited.id;
                if (model.META_TYPE_OBJECT === meta[model.META_TYPE_PROPERTY]) {
                    meta.typeId = edited.typeId();
                }
                if (model.META_TYPE_TYPE === meta[model.META_TYPE_PROPERTY]) {
                    meta.schema = {};
                }
                return vm.viewmodel.client.write(protocol.writeRequest({
                    action: protocol.WRITE_ACTION.DELETE,
                    channel: vm.writeChannel(),
                    meta
                })).then(function () {
                    vm.errorReply(undefined);
                    edited.deleted(true);
                }, (exc) => vm.errorReply(JSON.stringify(exc, undefined, 4)));
            };

            vm.deleteAll = function () {
                if (confirm(i18next.t("delete_all_confirmation")) === false) {
                    return;
                }
                return Promise.all(vm.metaObjects.objects().map(vm.deleteMeta)).then(function () {
                    vm.errorReply(undefined);
                    vm.cancel();
                }, (exc) => vm.errorReply(JSON.stringify(exc, undefined, 4)));
            };

            vm.isUpdateActive = ko.observable(false);
            vm.updateName = ko.observable();
            vm.updateValue = ko.observable();
            vm.isUpdateValueValid = ko.observable(true);
            vm.updateValueString = vm.viewmodel.newComputedJsonField(vm.updateValue, undefined, vm.isUpdateValueValid);
            vm.updatePosition = ko.observable();
            vm.clearUpdatePosition = ko.observable(false);

            vm.pickUpdatePosition = function () {
                vm.isUpdateActive(false);
                return vm.pickPositionInternal().then(function (position) {
                    vm.isUpdateActive(true);
                    vm.updatePosition(position);
                });
            };

            vm.toggleClearUpdatePosition = () => vm.clearUpdatePosition(!vm.clearUpdatePosition());

            vm.activateUpdate = function () {
                vm.updateName(undefined);
                vm.updateValue(undefined);
                vm.updateValueString.format();
                vm.updatePosition(undefined);
                vm.clearUpdatePosition(false);
                vm.isUpdateActive(true);
            };

            vm.update = function (form) {
                const edited = vm.viewmodel.edited();
                LOG.debug("update, metaId=" + edited.id);
                const updateName = vm.updateName();
                const updateValue = vm.updateValue();
                const updatePosition = vm.updatePosition();
                const updateMeta = Object.assign({}, model.metaObject({
                    id: edited.id,
                    typeId: edited.typeId()
                }));
                if (ui.validateForm(form) === false) {
                    return;
                }
                if (_.isEmpty(updateName) === false) {
                    updateMeta.name = updateName;
                } else {
                    delete updateMeta[model.META_NAME];
                }
                if (_.isObject(updateValue)) {
                    updateMeta.value = updateValue;
                } else {
                    delete updateMeta[model.OBJECT_VALUE];
                }
                if (model.isValidPosition(updatePosition)) {
                    updateMeta.position = updatePosition;
                }
                if (vm.clearUpdatePosition()) {
                    updateMeta.position = model.NO_POSITION;
                }
                return vm.viewmodel.client.writeObjectUpdate(protocol.writeObjectUpdateRequest({
                    channel: vm.writeChannel(),
                    meta: updateMeta
                })).then(function () {
                    vm.errorReply(undefined);
                    vm.isUpdateActive(false);
                    vm.isSaveSuccess(true);
                    ui.resetForm(form);
                }).catch((exc) => vm.errorReply(JSON.stringify(exc, undefined, 4)));
            };

            vm.isExecuteActive = ko.observable(false);
            vm.executeParam = ko.observable();
            vm.isExecuteParamValid = ko.observable(true);
            vm.executeParamString = vm.viewmodel.newComputedJsonField(vm.executeParam, undefined, vm.isExecuteParamValid);
            vm.isExecuting = ko.observable(false);
            vm.executeResult = ko.observable();

            vm.activateExecute = function () {
                const edited = vm.viewmodel.edited();
                const param = setSchemaRequired(edited.paramSchema());
                vm.executeParam(param);
                vm.executeParamString.format();
                vm.executeResult(undefined);
                vm.isExecuteActive(true);
            };

            vm.execute = function (form) {
                if (ui.validateForm(form) === false) {
                    return;
                }
                vm.isExecuting(true);
                const edited = vm.viewmodel.edited();
                LOG.debug(`execute, metaId=${edited.id}`);
                return vm.viewmodel.client.execute(protocol.executeRequest({
                    actionId: edited.id,
                    param: vm.executeParam()
                })).then(function (executeReply) {
                    vm.errorReply(undefined);
                    vm.executeResult(JSON.stringify(executeReply.result, undefined, 4));
                    ui.resetForm(form);
                }).catch((exc) => vm.errorReply(JSON.stringify(exc, undefined, 4))).then(
                    () => vm.isExecuting(false)
                );
            };

            vm.cancel = () => vm.edit(undefined);

            vm.pickPosition = function () {
                return vm.pickPositionInternal().then(function (position) {
                    vm.viewmodel.edited().position(position);
                });
            };

            vm.clearPosition = () => vm.viewmodel.edited().position(undefined);

            vm.insertDate = () => ui.insertText("#objectValue", dates.nowDateString());

            vm.insertDateTime = () => ui.insertText("#objectValue", dates.nowDateTimeString());

            vm.insertPosition = function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                if (Boolean(mapApi) === false) {
                    return Promise.resolve(undefined);
                }
                return mapApi.pickPosition().then(function (position) {
                    ui.insertText("#objectValue", JSON.stringify(position));
                });
            };

            vm.visualizePoint = function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                const selected = getSelectedValue();
                const position = JSON.parse(selected);
                if (mapApi) {
                    mapApi.createMarker("visualizePoint", i18next.t("visualized_point"), position, undefined, mapApi.ICONS.PINK).addTo(geoVisualizations);
                    mapApi.panTo(position);
                }
            };

            vm.visualizeLine = function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                const selected = getSelectedValue();
                const positions = JSON.parse(selected);
                if (mapApi) {
                    mapApi.createPolyline("visualizeLine", i18next.t("visualized_line"), positions).addTo(geoVisualizations);
                    mapApi.fitBounds(positions);
                }
            };

            vm.visualizeBox = function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                const selected = getSelectedValue();
                const positions = JSON.parse(selected);
                if (mapApi) {
                    mapApi.createRectangle("visualizeBox", i18next.t("visualized_box"), positions).addTo(geoVisualizations);
                    mapApi.fitBounds(positions);
                }
            };

            vm.visualizePolygon = function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                const selected = getSelectedValue();
                const positions = JSON.parse(selected);
                if (mapApi) {
                    mapApi.createPolygon("visualizePolygon", i18next.t("visualized_polygon"), positions).addTo(geoVisualizations);
                    mapApi.fitBounds(positions);
                }
            };

            vm.visualizedImage = ko.observable();

            vm.visualizeImage = function () {
                const dataUrl = getSelectedValue();
                vm.visualizedImage(dataUrl);
            };

            vm.clearVisualizedImage = () => vm.visualizedImage(undefined);

            vm.isImportActive = ko.observable(false);
            vm.hasImportFileLoaded = ko.observable(false);
            vm.isImportProcessing = ko.observable(false);
            vm.importTypes = ko.observableArray();
            vm.importObjects = ko.observableArray();
            vm.importActions = ko.observableArray();
            vm.activateImport = function () {
                vm.hasImportFileLoaded(false);
                vm.isImportActive(true);
            };

            vm.loadImportFile = function (ignore, event) {
                return ext.loadFileContent({
                    event,
                    fileEndings: [".json"],
                    type: ext.FILE_CONTENT_TYPES.TEXT
                }).then(function (file) {
                    const metaImport = JSON.parse(file.content);
                    if (_.isEmpty(metaImport.types) === false) {
                        vm.importTypes(metaImport.types);
                    }
                    if (_.isEmpty(metaImport.objects) === false) {
                        vm.importObjects(metaImport.objects);
                    }
                    if (_.isEmpty(metaImport.actions) === false) {
                        vm.importActions(metaImport.actions);
                    }
                    vm.hasImportFileLoaded(true);
                });
            };

            vm.doImport = function () {
                const writes = [];
                const pushWrite = function (meta) {
                    writes.push(protocol.writeRequest({
                        action: protocol.WRITE_ACTION.CREATE,
                        meta
                    }));
                };
                vm.isImportProcessing(true);
                vm.importTypes().forEach(pushWrite);
                vm.importObjects().forEach(pushWrite);
                vm.importActions().forEach(pushWrite);
                return Promise.all(writes.map(function (write) {
                    return vm.viewmodel.client.write(write);
                })).then(function () {
                    vm.errorReply(undefined);
                }, function (exc) {
                    vm.errorReply(JSON.stringify(exc, undefined, 4));
                }).then(function () {
                    vm.isImportProcessing(false);
                    vm.isImportActive(false);
                    vm.importTypes.removeAll();
                    vm.importObjects.removeAll();
                    vm.importActions.removeAll();
                });
            };

            vm.isExportActive = ko.observable(false);
            vm.isExporting = ko.observable(false);
            vm.exportTypes = ko.observable();
            vm.exportObjects = ko.observable();
            vm.exportActions = ko.observable();
            vm.activateExport = function () {
                vm.isExportActive(true);
            };

            vm.doExport = function () {
                const metaExport = {};
                LOG.debug("doExport");
                vm.isExporting(true);
                try {
                    if (vm.exportTypes()) {
                        metaExport.types = vm.viewmodel.types().map(vm.viewmodel.vmToMeta);
                    }
                    if (vm.exportObjects()) {
                        metaExport.objects = vm.metaObjects.objects().map(vm.viewmodel.vmToMeta);
                    }
                    if (vm.exportActions()) {
                        metaExport.actions = vm.viewmodel.actions().map(vm.viewmodel.vmToMeta);
                    }
                    saveAs(
                        new Blob([JSON.stringify(metaExport, undefined, 4)], {type: "application/json;charset=utf-8"}),
                        `meta_export-${dates.now().toMillis()}.json`
                    );
                } catch (exc) {
                    LOG.warn("doExport failed", exc);
                } finally {
                    vm.isExporting(false);
                }
                vm.isExportActive(false);
                vm.exportTypes(false);
                vm.exportObjects(false);
                vm.exportActions(false);
            };

            vm.pickPositionInternal = function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                LOG.debug("pickPositionInternal");
                if (Boolean(mapApi) === false) {
                    return Promise.resolve(undefined);
                }
                return mapApi.pickPosition().then(function (position) {
                    return position;
                });
            };

            vm.navigateToTab = vm.viewmodel.navigateToTab.bind(undefined, vm.viewmodel.COMPONENTS.EDITOR);

            vm.onNavUpdate = function (tab) {
                const activeTab = (
                    _.isEmpty(tab)
                    ? vm.viewmodel.EDITOR_TABS.OBJECTS
                    : tab
                );
                LOG.debug(`onNavUpdate, tab=${tab}, activeTab=${activeTab}`);
                vm.activeTab(activeTab);
            };

            nav.register({
                id: vm.viewmodel.COMPONENTS.EDITOR,
                onUpdate: vm.onNavUpdate
            });

            vm.dispose = function () {
                LOG.debug("dispose");
                vm.viewmodel.client.unregisterOnEvent(eventHandle);
                logFromSub.dispose();
                objectDistanceComputed.dispose();
                vm.executeParamString.dispose();
            };

            ko.when(function () {
                return _.isObject(vm.viewmodel.mapApiProvider());
            }).then(function () {
                const mapApi = vm.viewmodel.mapApiProvider();
                mapApi.addOverlay(vm.objectDistanceCircle, i18next.t("search_distance"));
                mapApi.addOverlay(geoVisualizations, i18next.t("geo_visualizations"));
                mapApi.editedObjectMarker.subscribe(function (metaId) {
                    LOG.debug(`editedObjectMarker=${metaId}`);
                    if (metaId) {
                        vm.editInternal(model.metaObject({id: metaId}), true);
                        mapApi.editedObjectMarker(undefined);
                    }
                });
            });

            return Object.freeze(vm);
        }
    }
});