Source: upgrade.js

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/** Upgrade functions to load presentations made with older versions of Sozi.
 *
 * @module
 */

import {Frame} from "./model/Presentation";

/** The Sozi namespace for custom XML elements.
 *
 * @readonly
 * @type {string} */
const SOZI_NS = "http://sozi.baierouge.fr";

/** Convert a string to a boolean.
 *
 * This function is used when upgrading from Sozi 13 where presentation data
 * is kept in custom XML elements and attributes inside the SVG document.
 *
 * @param {string} str - A string to parse.
 * @returns {boolean} - `true` if `str` is `"true"`.
 */
function parseBoolean(str) {
    return str === "true";
}

/** Get the new name for a Sozi 13 timing function.
 *
 * @param {string} str - The name of a timing function in Sozi 13.
 * @returns {string} - The name of the corresponding timing function in the current version of Sozi.
 */
function convertTimingFunction(str) {
    switch (str) {
        case "accelerate":
        case "strong-accelerate":
            return "easeIn";

        case "decelerate":
        case "strong-decelerate":
            return "easeOut";

        case "accelerate-decelerate":
        case "strong-accelerate-decelerate":
            return "easeInOut";

        case "immediate-beginning":
            return "stepStart";

        case "immediate-end":
            return "stepEnd";

        case "immediate-middle":
            return "stepMiddle";

        default:
            return "linear";
    }
}

/** Retrieve the value of an XML attribute.
 *
 * This function is used when upgrading from Sozi 13 where presentation data
 * is kept in custom XML elements and attributes inside the SVG document.
 *
 * @param {object} obj - The object where to copy the value of the attribute.
 * @param {string} propName - The name of the property to write in `obj`.
 * @param {Element[]} elts - An array of candidate XML elements where to look up.
 * @param {string} attrName - The name of the attribute to read.
 * @param {Function} [fn=x=>x] - A conversion function to apply to the attribute.
 */
function importAttribute(obj, propName, elts, attrName, fn=x => x) {
    for (let e of elts) {
        if (e && e.hasAttribute(attrName)) {
            obj[propName] = fn(e.getAttribute(attrName));
            break;
        }
    }
}

/** Retrieve the value of an XML attribute with namespace.
 *
 * This function is used when upgrading from Sozi 13 where presentation data
 * is kept in custom XML elements and attributes inside the SVG document.
 *
 * @param {object} obj - The object where to copy the value of the attribute.
 * @param {string} propName - The name of the property to write in `obj`.
 * @param {Element[]} elts - An array of candidate XML elements where to look up.
 * @param {string} nsUri - The XML namespace URI of the attribute.
 * @param {string} attrName - The name of the attribute to read.
 * @param {Function} [fn=x=>x] - A conversion function to apply to the attribute.
 */
function importAttributeNS(obj, propName, elts, nsUri, attrName, fn=x=>x) {
    for (let e of elts) {
        if (e && e.hasAttributeNS(nsUri, attrName)) {
            obj[propName] = fn(e.getAttributeNS(nsUri, attrName));
            break;
        }
    }
}

/** Read a Sozi 13 presentation.
 *
 * This function reads the custom XML elements and attributes from the SVG
 * document and populates the presentation data structure.
 *
 * @param {module:model/Presentation.Presentation} pres - The current presentation object.
 * @param {module:Controller.Controller} controller - The controller that manages the current editor.
 */
export function upgradeFromSVG(pres, controller) {
    // In the inlined SVG, DOM accessors fail to get elements with explicit XML namespaces.
    // getElementsByTagNameNS, getAttributeNS do not work for elements with the Sozi namespace.
    // We need to use an explicit namespace prefix ("ns:attr") and use method
    // getAttribute as if the prefix was part of the attribute name.
    // With SVG documents from Inkscape, custom namespaces have an automatically generated prefix
    // (ns1, ns2, ...). We first need to identify which one corresponds to the Sozi namespace.

    // Get the xmlns for the Sozi namespace
    const soziNsAttrs = Array.from(pres.document.root.attributes).filter(a => a.value === SOZI_NS);
    if (!soziNsAttrs.length) {
        return;
    }
    const soziPrefix = soziNsAttrs[0].name.replace(/^xmlns:/, "") + ":";

    // Get an ordered array of sozi:frame elements
    const frameElts = Array.from(pres.document.root.getElementsByTagNameNS(SOZI_NS, "frame"));
    frameElts.sort((a, b) => parseInt(a.getAttributeNS(SOZI_NS, "sequence")) - parseInt(b.getAttributeNS(SOZI_NS, "sequence")));

    // The "default" pool contains all layers that have no corresponding
    // <layer> element in any frame. The properties for these layers are
    // set in the <frame> elements. This array is updated as we process
    // the sequence of frames.
    const defaultLayers = pres.layers.slice();

    frameElts.forEach((frameElt, frameIndex) => {
        // Create a new frame with default camera states
        const frame = new Frame(pres);
        pres.frames.splice(frameIndex, 0, frame);

        // If this is not the first frame, the state is cloned from the previous frame.
        if (frameIndex) {
            frame.copy(pres.frames[frameIndex - 1]);
        }

        // Collect layer elements inside the current frame element
        const layerEltsByGroupId = {};
        for (let layerElt of frameElt.getElementsByTagNameNS(SOZI_NS, "layer")) {
            layerEltsByGroupId[layerElt.getAttributeNS(SOZI_NS, "group")] = layerElt;
        }

        pres.layers.forEach((layer, layerIndex) => {
            let layerElt = null;
            if (!layer.auto) {
                // If the current layer has a corresponding <layer> element, use it
                // and consider that the layer is no longer in the "default" pool.
                // Else, if the layer is in the "default" pool, then it is managed
                // by the <frame> element.
                // Other frames are cloned from the predecessors.
                const defaultLayerIndex = defaultLayers.indexOf(layer);
                const groupId = layer.svgNodes[0].getAttribute("id");
                if (groupId in layerEltsByGroupId) {
                    layerElt = layerEltsByGroupId[groupId];
                    if (defaultLayerIndex >= 0) {
                        defaultLayers.splice(defaultLayerIndex, 1);
                        controller.editableLayers.push(layer);
                    }
                }
            }

            const layerProperties = frame.layerProperties[layerIndex];
            const cameraState = frame.cameraStates[layerIndex];

            // It the current layer is managed by a <frame> or <layer> element,
            // update the camera state for this layer.
            let refElt;
            if (layerElt && layerElt.hasAttributeNS(SOZI_NS, "refid")) {
                refElt = pres.document.root.getElementById(layerElt.getAttributeNS(SOZI_NS, "refid"));
            }
            else if (defaultLayers.indexOf(layer) >= 0) {
                refElt = pres.document.root.getElementById(frameElt.getAttributeNS(SOZI_NS, "refid"));
            }
            if (refElt) {
                layerProperties.referenceElementId = layerProperties.outlineElementId = refElt.getAttribute("id");
                cameraState.setAtElement(refElt);
            }

            importAttributeNS(cameraState,     "clipped",                  [layerElt, frameElt], SOZI_NS, "clip",                    parseBoolean);
            importAttributeNS(layerProperties, "outlineElementHide",       [layerElt, frameElt], SOZI_NS, "hide",                    parseBoolean);
            importAttributeNS(layerProperties, "transitionTimingFunction", [layerElt, frameElt], SOZI_NS, "transition-profile",      convertTimingFunction);
            importAttributeNS(layerProperties, "transitionRelativeZoom",   [layerElt, frameElt], SOZI_NS, "transition-zoom-percent", z => parseFloat(z) / 100);
            importAttributeNS(layerProperties, "transitionPathId",         [layerElt, frameElt], SOZI_NS, "transition-path");
            importAttributeNS(layerProperties, "transitionPathHide",       [layerElt, frameElt], SOZI_NS, "transition-path-hide",    parseBoolean);
        });

        importAttribute(  frame, "frameId",              [frameElt],          "id");
        importAttributeNS(frame, "title",                [frameElt], SOZI_NS, "title");
        importAttributeNS(frame, "transitionDurationMs", [frameElt], SOZI_NS, "transition-duration-ms", parseFloat);
        importAttributeNS(frame, "timeoutMs",            [frameElt], SOZI_NS, "timeout-ms",             parseFloat);
        importAttributeNS(frame, "timeoutEnable",        [frameElt], SOZI_NS, "timeout-enable",         parseBoolean);
        importAttributeNS(frame, "showInFrameList",      [frameElt], SOZI_NS, "show-in-frame-list",     parseBoolean);
    });
}

/** Upgrade presentation data from an earlier version of Sozi.
 *
 * This function operates on a raw object loaded from a Sozi JSON file.
 *
 * @param {object} storable - The data loaded from a Sozi JSON file.
 */
export function upgradeFromStorable(storable) {
    // Sozi 17.02.05
    // Remove property referenceElementAuto
    // Replace referenceElementHide with outlineElementHide
    for (let frame of storable.frames) {
        for (let layerId in frame.layerProperties) {
            const layer = frame.layerProperties[layerId];
            if (layer.hasOwnProperty("referenceElementAuto")) {
                delete layer.referenceElementAuto;
            }
            if (layer.hasOwnProperty("referenceElementHide")) {
                layer.outlineElementHide = layer.referenceElementHide;
                delete layer.referenceElementHide;
            }
            if (layer.hasOwnProperty("referenceElementId") && !layer.hasOwnProperty("outlineElementId")) {
                layer.outlineElementId = layer.referenceElementId;
            }
        }
    }
}