Commit 51717470 authored by Matthias Betz's avatar Matthias Betz
Browse files

support geolocation

parent a5904da0
export const ifcIconSvg = `
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
......@@ -6,14 +7,13 @@
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="IFC_Icon_J_v01.svg"
sodipodi:docname="IFC_Icon_J_v02.svg"
xml:space="preserve"
inkscape:export-filename="..\Export\IFC_Icon_I_v01.svg"
inkscape:export-filename="..\Export\IFC_Icon_J_Plain_v01.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
......@@ -27,15 +27,15 @@
inkscape:document-units="px"
showborder="true"
showgrid="true"
inkscape:zoom="8"
inkscape:cx="-39.25"
inkscape:cy="13.3125"
inkscape:zoom="22.627417"
inkscape:cx="10.827573"
inkscape:cy="13.50132"
inkscape:window-width="1920"
inkscape:window-height="1129"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g7"
inkscape:current-layer="layer5"
showguides="false"><inkscape:grid
id="grid1"
units="px"
......@@ -284,13 +284,11 @@
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;stroke-width:3.78418;enable-background:accumulate;stop-color:#000000"
d="m 16.719483,5.3400135 c 0,1.0219276 0.415528,1.9976456 1.152991,2.7050956 0.737462,0.7074485 1.571251,1.0679705 2.442959,1.0587195 l 0.395171,1.9863654 C 19.14539,11.155244 17.620858,10.57823 16.490363,9.493742 15.359868,8.4092541 14.71653,6.9065791 14.71653,5.3400135 Z"
id="use13-5"
sodipodi:nodetypes="csccscc" /><use
x="0"
y="0"
xlink:href="#path10-4"
sodipodi:nodetypes="csccscc" /><path
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;stroke-width:3.78418;enable-background:accumulate;stop-color:#000000"
d="m 16.719483,1.6002563 c 0,1.0219286 0.415528,1.9976461 1.152991,2.7050959 0.737462,0.7074495 1.571251,1.0679715 2.44296,1.0587205 l 0.395171,1.9863596 C 19.14539,7.4154823 17.620858,6.8384683 16.490363,5.7539827 15.359868,4.6694977 14.71653,3.1668221 14.71653,1.6002563 Z"
id="use14-1"
transform="matrix(-1,0,0,1,29.433068,0)"
style="display:inline" /></g><g
sodipodi:nodetypes="csccscc" /></g><g
inkscape:groupmode="layer"
id="g7"
inkscape:label="Haus1"
......@@ -315,3 +313,6 @@
stroke-width="0"
id="path1"
style="display:inline;fill:#000000" /></g></svg>
`;
export default ifcIconSvg;
\ No newline at end of file
import {getPluginAssetUrl, NotificationType, ToolboxType, WindowSlot} from '@vcmap/ui';
import {
getPluginAssetUrl,
NotificationType,
ToolboxType,
WindowSlot,
} from '@vcmap/ui';
import * as Cesium from '@vcmap-cesium/engine';
import {
Cartesian3,
......@@ -15,6 +20,7 @@ import { mapVersion, name, version } from '../package.json';
import BIMOptions, { bimOptionsId } from './upload.vue';
import objectList, { loadCatalogModel, objectListId } from './objectList.vue';
import scaleControl, { scaleControlId } from './scaleControl.vue';
import ifcIconSvg from '@icon.js';
let model;
let conditions = [[true, true]];
......@@ -29,38 +35,12 @@ let hpRoll;
let app;
let currentlyScaledModel;
const hidingFeatures = [];
const hidingListener = function (tile) {
let allHidden = true;
for (const featureToHide of hidingFeatures) {
if (featureToHide.loops < 10) {
allHidden = false;
const feature = tile.content.getFeature(featureToHide.id);
if (feature !== undefined) {
app.maps.activeMap.layerCollection.globalHider.addFeature(
'manual',
feature,
);
app.maps.activeMap.layerCollection.globalHider.hideObjects(['manual']);
featureToHide.loops++;
}
}
}
if (allHidden) {
const primitives = app.maps.activeMap.getScene().primitives;
for (let i = 0; i < primitives.length; i++) {
const tileSet = primitives.get(i);
if (!(tileSet instanceof Cesium3DTileset)) {
continue;
}
tileSet.tileVisible.removeEventListener(hidingListener);
}
}
};
const pluginState = {
files: [],
objects: [],
geolocations: {},
movedBy: {},
};
function updateHideCondition(gmlId) {
......@@ -118,6 +98,9 @@ function saveObjects() {
const wgs84X = CMath.toDegrees(ellipPos.longitude);
const wgs84Y = CMath.toDegrees(ellipPos.latitude);
const height = ellipPos.height;
if (m.model.fromCatalog === undefined) {
m.model.fromCatalog = false;
}
outputArray.push({
name: m.name,
position: [wgs84X, wgs84Y, height],
......@@ -148,20 +131,82 @@ function saveFile(fileName, urlFile) {
a.remove();
}
async function placeModelAtPosition(vcMap, lon, lat, modelUrl, name) {
async function placeModelAtPosition(vcMap, lon, lat, modelUrl, name, rot, ele) {
// Create Cartographic position
const position = Cesium.Cartographic.fromDegrees(lon, lat);
// Wait for terrain height
const [updatedPosition] = await Cesium.sampleTerrainMostDetailed(
vcMap.getCesiumWidget().terrainProvider,
[position],
);
const height = updatedPosition.height + 10;
console.log("Height: " + height);
console.log("Lon: " + lon);
console.log("Lat: " + lat);
const cartesianPosition = Cartesian3.fromDegrees(lon, lat, height);
console.log(cartesianPosition);
let height = ele;
if (!height) {
let [updatedPosition] = await Cesium.sampleTerrainMostDetailed(
vcMap.getCesiumWidget().terrainProvider,
[position],
);
height = updatedPosition.height;
}
let cartesianPosition = Cartesian3.fromDegrees(lon, lat, height);
let flyToPos = Cartesian3.fromDegrees(lon, lat, height + 200);
const movedBy = pluginState.movedBy[name];
if (movedBy) {
console.log(movedBy);
// Old Cartesian position (Cesium.Cartesian3)
let oldCartesian = cartesianPosition;
// Build ENU transform at this point
let enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(oldCartesian);
// Local offset you want (meters)
let localOffset = new Cesium.Cartesian3(+movedBy.x, -movedBy.z, 0);
// Convert local ENU offset → ECEF movement
let offsetMatrix = Cesium.Matrix4.multiplyByTranslation(
enuTransform,
localOffset,
new Cesium.Matrix4(),
);
let newCartesian = Cesium.Matrix4.getTranslation(offsetMatrix, new Cesium.Cartesian3());
let newCartographic = Cesium.Cartographic.fromCartesian(newCartesian);
if (!ele) {
// update height for new location
// Wait for terrain height
let [updatedPosition] = await Cesium.sampleTerrainMostDetailed(
vcMap.getCesiumWidget().terrainProvider,
[newCartographic],
);
height = updatedPosition.height;
}
let newFlyPos = Cartesian3.fromRadians(
newCartographic.longitude,
newCartographic.latitude,
height + 200,
);
newCartesian = Cartesian3.fromRadians(
newCartographic.longitude,
newCartographic.latitude,
height,
);
enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(newCartesian);
// Local offset you want (meters)
localOffset = new Cesium.Cartesian3(0, 0, +movedBy.y);
// Convert local ENU offset → ECEF movement
offsetMatrix = Cesium.Matrix4.multiplyByTranslation(
enuTransform,
localOffset,
new Cesium.Matrix4(),
);
newCartesian = Cesium.Matrix4.getTranslation(offsetMatrix, new Cesium.Cartesian3());
flyToPos = newFlyPos;
cartesianPosition = newCartesian;
}
// Add the model to the viewer
model = vcMap.getEntities().add({
name: name,
......@@ -177,22 +222,33 @@ async function placeModelAtPosition(vcMap, lon, lat, modelUrl, name) {
model: model,
name: name,
});
if (rot) {
const roll = new HeadingPitchRoll();
hpRollMap.set(model, roll);
roll.heading = rot;
if (roll.heading > CMath.TWO_PI) {
roll.heading -= CMath.TWO_PI;
}
if (roll.heading < 0.0) {
roll.heading += CMath.TWO_PI;
}
const modelPos = model.position.getValue();
model.orientation = new ConstantProperty(
Transforms.headingPitchRollQuaternion(modelPos, roll),
);
}
vcMap.requestRender();
vcMap.getCesiumWidget().camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(lon, lat, 1500),
orientation: {
heading: Cesium.Math.toRadians(0.0), // rotation from north
pitch: Cesium.Math.toRadians(-45.0), // negative to look down
roll: 0.0,
},
duration: 2.0, // seconds
destination: flyToPos,
});
}
function setupModel(vcsApp, windowPos, url, name, fromCatalog) {
function setupModel(vcsApp, windowPos, url, name, fromCatalog, useGeolocation) {
const vcMap = vcsApp.maps.activeMap;
const geolocation = pluginState.geolocations[name];
if (geolocation === undefined) {
if (!useGeolocation || geolocation === undefined) {
setupModelWithoutGeolocation(
vcMap,
windowPos,
......@@ -205,7 +261,9 @@ function setupModel(vcsApp, windowPos, url, name, fromCatalog) {
} else {
const lon = geolocation.lon;
const lat = geolocation.lat;
placeModelAtPosition(vcMap, lon, lat, url, name);
const rot = geolocation.heading;
const ele = geolocation.ele;
placeModelAtPosition(vcMap, lon, lat, url, name, rot, ele);
}
}
......@@ -473,7 +531,6 @@ export default function smartVillagesPlugin(config, baseUrl) {
loadingObject.position[1],
loadingObject.position[2],
);
console.log(cartPos);
// Add the model to the viewer
model = vcMap.getEntities().add({
name: loadingObject.name,
......@@ -667,10 +724,11 @@ export default function smartVillagesPlugin(config, baseUrl) {
* @returns {Promise<void>}
*/
async onVcsAppMounted(vcsApp) {
let importedIcon = `svgString:${ifcIconSvg}`;
const bimGroup = {
id: 'bim-functions',
type: ToolboxType.GROUP,
icon: getPluginAssetUrl(app, name, 'plugin-assets/IFC_Icon_J_v01.svg'),
icon: importedIcon,
disabled: false,
title: 'BIM- und Planungsfunktionen',
};
......
......@@ -115,32 +115,49 @@ const items = ref(defaultItems);
let setupModelFunction;
let app;
let projectStateObject;
let uploadedModels = [];
function addModelToObjectList(name, url) {
function addModelToObjectList(name, url, hasGeolocation) {
if (projectStateObject === undefined) {
// object library wasn't opened before
return;
}
projectStateObject.objects.push({
name: name,
url: url,
});
const item = {
let actions = [
{
name: 'Objekt platzieren',
callback() {
const x = window.innerWidth / 2;
const y = window.innerHeight / 2;
const cart = new Cartesian2(x, y);
setupModelFunction(app, cart, url, name, false, false);
},
}
];
if (hasGeolocation) {
const geolocation = projectStateObject.geolocations[name];
const lon = geolocation.lon.toFixed(1);
const lat = geolocation.lat.toFixed(1);
actions.push({
name: 'Objekt platzieren mit Georeferenz (' + lon + '°E ' + lat + '°N)',
callback() {
const x = window.innerWidth / 2;
const y = window.innerHeight / 2;
const cart = new Cartesian2(x, y);
setupModelFunction(app, cart, url, name, false, true);
},
});
}
const placeModel = {
name: name,
title: name,
actions: [
{
name: 'Objekt platzieren',
callback() {
const x = window.innerWidth / 2;
const y = window.innerHeight / 2;
const cart = new Cartesian2(x, y);
setupModelFunction(app, cart, url, name, false);
},
}
]
actions: actions
};
items.value.push(item);
items.value.push(placeModel);
}
async function loadCatalogsFromConfig(config) {
......@@ -301,13 +318,28 @@ export async function loadCatalogModel(modelLink) {
})
}
export function loadModel(url, name) {
export function loadModelFromUrl(url, name, hasGeolocation) {
if (projectStateObject === undefined) {
// object library wasn't opened before
// store them, and load them once opened
uploadedModels.push({
name: name,
url: url,
hasGeolocation: hasGeolocation,
});
return;
}
const loader = new GLTFLoader();
loader.load(url, function (gltf) {
let bounds = new THREE.Box3().setFromObject(gltf.scene);
const x = bounds.min.x + (bounds.max.x - bounds.min.x) / 2;
const z = bounds.min.z + (bounds.max.z - bounds.min.z) / 2;
const m = new THREE.Matrix4().makeTranslation(-x, -bounds.min.y, -z);
projectStateObject.movedBy[name] = {
x: x,
y: bounds.min.y,
z: z,
}
gltf.scene.applyMatrix4(m);
const exporter = new GLTFExporter();
const options = {
......@@ -319,7 +351,7 @@ export function loadModel(url, name) {
function (output) {
const blob = new Blob([output]);
url = URL.createObjectURL(blob);
addModelToObjectList(name, url);
addModelToObjectList(name, url, hasGeolocation);
},
// called when there is an error in the generation
function (error) {
......@@ -364,7 +396,7 @@ function uploadModel() {
type: "application/octet-stream",
});
let url = URL.createObjectURL(blob);
loadModel(url, file.name);
loadModelFromUrl(url, file.name, false);
}
reader.readAsArrayBuffer(file);
}
......@@ -434,7 +466,15 @@ export default {
const dialog = ref(false);
setupModelFunction = setupModel;
projectStateObject = pluginState;
onMounted(() => loadCatalogsFromConfig(config).then(() => { console.log("Object library loaded") }));
onMounted(() => {
loadCatalogsFromConfig(config).then(() => { console.log("Object library loaded") });
for (const o of uploadedModels) {
loadModelFromUrl(o.url, o.name, o.hasGeolocation);
}
uploadedModels = [];
});
return {
draggable,
selectable,
......@@ -548,15 +588,22 @@ export default {
display: grid;
grid-template-columns: 1fr 1fr;
}
.no-scroll {
max-height: none; /* remove height limit */
overflow: visible; /* show all content */
max-height: none;
/* remove height limit */
overflow: visible;
/* show all content */
padding-top: 0;
padding-bottom: 0;
}
.list-col {
height: auto; /* let column grow with content */
min-height: 0; /* reset flex min-height */
padding: 0; /* remove top/bottom padding if necessary */
height: auto;
/* let column grow with content */
min-height: 0;
/* reset flex min-height */
padding: 0;
/* remove top/bottom padding if necessary */
}
</style>
......@@ -3,13 +3,13 @@
<v-card class="pa-2 ma-2">
<v-container>
<v-row no-gutters class="justify-center">
<v-col>
<v-col class="flex-grow-0">
<VcsLabel html-for="fileInput">
IFC Datei:
</VcsLabel>
</v-col>
<v-col>
<VcsTextField id="fileInput" type="file" @change="onFileSelected" />
<VcsFileInput id="fileInput" type="file" @change="onFileSelected" accept=".ifc" />
</v-col>
</v-row>
<v-row no-gutters class="justify-center">
......@@ -29,6 +29,7 @@ import {
VcsRadio,
VcsFormButton,
VcsTextField,
VcsFileInput,
NotificationType,
VcsFormattedNumber,
VcsFormSection,
......@@ -39,7 +40,7 @@ import {
import { VContainer, VRow, VForm, VCol } from 'vuetify/components';
import { name } from '../package.json';
import ConverterStatus from "./converterStatus.vue";
import {loadModel} from "./objectList.vue";
import {loadModelFromUrl} from "./objectList.vue";
export const bimOptionsId = 'upload_ifc_id';
......@@ -55,6 +56,7 @@ export default {
VcsSelect,
VcsTextField,
VcsCheckbox,
VcsFileInput,
VcsRadio,
VcsFormattedNumber,
VcsFormSection,
......@@ -102,10 +104,18 @@ export default {
}
if (res.ok) {
// status code was 200-299
let message = "Datei erfolgreich konvertiert.";
let timeout = 5000;
if (geolocationString) {
const geolocation = JSON.parse(geolocationString);
message = "Datei erfolgreich konvertiert. Gefundene Position: (" + geolocation.lon
+ "°E " + geolocation.lat + "°N)";
timeout = 15000;
}
app.notifier.add({
type: NotificationType.SUCCESS,
message: "Datei erfolgreich konvertiert",
timeout: 5000,
message: message,
timeout: timeout,
});
return res.blob();
} else {
......@@ -120,6 +130,7 @@ export default {
type: NotificationType.ERROR,
message: errorMessage,
});
disable.value = false;
}).then((blob) => {
if (blob != null) {
var url = window.URL.createObjectURL(blob);
......@@ -129,7 +140,11 @@ export default {
document.body.appendChild(a);
a.click();
a.remove();
loadModel(url, filename);
let hasGeolocation = pluginState.geolocations[filename] != undefined;
if (!hasGeolocation) {
hasGeolocation = false;
}
loadModelFromUrl(url, filename, hasGeolocation);
}
})
},
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment