Skip to content

Commit

Permalink
PB-849: show segments multi segment file
Browse files Browse the repository at this point in the history
  • Loading branch information
sommerfe committed Nov 15, 2024
1 parent 0c00f5a commit a5cc29e
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 67 deletions.
61 changes: 43 additions & 18 deletions src/api/profile/ElevationProfile.class.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,42 @@ import { LineString } from 'ol/geom'
* calculation related to profile (hiking time, slop/distance, etc...)
*/
export default class ElevationProfile {
/** @param {ElevationProfileSegment[]} segments */
/**
* Creates an instance of ElevationProfile.
*
* @param {ElevationProfileSegment[]} segments - An array of elevation profile segments.
* @param {number} _activeSegmentIndex - The index of the active segment.
*/
constructor(segments) {
/** @type {ElevationProfileSegment[]} */
this.segments = [...segments]
this._activeSegmentIndex = 0
}

get points() {
return this.segments.flatMap((segment) => segment.points)
}

get segmentPoints() {
return this.segments[this._activeSegmentIndex].points
}

/** @returns {Number} */
get segmentsCount() {
return this.segments.length
}

/** @returns {Number} */
get activeSegmentIndex() {
return this._activeSegmentIndex
}

set activeSegmentIndex(index) {
if (index < 0 || index >= this.segmentsCount) {
return
}
this._activeSegmentIndex = index
}

/** @returns {Number} */
get length() {
return this.points.length
Expand All @@ -38,15 +64,17 @@ export default class ElevationProfile {
if (!this.hasDistanceData) {
return 0
}
return this.segments.slice(-1)[0].maxDist
return this.segments[this._activeSegmentIndex].maxDist
}

/** @returns {Number} */
get maxElevation() {
if (!this.hasElevationData) {
return 0
}
return Math.max(...this.points.map((point) => point.elevation))
return Math.max(
...this.segments[this._activeSegmentIndex].points.map((point) => point.elevation)
)
}

/** @returns {Number} */
Expand All @@ -55,7 +83,9 @@ export default class ElevationProfile {
return 0
}
return Math.min(
...this.points.filter((point) => point.hasElevationData).map((point) => point.elevation)
...this.segments[this._activeSegmentIndex].points
.filter((point) => point.hasElevationData)
.map((point) => point.elevation)
)
}

Expand All @@ -64,26 +94,23 @@ export default class ElevationProfile {
if (!this.hasElevationData) {
return 0
}
return this.points.slice(-1)[0].elevation - this.points[0].elevation
return (
this.segments[this._activeSegmentIndex].points.slice(-1)[0].elevation -
this.segments[this._activeSegmentIndex].points[0].elevation
)
}

get totalAscent() {
return this.segments.reduce((totalAscent, currentSegment) => {
return totalAscent + currentSegment.totalAscent
}, 0)
return this.segments[this._activeSegmentIndex].totalAscent
}

get totalDescent() {
return this.segments.reduce((totalDescent, currentSegment) => {
return totalDescent + currentSegment.totalDescent
}, 0)
return this.segments[this._activeSegmentIndex].totalDescent
}

/** @returns {Number} Sum of slope/surface distances (distance on the ground) */
get slopeDistance() {
return this.segments.reduce((slopeDistance, currentSegment) => {
return slopeDistance + currentSegment.slopeDistance
}, 0)
return this.segments[this._activeSegmentIndex].slopeDistance
}

get coordinates() {
Expand All @@ -105,8 +132,6 @@ export default class ElevationProfile {
* @returns {number} Estimation of hiking time for this profile
*/
get hikingTime() {
return this.segments.reduce((hikingTime, currentSegment) => {
return hikingTime + currentSegment.hikingTime
}, 0)
return this.segments[this._activeSegmentIndex].hikingTime
}
}
89 changes: 52 additions & 37 deletions src/api/profile/profile.api.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,56 +166,71 @@ export async function getProfileDataForChunk(chunk, startingPoint, startingDist,
/**
* Gets profile from https://api3.geo.admin.ch/services/sdiservices.html#profile
*
* @param {[Number, Number][]} coordinates Coordinates, expressed in the given projection, from
* which we want the profile
* @param {[Number, Number][]} profileCoordinates Coordinates, expressed in the given projection,
* from which we want the profile
* @param {CoordinateSystem} projection The projection used to describe the coordinates
* @returns {ElevationProfile | null} The profile, or null if there was no valid data to produce a
* profile
* @throws ProfileError
*/
export default async (coordinates, projection) => {
if (!coordinates || coordinates.length === 0) {
export default async (profileCoordinates, projection) => {
if (!profileCoordinates || profileCoordinates.length === 0) {
const errorMessage = `Coordinates not provided`
log.error(errorMessage)
throw new ProfileError(errorMessage, 'could_not_generate_profile')
}
// the service only works with LV95 coordinate, we have to transform them if they are not in this projection
// removing any 3d dimension that could come from OL
let coordinatesInLV95 = removeZValues(unwrapGeometryCoordinates(coordinates))
if (projection.epsg !== LV95.epsg) {
coordinatesInLV95 = coordinates.map((coordinate) =>
proj4(projection.epsg, LV95.epsg, coordinate)
)
}
const segments = []
let coordinateChunks = splitIfTooManyPoints(LV95.bounds.splitIfOutOfBounds(coordinatesInLV95))
if (!coordinateChunks) {
log.error('No chunks found, no profile data could be fetched', coordinatesInLV95)
throw new ProfileError(
'No chunks found, no profile data could be fetched',
'could_not_generate_profile'
)
const hasDoubleNestedArray = (arr) =>
arr.some((item) => Array.isArray(item) && item.some((subItem) => Array.isArray(subItem)))

// if the profileCoordinates is not a double nested array, we make it one
// segmented files have a double nested array, but not all files or self made drawings
// so we have to make sure we have a double nested array and then iterate over it
if (!hasDoubleNestedArray(profileCoordinates)) {
profileCoordinates = [profileCoordinates]
}
let lastCoordinate = null
let lastDist = 0
const requestsForChunks = coordinateChunks.map((chunk) =>
getProfileDataForChunk(chunk, lastCoordinate, lastDist, projection)
)
for (const chunkResponse of await Promise.allSettled(requestsForChunks)) {
if (chunkResponse.status === 'fulfilled') {
const segment = parseProfileFromBackendResponse(
chunkResponse.value,
lastDist,
projection
for (const coordinates of profileCoordinates) {
// The service only works with LV95 coordinate, we have to transform them if they are not in this projection
// removing any 3d dimension that could come from OL
let coordinatesInLV95 = removeZValues(unwrapGeometryCoordinates(coordinates))
if (projection.epsg !== LV95.epsg) {
coordinatesInLV95 = coordinates.map((coordinate) =>
proj4(projection.epsg, LV95.epsg, coordinate)
)
if (segment) {
const newSegmentLastPoint = segment.points.slice(-1)[0]
lastCoordinate = newSegmentLastPoint.coordinate
lastDist = newSegmentLastPoint.dist
segments.push(segment)
}
let coordinateChunks = splitIfTooManyPoints(
LV95.bounds.splitIfOutOfBounds(coordinatesInLV95)
)

if (!coordinateChunks) {
log.error('No chunks found, no profile data could be fetched', coordinatesInLV95)
throw new ProfileError(
'No chunks found, no profile data could be fetched',
'could_not_generate_profile'
)
}
let lastCoordinate = null
let lastDist = 0
const requestsForChunks = coordinateChunks.map((chunk) =>
getProfileDataForChunk(chunk, lastCoordinate, lastDist, projection)
)

for (const chunkResponse of await Promise.allSettled(requestsForChunks)) {
if (chunkResponse.status === 'fulfilled') {
const segment = parseProfileFromBackendResponse(
chunkResponse.value,
lastDist,
projection
)
if (segment) {
const newSegmentLastPoint = segment.points.slice(-1)[0]
lastCoordinate = newSegmentLastPoint.coordinate
lastDist = newSegmentLastPoint.dist
segments.push(segment)
}
} else {
log.error('Error while getting profile for chunk', chunkResponse.reason?.message)
}
} else {
log.error('Error while getting profile for chunk', chunkResponse.reason?.message)
}
}
return new ElevationProfile(segments)
Expand Down
1 change: 1 addition & 0 deletions src/modules/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
"search_in_catalogue_placeholder": "Suche in importierten Karten",
"search_placeholder": "Suche nach Adressen, Parzellen oder Karten",
"search_title": "Ort suchen oder Karte hinzufügen:",
"profile_segment": "Segment {segmentNumber}",
"select_feature_annotation": "Klicke, um den Text zu selektieren",
"select_feature_linepolygon": "Klicke, um die Flächenlinie zu selektieren",
"select_feature_marker": "Klicke, um das Symbol zu selektieren",
Expand Down
1 change: 1 addition & 0 deletions src/modules/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
"search_in_catalogue_placeholder": "Search in imported maps",
"search_placeholder": "Search for addresses, parcels or maps",
"search_title": "Search for a place or add a map:",
"profile_segment": "Segment {segmentNumber}",
"select_feature_annotation": "Click to select the text",
"select_feature_linepolygon": "Click to select the line or the surface",
"select_feature_marker": "Click to select the marker",
Expand Down
1 change: 1 addition & 0 deletions src/modules/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
"search_in_catalogue_placeholder": "Rechercher dans les cartes importées",
"search_placeholder": "Recherche d'adresse, parcelles ou cartes",
"search_title": "Rechercher un lieu ou ajouter une carte :",
"profile_segment": "Segment {segmentNumber}",
"select_feature_annotation": "Cliquer pour selectionner l'annotation",
"select_feature_linepolygon": "Cliquer pour selectionner le trait ou la surface",
"select_feature_marker": "Cliquer pour sélectionner le symbole",
Expand Down
1 change: 1 addition & 0 deletions src/modules/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@
"search_in_catalogue_placeholder": "Cerca nelle mappe importate",
"search_placeholder": "Ricerca di indirizzi, parcelle o mappe",
"search_title": "Cercare un luogo od aggiungi un set di dati :",
"profile_segment": "Segmento {segmentNumber}",
"select_feature_annotation": "Cliccare per selezionare il testo",
"select_feature_linepolygon": "Cliccare per selezionare la linea o la superficie",
"select_feature_marker": "Cliccare per selezionare il simbolo",
Expand Down
1 change: 1 addition & 0 deletions src/modules/i18n/locales/rm.json
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,7 @@
"search_in_catalogue_placeholder": "Tschertga en chartas importadas",
"search_placeholder": "Tschertga d'adressas, parcellas u cartas",
"search_title": "Tschertgar lieu u agiuntar charta :",
"profile_segment": "Segment {segmentNumber}",
"select_feature_annotation": "Cliccar per tscherner il text",
"select_feature_linepolygon": "Cliccar per tscherner il Lingia / surfatscha",
"select_feature_marker": "Cliccar per tscherner il indicatur",
Expand Down
30 changes: 28 additions & 2 deletions src/modules/infobox/components/FeatureElevationProfilePlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
@mouseenter="startPositionTracking"
@mouseleave="stopPositionTracking"
>
<div v-if="elevationProfile.segmentsCount > 1" class="d-flex gap-1 segment-container">
<button
v-for="(_, index) in elevationProfile.segments"
:key="index"
class="btn btn-light text-nowrap"
:class="{ 'btn-dark': index === elevationProfile.activeSegmentIndex }"
:data-cy="`profile-segment-button-${index}`"
@click="() => activateSegmentIndex(index)"
>
{{ $t('profile_segment', { segmentNumber: index + 1 }) }}
</button>
</div>
<!-- Here below we need to set the w-100 in order to have proper PDF print of the Chart -->
<LineChart
ref="chart"
Expand Down Expand Up @@ -66,7 +78,7 @@
<script>
import { resetZoom } from 'chartjs-plugin-zoom'
import { Line as LineChart } from 'vue-chartjs'
import { mapState } from 'vuex'
import { mapActions, mapState } from 'vuex'
import ElevationProfile from '@/api/profile/ElevationProfile.class'
import FeatureElevationProfilePlotCesiumBridge from '@/modules/infobox/FeatureElevationProfilePlotCesiumBridge.vue'
Expand All @@ -85,6 +97,7 @@ const GAP_BETWEEN_TOOLTIP_AND_PROFILE = 12 //px
* @property {Number} elevation
* @property {Boolean} hasElevationData
*/
const dispatcher = { dispatcher: 'FeatureElevationProfilePlot.vue' }
/**
* Encapsulate ChartJS profile plot generation.
Expand Down Expand Up @@ -213,7 +226,7 @@ export default {
datasets: [
{
label: `${this.$t('elevation')}`,
data: this.elevationProfile.points,
data: this.elevationProfile.segmentPoints,
parsing: {
xAxisKey: 'dist',
yAxisKey: 'elevation',
Expand Down Expand Up @@ -384,6 +397,7 @@ export default {
}
},
methods: {
...mapActions(['setActiveSegmentIndex']),
startPositionTracking() {
this.track = true
},
Expand All @@ -405,6 +419,12 @@ export default {
resizeChart() {
this.$refs.chart.chart.resize()
},
activateSegmentIndex(index) {
this.setActiveSegmentIndex({
index,
...dispatcher,
})
},
},
}
</script>
Expand All @@ -426,6 +446,7 @@ $tooltip-width: 170px;
.profile-graph {
width: 100%;
flex-direction: column;
&-container {
overflow: hidden;
Expand All @@ -434,6 +455,11 @@ $tooltip-width: 170px;
pointer-events: auto;
}
}
.segment-container {
overflow-x: auto;
}
.profile-tooltip {
width: $tooltip-width;
height: $tooltip-height;
Expand Down
4 changes: 3 additions & 1 deletion src/modules/infobox/components/FeatureListCategoryItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import EditableFeature from '@/api/features/EditableFeature.class'
import LayerFeature from '@/api/features/LayerFeature.class'
import FeatureDetail from '@/modules/infobox/components/FeatureDetail.vue'
import ShowGeometryProfileButton from '@/modules/infobox/components/ShowGeometryProfileButton.vue'
import { canFeatureShowProfile } from '@/store/modules/features.store'
import TextTruncate from '@/utils/components/TextTruncate.vue'
import ZoomToExtentButton from '@/utils/components/ZoomToExtentButton.vue'
Expand All @@ -32,6 +33,7 @@ const { name, item, showContentByDefault } = toRefs(props)
const content = ref(null)
const featureTitle = ref(null)
const showContent = ref(!!showContentByDefault.value)
const canDisplayProfile = computed(() => canFeatureShowProfile(item.value))
const store = useStore()
const isHighlightedFeature = computed(
Expand Down Expand Up @@ -107,7 +109,7 @@ function showContentAndScrollIntoView(event) {
@mouseleave.passive="clearHighlightedFeature"
>
<FeatureDetail :feature="item" />
<div class="d-grid p-1">
<div v-if="canDisplayProfile" class="d-grid p-1">
<ShowGeometryProfileButton :feature="item" @click="showContentAndScrollIntoView" />
</div>
</div>
Expand Down
Loading

0 comments on commit a5cc29e

Please sign in to comment.