diff --git a/src/components/generic/maplibre-wrapper/MaplibreWrapper.scss b/src/components/generic/maplibre-wrapper/MaplibreWrapper.scss
deleted file mode 100644
index 8ec8786f1..000000000
--- a/src/components/generic/maplibre-wrapper/MaplibreWrapper.scss
+++ /dev/null
@@ -1,25 +0,0 @@
-.maplibre-wrapper {
- position: relative;
- width: 100%;
- height: 100%;
- filter: grayscale(0.7);
-
- .maptiler-link {
- position: absolute;
- z-index: 1;
- bottom: calc(var(--spacing-unit) / 4);
- left: calc(var(--spacing-unit) / 2);
- user-select: none;
- }
-
- &:deep() {
- .maplibregl-canvas-container {
- width: 100%;
- height: 100%;
- }
-
- .maplibregl-canvas {
- outline: none;
- }
- }
-}
diff --git a/src/components/generic/maplibre-wrapper/MaplibreWrapper.vue b/src/components/generic/maplibre-wrapper/MaplibreWrapper.vue
index 42c427855..4a2aafc65 100644
--- a/src/components/generic/maplibre-wrapper/MaplibreWrapper.vue
+++ b/src/components/generic/maplibre-wrapper/MaplibreWrapper.vue
@@ -3,13 +3,13 @@
-
+
-
+
diff --git a/src/components/specific/models/models-map/ModelsMap.vue b/src/components/specific/models/models-map/ModelsMap.vue
new file mode 100644
index 000000000..bd5ef3d57
--- /dev/null
+++ b/src/components/specific/models/models-map/ModelsMap.vue
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/specific/projects/project-card/ProjectCard.vue b/src/components/specific/projects/project-card/ProjectCard.vue
index 8528ac7c8..7b31b7a29 100644
--- a/src/components/specific/projects/project-card/ProjectCard.vue
+++ b/src/components/specific/projects/project-card/ProjectCard.vue
@@ -67,7 +67,7 @@
+
+
+
diff --git a/src/composables/maplibre.js b/src/composables/maplibre.js
deleted file mode 100644
index 382f6309e..000000000
--- a/src/composables/maplibre.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import maplibregl from "maplibre-gl";
-
-const MAP_TILER_TOKEN = ENV.VUE_APP_MAPTILER_TOKEN;
-
-export function useMaplibre(containerID) {
- const loadMap = (longitude, latitude) => {
- if (!document.getElementById(containerID)) {
- // Do not try to load map if container element does not exist
- return;
- }
-
- if (longitude && latitude) {
- try {
- const map = new maplibregl.Map({
- container: containerID,
- style: `https://api.maptiler.com/maps/streets/style.json?key=${MAP_TILER_TOKEN}`,
- center: [longitude, latitude],
- zoom: 15.5,
- pitch: 45,
- bearing: -17.6,
- attributionControl: false,
- antialias: true
- });
-
- map.on("load", () => {
- const labelsLayer = map
- .getStyle()
- .layers.find(
- layer => layer.type === "symbol" && layer.layout["text-field"]
- );
-
- map.addLayer(
- {
- type: "fill-extrusion",
- id: "3d-buildings",
- source: "openmaptiles",
- "source-layer": "building",
- filter: ["==", "extrude", "true"],
- minzoom: 15,
- paint: {
- "fill-extrusion-color": "#aaa",
- "fill-extrusion-opacity": 0.6,
- "fill-extrusion-height": [
- "interpolate",
- ["linear"],
- ["zoom"],
- 15,
- 0,
- 15.05,
- ["get", "height"]
- ],
- "fill-extrusion-base": [
- "interpolate",
- ["linear"],
- ["zoom"],
- 15,
- 0,
- 15.05,
- ["get", "min_height"]
- ]
- }
- },
- labelsLayer ? labelsLayer.id : undefined
- );
-
- new maplibregl.Marker({ color: "#2F374A" /* color primary */ })
- .setLngLat([longitude, latitude])
- .addTo(map);
- });
- } catch (error) {
- console.warn(error);
- }
- }
- };
-
- return {
- loadMap
- };
-}
diff --git a/src/services/ModelService.js b/src/services/ModelService.js
index 64abe7109..acd742233 100644
--- a/src/services/ModelService.js
+++ b/src/services/ModelService.js
@@ -6,9 +6,7 @@ import { ERRORS, RuntimeError, ErrorService } from "./ErrorService.js";
class ModelService {
constructor() {
this.cache = new Map();
- this.callQueue = queue(async task => {
- return await task();
- }, 40);
+ this.callQueue = queue(async task => { return await task(); }, 40);
}
async fetchModels(project, { cache } = {}) {
@@ -113,14 +111,16 @@ class ModelService {
}
fetchModelElements(project, model, params = {}) {
- return apiClient.modelApi.getElements(
- project.cloud.id,
- model.id,
- project.id,
- params.classification,
- params.classificationNotation,
- undefined, // property_filter
- params.type
+ return this.callQueue.push(() =>
+ apiClient.modelApi.getElements(
+ project.cloud.id,
+ model.id,
+ project.id,
+ params.classification,
+ params.classificationNotation,
+ undefined, // property_filter
+ params.type
+ )
);
}
diff --git a/src/utils/location.js b/src/utils/location.js
index e606f0343..f35c08e38 100644
--- a/src/utils/location.js
+++ b/src/utils/location.js
@@ -96,7 +96,7 @@ function DMS2DD([degrees, minutes, seconds, secondsFraction = 0], type) {
seconds *= -1;
secondsFraction *= -1;
}
- seconds += secondsFraction/1000000;
+ seconds += secondsFraction / 1_000_000;
const dmsString = `${degrees}°${minutes}′${seconds}″ ${direction}`;
return parseDMS(dmsString);
}
@@ -132,7 +132,7 @@ function DD2DMS(lat, long) {
const MAP_TILER_TOKEN = ENV.VUE_APP_MAPTILER_TOKEN;
/**
- * Get the DD coordinates of a given address using OpenStreetMapAPI.
+ * Get the DD coordinates of a given address using Maptiler API.
* The returned value is an object with a "longitude" and "latitude" fields.
* If no coordinates are found, "longitude" and "latitude" will be null.
*
diff --git a/src/utils/maplibre.js b/src/utils/maplibre.js
new file mode 100644
index 000000000..e9a2c6aa4
--- /dev/null
+++ b/src/utils/maplibre.js
@@ -0,0 +1,63 @@
+import maplibregl from "maplibre-gl";
+
+const MAP_TILER_TOKEN = ENV.VUE_APP_MAPTILER_TOKEN;
+const MAP_STYLE_URL = `https://api.maptiler.com/maps/streets/style.json?key=${MAP_TILER_TOKEN}`;
+const MAP_DEFAULT_CENTER = [2.294481, 48.858370]; // Tour Eiffel
+
+const MARKER_COLOR = "#2F374A"; // color primary
+
+export class MaplibreMap {
+ constructor(containerId, center = MAP_DEFAULT_CENTER) {
+ this.map = new maplibregl.Map({
+ container: containerId,
+ style: MAP_STYLE_URL,
+ center,
+ zoom: 15.5,
+ pitch: 45,
+ bearing: 0,
+ attributionControl: false,
+ });
+
+ const { promise, resolve } = Promise.withResolvers();
+ this.isReady = promise;
+
+ this.map.on("load", resolve);
+ }
+
+ setCenter(center) {
+ this.map.setCenter(center);
+ }
+
+ addMarker([longitude, latitude]) {
+ const marker = new maplibregl.Marker({ color: MARKER_COLOR });
+ marker.setLngLat([longitude, latitude]);
+
+ return this.isReady.then(() => marker.addTo(this.map));
+ }
+}
+
+export function computeBounds(markers) {
+ if (markers.length < 2) {
+ console.warn("[computeBounds] We need at least 2 markers to compute map bounds.");
+ return;
+ }
+
+ markers = markers.map(m => {
+ const { lng, lat } = m.getLngLat();
+ return [lng, lat];
+ });
+
+ let [a0, b0] = markers.pop(), [a1, b1] = markers.pop();
+ let sw = [Math.min(a0, a1), Math.min(b0, b1)]; // south-west
+ let ne = [Math.max(a0, a1), Math.max(b0, b1)]; // north-east
+
+ while (markers.length > 0) {
+ let [lng, lat] = markers.pop();
+ if (lng < sw[0]) sw[0] = lng;
+ if (lat < sw[1]) sw[1] = lat;
+ if (lng > ne[0]) ne[0] = lng;
+ if (lat > ne[1]) ne[1] = lat;
+ }
+
+ return [sw, ne];
+}
diff --git a/src/views/space-board/SpaceBoard.scss b/src/views/space-board/SpaceBoard.scss
deleted file mode 100644
index cbe5cd4c2..000000000
--- a/src/views/space-board/SpaceBoard.scss
+++ /dev/null
@@ -1,34 +0,0 @@
-.space-board {
- height: 100%;
-
- &__banner {
- position: absolute;
- top: 0;
- left: 0;
- }
-
- // Add top padding to header when banner is displayed
- &__banner.visible + &__header {
- padding-top: var(--spacing-unit);
- }
-
- &__header {
- &__search {
- background-color: var(--color-white);
- }
-
- &__actions {
- display: flex;
- align-items: center;
- gap: var(--spacing-unit);
- }
-
- &__btn {
- color: var(--color-granite-light);
- }
- }
-
- &__body {
- padding: calc(var(--spacing-unit) * 2) 0;
- }
-}
diff --git a/src/views/space-board/SpaceBoard.vue b/src/views/space-board/SpaceBoard.vue
index e75f4667e..bac572ad4 100644
--- a/src/views/space-board/SpaceBoard.vue
+++ b/src/views/space-board/SpaceBoard.vue
@@ -61,18 +61,22 @@
-
-
-
-
-
-
+
@@ -100,10 +104,11 @@ import GoBackButton from "../../components/specific/app/go-back-button/GoBackBut
import ViewHeader from "../../components/specific/app/view-header/ViewHeader.vue";
import ProjectCard from "../../components/specific/projects/project-card/ProjectCard.vue";
import ProjectCreationCard from "../../components/specific/projects/project-creation-card/ProjectCreationCard.vue";
+import ProjectsMap from "../../components/specific/projects/projects-map/ProjectsMap.vue";
+import StatusFilterButton from "../../components/specific/projects/status-filter-button/StatusFilterButton.vue";
import SpaceSizeInfo from "../../components/specific/subscriptions/space-size-info/SpaceSizeInfo.vue";
import SubscriptionStatusBanner from "../../components/specific/subscriptions/subscription-status-banner/SubscriptionStatusBanner.vue";
import SpaceUsersManager from "../../components/specific/users/space-users-manager/SpaceUsersManager.vue";
-import StatusFilterButton from "../../components/specific/projects/status-filter-button/StatusFilterButton.vue";
export default {
components: {
@@ -113,11 +118,12 @@ export default {
GoBackButton,
ProjectCard,
ProjectCreationCard,
+ ProjectsMap,
SpaceSizeInfo,
SpaceUsersManager,
+ StatusFilterButton,
SubscriptionStatusBanner,
ViewHeader,
- StatusFilterButton,
},
setup() {
const viewContainer = inject("viewContainer");
@@ -142,6 +148,12 @@ export default {
const { sortToggle: sortProjects } = useListSort(displayedProjects, (project) => project.name);
+ const map = ref(null);
+ const onProjectLoaded = ({ project, models }) => {
+ project.models = models;
+ map.value.addProject(project);
+ };
+
useInterval(() => {
if (isOpenRight.value) {
loadSpaceUsers(currentSpace.value);
@@ -168,6 +180,7 @@ export default {
// References
invitations: spaceInvitations,
IS_SUBSCRIPTION_ENABLED,
+ map,
projects: displayedProjects,
searchText,
space: currentSpace,
@@ -177,6 +190,7 @@ export default {
filteredProjects,
// Methods
isSpaceAdmin,
+ onProjectLoaded,
openUsersManager: openSidePanel,
sortProjects,
// Responsive breakpoints
@@ -186,4 +200,47 @@ export default {
};
-
+