Reference Source

src/parsers/parseCityJSONIntoXKTModel.js

import {earcut} from './../lib/earcut';
import {math} from "./../lib/math.js";

const tempVec2a = math.vec2();
const tempVec3a = math.vec3();
const tempVec3b = math.vec3();
const tempVec3c = math.vec3();

/**
 * @desc Parses a CityJSON model into an {@link XKTModel}.
 *
 * [CityJSON](https://www.cityjson.org) is a JSON-based encoding for a subset of the CityGML data model (version 2.0.0),
 * which is an open standardised data model and exchange format to store digital 3D models of cities and
 * landscapes. CityGML is an official standard of the [Open Geospatial Consortium](https://www.ogc.org/).
 *
 * This converter function supports most of the [CityJSON 1.0.2 Specification](https://www.cityjson.org/specs/1.0.2),
 * with the following limitations:
 *
 * * Does not (yet) support CityJSON semantics for geometry primitives.
 * * Does not (yet) support textured geometries.
 * * Does not (yet) support geometry templates.
 * * When the CityJSON file provides multiple *themes* for a geometry, then we parse only the first of the provided themes for that geometry.
 *
 * ## Usage
 *
 * In the example below we'll create an {@link XKTModel}, then load a CityJSON model into it.
 *
 * ````javascript
 * utils.loadJSON("./models/cityjson/DenHaag.json", async (data) => {
 *
 *     const xktModel = new XKTModel();
 *
 *     parseCityJSONIntoXKTModel({
 *          data,
 *          xktModel,
 *          log: (msg) => { console.log(msg); }
 *     }).then(()=>{
 *        xktModel.finalize();
 *     },
 *     (msg) => {
 *         console.error(msg);
 *     });
 * });
 * ````
 *
 * @param {Object} params Parsing params.
 * @param {Object} params.data CityJSON data.
 * @param {XKTModel} params.xktModel XKTModel to parse into.
 * @param {boolean} [params.center=false] Set true to center the CityJSON vertex positions to [0,0,0]. This is applied before the transformation matrix, if specified.
 * @param {Boolean} [params.transform] 4x4 transformation matrix to transform CityJSON vertex positions. Use this to rotate, translate and scale them if neccessary.
 * @param {Object} [params.stats] Collects statistics.
 * @param {function} [params.log] Logging callback.
 @returns {Promise} Resolves when CityJSON has been parsed.
 */
function parseCityJSONIntoXKTModel({
                                       data,
                                       xktModel,
                                       center = false,
                                       transform = null,
                                       stats = {}, log
                                   }) {

    return new Promise(function (resolve, reject) {

        if (!data) {
            reject("Argument expected: data");
            return;
        }

        if (data.type !== "CityJSON") {
            reject("Invalid argument: data is not a CityJSON file");
            return;
        }

        if (!xktModel) {
            reject("Argument expected: xktModel");
            return;
        }

        let vertices;

        log("Using parser: parseCityJSONIntoXKTModel");

        log(`center: ${center}`);
        if (transform) {
            log(`transform: [${transform}]`);
        }

        if (data.transform || center || transform) {
            vertices = copyVertices(data.vertices);
            if (data.transform) {
                transformVertices(vertices, data.transform)
            }
            if (center) {
                centerVertices(vertices);
            }
            if (transform) {
                customTransformVertices(vertices, transform);
            }
        } else {
            vertices = data.vertices;
        }

        stats.sourceFormat = data.type || "";
        stats.schemaVersion = data.version || "";
        stats.title = "";
        stats.author = "";
        stats.created = "";
        stats.numMetaObjects = 0;
        stats.numPropertySets = 0;
        stats.numTriangles = 0;
        stats.numVertices = 0;
        stats.numObjects = 0;
        stats.numGeometries = 0;

        const rootMetaObjectId = math.createUUID();

        xktModel.createMetaObject({
            metaObjectId: rootMetaObjectId,
            metaObjectType: "Model",
            metaObjectName: "Model"
        });

        stats.numMetaObjects++;

        const modelMetaObjectId = math.createUUID();

        xktModel.createMetaObject({
            metaObjectId: modelMetaObjectId,
            metaObjectType: "CityJSON",
            metaObjectName: "CityJSON",
            parentMetaObjectId: rootMetaObjectId
        });

        stats.numMetaObjects++;

        const ctx = {
            data,
            vertices,
            xktModel,
            rootMetaObjectId: modelMetaObjectId,
            log: (log || function (msg) {
            }),
            nextId: 0,
            stats
        };

        ctx.xktModel.schema = data.type + " " + data.version;

        ctx.log("Converting " + ctx.xktModel.schema);

        parseCityJSON(ctx);

        resolve();
    });
}

function copyVertices(vertices) {
    const vertices2 = [];
    for (let i = 0, j = 0; i < vertices.length; i++, j += 3) {
        const x = vertices[i][0];
        const y = vertices[i][1];
        const z = vertices[i][2];
        vertices2.push([x, y, z]);
    }
    return vertices2;
}

function transformVertices(vertices, cityJSONTransform) {
    const scale = cityJSONTransform.scale || math.vec3([1, 1, 1]);
    const translate = cityJSONTransform.translate || math.vec3([0, 0, 0]);
    for (let i = 0; i < vertices.length; i++) {
        const vertex = vertices[i];
        vertex[0] = (vertex[0] * scale[0]) + translate[0];
        vertex[1] = (vertex[1] * scale[1]) + translate[1];
        vertex[2] = (vertex[2] * scale[2]) + translate[2];
    }
}

function centerVertices(vertices) {
    if (center) {
        const centerPos = math.vec3();
        const numPoints = vertices.length;
        for (let i = 0, len = vertices.length; i < len; i++) {
            const vertex = vertices[i];
            centerPos[0] += vertex[0];
            centerPos[1] += vertex[1];
            centerPos[2] += vertex[2];
        }
        centerPos[0] /= numPoints;
        centerPos[1] /= numPoints;
        centerPos[2] /= numPoints;
        for (let i = 0, len = vertices.length; i < len; i++) {
            const vertex = vertices[i];
            vertex[0] -= centerPos[0];
            vertex[1] -= centerPos[1];
            vertex[2] -= centerPos[2];
        }
    }
}

function customTransformVertices(vertices, transform) {
    if (transform) {
        const mat = math.mat4(transform);
        for (let i = 0, len = vertices.length; i < len; i++) {
            const vertex = vertices[i];
            math.transformPoint3(mat, vertex, vertex);
        }
    }
}

function parseCityJSON(ctx) {

    const data = ctx.data;
    const cityObjects = data.CityObjects;

    for (const objectId in cityObjects) {
        if (cityObjects.hasOwnProperty(objectId)) {
            const cityObject = cityObjects[objectId];
            parseCityObject(ctx, cityObject, objectId);
        }
    }
}

function parseCityObject(ctx, cityObject, objectId) {

    const xktModel = ctx.xktModel;
    const data = ctx.data;
    const metaObjectId = objectId;
    const metaObjectType = cityObject.type;
    const metaObjectName = metaObjectType + " : " + objectId;

    const parentMetaObjectId = cityObject.parents ? cityObject.parents[0] : ctx.rootMetaObjectId;

    xktModel.createMetaObject({
        metaObjectId,
        metaObjectName,
        metaObjectType,
        parentMetaObjectId
    });

    ctx.stats.numMetaObjects++;

    if (!(cityObject.geometry && cityObject.geometry.length > 0)) {
        return;
    }

    const meshIds = [];

    for (let i = 0, len = cityObject.geometry.length; i < len; i++) {

        const geometry = cityObject.geometry[i];

        let objectMaterial;
        let surfaceMaterials;

        const appearance = data.appearance;
        if (appearance) {
            const materials = appearance.materials;
            if (materials) {
                const geometryMaterial = geometry.material;
                if (geometryMaterial) {
                    const themeIds = Object.keys(geometryMaterial);
                    if (themeIds.length > 0) {
                        const themeId = themeIds[0];
                        const theme = geometryMaterial[themeId];
                        if (theme.value !== undefined) {
                            objectMaterial = materials[theme.value];
                        } else {
                            const values = theme.values;
                            if (values) {
                                surfaceMaterials = [];
                                for (let j = 0, lenj = values.length; j < lenj; j++) {
                                    const value = values[i];
                                    const surfaceMaterial = materials[value];
                                    surfaceMaterials.push(surfaceMaterial);
                                }
                            }
                        }
                    }
                }
            }
        }

        if (surfaceMaterials) {
            parseGeometrySurfacesWithOwnMaterials(ctx, geometry, surfaceMaterials, meshIds);

        } else {
            parseGeometrySurfacesWithSharedMaterial(ctx, geometry, objectMaterial, meshIds);
        }
    }

    if (meshIds.length > 0) {
        xktModel.createEntity({
            entityId: objectId,
            meshIds: meshIds
        });

        ctx.stats.numObjects++;
    }
}

function parseGeometrySurfacesWithOwnMaterials(ctx, geometry, surfaceMaterials, meshIds) {

    const geomType = geometry.type;

    switch (geomType) {

        case "MultiPoint":
            break;

        case "MultiLineString":
            break;

        case "MultiSurface":

        case "CompositeSurface":
            const surfaces = geometry.boundaries;
            parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds);
            break;

        case "Solid":
            const shells = geometry.boundaries;
            for (let j = 0; j < shells.length; j++) {
                const surfaces = shells[j];
                parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds);
            }
            break;

        case "MultiSolid":

        case "CompositeSolid":
            const solids = geometry.boundaries;
            for (let j = 0; j < solids.length; j++) {
                for (let k = 0; k < solids[j].length; k++) {
                    const surfaces = solids[j][k];
                    parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds);
                }
            }
            break;

        case "GeometryInstance":
            break;
    }
}

function parseSurfacesWithOwnMaterials(ctx, surfaceMaterials, surfaces, meshIds) {

    const vertices = ctx.vertices;
    const xktModel = ctx.xktModel;

    for (let i = 0; i < surfaces.length; i++) {

        const surface = surfaces[i];
        const surfaceMaterial = surfaceMaterials[i] || {diffuseColor: [0.8, 0.8, 0.8], transparency: 1.0};

        const face = [];
        const holes = [];

        const sharedIndices = [];

        const geometryCfg = {
            positions: [],
            indices: []
        };

        for (let j = 0; j < surface.length; j++) {

            if (face.length > 0) {
                holes.push(face.length);
            }

            const newFace = extractLocalIndices(ctx, surface[j], sharedIndices, geometryCfg);

            face.push(...newFace);
        }

        if (face.length === 3) { // Triangle

            geometryCfg.indices.push(face[0]);
            geometryCfg.indices.push(face[1]);
            geometryCfg.indices.push(face[2]);

        } else if (face.length > 3) { // Polygon

            // Prepare to triangulate

            const pList = [];

            for (let k = 0; k < face.length; k++) {
                pList.push({
                    x: vertices[sharedIndices[face[k]]][0],
                    y: vertices[sharedIndices[face[k]]][1],
                    z: vertices[sharedIndices[face[k]]][2]
                });
            }

            const normal = getNormalOfPositions(pList, math.vec3());

            // Convert to 2D

            let pv = [];

            for (let k = 0; k < pList.length; k++) {

                to2D(pList[k], normal, tempVec2a);

                pv.unshift(tempVec2a[0]);
                pv.unshift(tempVec2a[1]);
            }

            // Triangulate

            const tr = earcut(pv, holes, 2);

            // Create triangles

            for (let k = 0; k < tr.length; k += 3) {
                geometryCfg.indices.unshift(face[tr[k]]);
                geometryCfg.indices.unshift(face[tr[k + 1]]);
                geometryCfg.indices.unshift(face[tr[k + 2]]);
            }
        }

        const geometryId = "" + ctx.nextId++;
        const meshId = "" + ctx.nextId++;

        xktModel.createGeometry({
            geometryId: geometryId,
            primitiveType: "triangles",
            positions: geometryCfg.positions,
            indices: geometryCfg.indices
        });

        xktModel.createMesh({
            meshId: meshId,
            geometryId: geometryId,
            color: (surfaceMaterial && surfaceMaterial.diffuseColor) ? surfaceMaterial.diffuseColor : [0.8, 0.8, 0.8],
            opacity: 1.0
            //opacity: (surfaceMaterial && surfaceMaterial.transparency !== undefined) ? (1.0 - surfaceMaterial.transparency) : 1.0
        });

        meshIds.push(meshId);

        ctx.stats.numGeometries++;
        ctx.stats.numVertices += geometryCfg.positions.length / 3;
        ctx.stats.numTriangles += geometryCfg.indices.length / 3;
    }
}

function parseGeometrySurfacesWithSharedMaterial(ctx, geometry, objectMaterial, meshIds) {

    const xktModel = ctx.xktModel;
    const sharedIndices = [];
    const geometryCfg = {
        positions: [],
        indices: []
    };

    const geomType = geometry.type;

    switch (geomType) {
        case "MultiPoint":
            break;

        case "MultiLineString":
            break;

        case "MultiSurface":
        case "CompositeSurface":
            const surfaces = geometry.boundaries;
            parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, geometryCfg);
            break;

        case "Solid":
            const shells = geometry.boundaries;
            for (let j = 0; j < shells.length; j++) {
                const surfaces = shells[j];
                parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, geometryCfg);
            }
            break;

        case "MultiSolid":
        case "CompositeSolid":
            const solids = geometry.boundaries;
            for (let j = 0; j < solids.length; j++) {
                for (let k = 0; k < solids[j].length; k++) {
                    const surfaces = solids[j][k];
                    parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, geometryCfg);
                }
            }
            break;

        case "GeometryInstance":
            break;
    }

    const geometryId = "" + ctx.nextId++;
    const meshId = "" + ctx.nextId++;

    xktModel.createGeometry({
        geometryId: geometryId,
        primitiveType: "triangles",
        positions: geometryCfg.positions,
        indices: geometryCfg.indices
    });

    xktModel.createMesh({
        meshId: meshId,
        geometryId: geometryId,
        color: (objectMaterial && objectMaterial.diffuseColor) ? objectMaterial.diffuseColor : [0.8, 0.8, 0.8],
        opacity: 1.0
        //opacity: (objectMaterial && objectMaterial.transparency !== undefined) ? (1.0 - objectMaterial.transparency) : 1.0
    });

    meshIds.push(meshId);

    ctx.stats.numGeometries++;
    ctx.stats.numVertices += geometryCfg.positions.length / 3;
    ctx.stats.numTriangles += geometryCfg.indices.length / 3;
}

function parseSurfacesWithSharedMaterial(ctx, surfaces, sharedIndices, primitiveCfg) {

    const vertices = ctx.vertices;

    for (let i = 0; i < surfaces.length; i++) {

        let boundary = [];
        let holes = [];

        for (let j = 0; j < surfaces[i].length; j++) {
            if (boundary.length > 0) {
                holes.push(boundary.length);
            }
            const newBoundary = extractLocalIndices(ctx, surfaces[i][j], sharedIndices, primitiveCfg);
            boundary.push(...newBoundary);
        }

        if (boundary.length === 3) { // Triangle

            primitiveCfg.indices.push(boundary[0]);
            primitiveCfg.indices.push(boundary[1]);
            primitiveCfg.indices.push(boundary[2]);

        } else if (boundary.length > 3) { // Polygon

            let pList = [];

            for (let k = 0; k < boundary.length; k++) {
                pList.push({
                    x: vertices[sharedIndices[boundary[k]]][0],
                    y: vertices[sharedIndices[boundary[k]]][1],
                    z: vertices[sharedIndices[boundary[k]]][2]
                });
            }

            const normal = getNormalOfPositions(pList, math.vec3());
            let pv = [];

            for (let k = 0; k < pList.length; k++) {
                to2D(pList[k], normal, tempVec2a);
                pv.unshift(tempVec2a[0]);
                pv.unshift(tempVec2a[1]);
            }

            const tr = earcut(pv, holes, 2);

            for (let k = 0; k < tr.length; k += 3) {
                primitiveCfg.indices.unshift(boundary[tr[k]]);
                primitiveCfg.indices.unshift(boundary[tr[k + 1]]);
                primitiveCfg.indices.unshift(boundary[tr[k + 2]]);
            }
        }
    }
}

function extractLocalIndices(ctx, boundary, sharedIndices, geometryCfg) {

    const vertices = ctx.vertices;
    const newBoundary = []

    for (let i = 0, len = boundary.length; i < len; i++) {

        const index = boundary[i];

        if (sharedIndices.includes(index)) {
            const vertexIndex = sharedIndices.indexOf(index);
            newBoundary.push(vertexIndex);

        } else {
            geometryCfg.positions.push(vertices[index][0]);
            geometryCfg.positions.push(vertices[index][1]);
            geometryCfg.positions.push(vertices[index][2]);

            newBoundary.push(sharedIndices.length);

            sharedIndices.push(index);
        }
    }

    return newBoundary
}

function getNormalOfPositions(positions, normal) {

    for (let i = 0; i < positions.length; i++) {

        let nexti = i + 1;
        if (nexti === positions.length) {
            nexti = 0;
        }

        normal[0] += ((positions[i].y - positions[nexti].y) * (positions[i].z + positions[nexti].z));
        normal[1] += ((positions[i].z - positions[nexti].z) * (positions[i].x + positions[nexti].x));
        normal[2] += ((positions[i].x - positions[nexti].x) * (positions[i].y + positions[nexti].y));
    }

    return math.normalizeVec3(normal);
}

function to2D(_p, _n, re) {

    const p = tempVec3a;
    const n = tempVec3b;
    const x3 = tempVec3c;

    p[0] = _p.x;
    p[1] = _p.y;
    p[2] = _p.z;

    n[0] = _n.x;
    n[1] = _n.y;
    n[2] = _n.z;

    x3[0] = 1.1;
    x3[1] = 1.1;
    x3[2] = 1.1;

    const dist = math.lenVec3(math.subVec3(x3, n));

    if (dist < 0.01) {
        x3[0] += 1.0;
        x3[1] += 2.0;
        x3[2] += 3.0;
    }

    const dot = math.dotVec3(x3, n);
    const tmp2 = math.mulVec3Scalar(n, dot, math.vec3());

    x3[0] -= tmp2[0];
    x3[1] -= tmp2[1];
    x3[2] -= tmp2[2];

    math.normalizeVec3(x3);

    const y3 = math.cross3Vec3(n, x3, math.vec3());
    const x = math.dotVec3(p, x3);
    const y = math.dotVec3(p, y3);

    re[0] = x;
    re[1] = y;
}

export {parseCityJSONIntoXKTModel};