diff --git a/.env b/.env
index 8e4c4d78..a202379a 100644
--- a/.env
+++ b/.env
@@ -1,3 +1,5 @@
+VITE_V3_API_HOST="https://map.geoportail.lu/"
+
# Proxy urls
VITE_USE_PROXYURL=true
VITE_PROXYURL_WMS="/ogcproxywms"
@@ -9,6 +11,15 @@ VITE_GET_LEGENDS_URL="/legends/get_html"
VITE_GET_METADATA_URL="/getMetadata"
VITE_GET_INFO_SERVICE_URL="/getfeatureinfo"
+# Urls for location info
+VITE_FORAGE_URL="/getRapportForageVirtuel"
+VITE_LIDAR_URL="https://lidar.geoportail.lu"
+VITE_CYCLOMEDIA_URL="http://streetsmart.cyclomedia.com/streetsmart"
+VITE_OBLIQUE_URL="https://oblique.geoportail.lu/publication/viewer"
+VITE_SHORT_URL="/short/create"
+VITE_QR_URL="/qr"
+VITE_ADDRESS_URL="/geocode/reverse"
+
# Paths for symbols
VITE_SYMBOL_ICONS_URL="/mymaps"
VITE_SYMBOLS_URL="/mymaps/symbols"
diff --git a/.env.development b/.env.development
index d520915b..07678da4 100644
--- a/.env.development
+++ b/.env.development
@@ -1,3 +1,5 @@
+VITE_V3_API_HOST="http://localhost:8080/"
+
# Proxy urls
VITE_USE_PROXYURL=true
VITE_PROXYURL_WMS="https://migration.geoportail.lu/ogcproxywms"
@@ -9,6 +11,15 @@ VITE_GET_LEGENDS_URL="https://migration.geoportail.lu/legends/get_html"
VITE_GET_METADATA_URL="https://migration.geoportail.lu/getMetadata"
VITE_GET_INFO_SERVICE_URL="https://migration.geoportail.lu/getfeatureinfo"
+# Urls for location info
+VITE_FORAGE_URL="http://localhost:8080/getRapportForageVirtuel"
+VITE_LIDAR_URL="https://lidar.geoportail.lu"
+VITE_CYCLOMEDIA_URL="http://streetsmart.cyclomedia.com/streetsmart"
+VITE_OBLIQUE_URL="https://oblique.geoportail.lu/publication/viewer"
+VITE_SHORT_URL="http://localhost:8080/short/create"
+VITE_QR_URL="http://localhost:8080/qr"
+VITE_ADDRESS_URL="http://localhost:8080/geocode/reverse"
+
# Paths for symbols
VITE_SYMBOL_ICONS_URL="https://map.geoportail.lu/mymaps" # !!! use prod because of CORS
VITE_SYMBOLS_URL="https://map.geoportail.lu/mymaps/symbols" # !!! use prod because of CORS
diff --git a/.env.e2e b/.env.e2e
index b5c76e50..636e126a 100644
--- a/.env.e2e
+++ b/.env.e2e
@@ -1,3 +1,5 @@
+VITE_V3_API_HOST="https://migration.geoportail.lu/"
+
# Proxy urls
VITE_USE_PROXYURL=true
VITE_PROXYURL_WMS="https://map.geoportail.lu/ogcproxywms"
@@ -9,6 +11,15 @@ VITE_GET_LEGENDS_URL="https://migration.geoportail.lu/legends/get_html"
VITE_GET_METADATA_URL="https://migration.geoportail.lu/getMetadata"
VITE_GET_INFO_SERVICE_URL="https://migration.geoportail.lu/getfeatureinfo"
+# Urls for location info
+VITE_FORAGE_URL="https://migration.geoportail.lu/getRapportForageVirtuel"
+VITE_LIDAR_URL="https://lidar.geoportail.lu"
+VITE_CYCLOMEDIA_URL="http://streetsmart.cyclomedia.com/streetsmart"
+VITE_OBLIQUE_URL="https://oblique.geoportail.lu/publication/viewer"
+VITE_SHORT_URL="https://migration.geoportail.lu/short/create"
+VITE_QR_URL="https://migration.geoportail.lu/qr"
+VITE_ADDRESS_URL="https://migration.geoportail.lu/geocode/reverse"
+
# Paths for symbols
VITE_SYMBOL_ICONS_URL="https://map.geoportail.lu/mymaps" # !!! use prod because of CORS
VITE_SYMBOLS_URL="https://map.geoportail.lu/mymaps/symbols" # !!! use prod because of CORS
diff --git a/.env.staging b/.env.staging
index 071f1b9b..b1cb4531 100644
--- a/.env.staging
+++ b/.env.staging
@@ -1,3 +1,5 @@
+VITE_V3_API_HOST="https://migration.geoportail.lu/"
+
# Proxy urls
VITE_USE_PROXYURL=true
VITE_PROXYURL_WMS="https://migration.geoportail.lu/ogcproxywms"
@@ -9,6 +11,15 @@ VITE_GET_LEGENDS_URL="https://migration.geoportail.lu/legends/get_html"
VITE_GET_METADATA_URL="https://migration.geoportail.lu/getMetadata"
VITE_GET_INFO_SERVICE_URL="https://migration.geoportail.lu/getfeatureinfo"
+# Urls for location info
+VITE_FORAGE_URL="/getRapportForageVirtuel"
+VITE_LIDAR_URL="https://lidar.geoportail.lu"
+VITE_CYCLOMEDIA_URL="http://streetsmart.cyclomedia.com/streetsmart"
+VITE_OBLIQUE_URL="https://oblique.geoportail.lu/publication/viewer"
+VITE_SHORT_URL="https://migration.geoportail.lu/short/create"
+VITE_QR_URL="https://migration.geoportail.lu/qr"
+VITE_ADDRESS_URL="https://migration.geoportail.lu/geocode/reverse"
+
# Paths for symbols
VITE_SYMBOL_ICONS_URL="https://migration.geoportail.lu/mymaps"
VITE_SYMBOLS_URL="https://migration.geoportail.lu/mymaps/symbols"
diff --git a/cypress/e2e/draw/draw-feat-line.cy.ts b/cypress/e2e/draw/draw-feat-line.cy.ts
index a5cbb7b3..1a09bca3 100644
--- a/cypress/e2e/draw/draw-feat-line.cy.ts
+++ b/cypress/e2e/draw/draw-feat-line.cy.ts
@@ -11,6 +11,29 @@ function testFeatItemMeasurements() {
describe('Draw "Line"', () => {
beforeEach(() => {
+ cy.intercept(
+ {
+ method: 'POST',
+ pathname: '/profile.json',
+ },
+ req =>
+ new Promise(r => {
+ const resp = new Response(req.body, {
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
+ })
+ resp.formData().then(formData => {
+ const geom = formData.get('geom')
+ const coordinates = JSON.parse(geom as string).coordinates
+ if (coordinates[0][0] > 40000) {
+ r(req.body)
+ } else {
+ r({
+ fixture: 'profile.json',
+ })
+ }
+ })
+ })
+ ).as('profile-fixture')
cy.visit('/')
cy.get('button[data-cy="drawButton"]').click()
cy.get('button[data-cy="drawLineButton"]').click()
@@ -26,10 +49,12 @@ describe('Draw "Line"', () => {
})
it('displays measurements for Line', () => {
+ cy.wait('@profile-fixture')
testFeatItemMeasurements()
})
it('displays the elevation profile for Line', () => {
+ cy.wait('@profile-fixture')
cy.get('[data-cy="featItemProfileCumul"]').should(
'contain.text',
'Δ+964 m Δ-1105 m Δ-141 m'
@@ -50,11 +75,9 @@ describe('Draw "Line"', () => {
})
describe('When editing the line', () => {
- beforeEach(() => {
- cy.intercept('POST', '/profile.json', { fixture: 'profile.json' })
- })
it('refreshes the elevation profile for Line', () => {
cy.dragVertexOnMap(320, 223, 305, 305)
+ cy.wait('@profile-fixture')
cy.get('[data-cy="featItemProfileCumul"]').should($el => {
const text = $el.text()
const validValues = [
@@ -72,6 +95,7 @@ describe('Draw "Line"', () => {
})
it('downloads the profile elevation', () => {
+ cy.wait('@profile-fixture')
cy.get('[data-cy="featItemProfileCSV"]').click()
const downloadPath = 'cypress/downloads/Ligne_1.csv'
@@ -86,12 +110,15 @@ describe('Draw "Line"', () => {
})
it('updates length measurement when editing geometry', () => {
+ cy.wait('@profile-fixture')
cy.get('[data-cy="featItemLength"]').should('contain.text', '42.31 km')
cy.dragVertexOnMap(320, 223, 305, 305)
+ cy.wait('@profile-fixture')
cy.get('[data-cy="featItemLength"]').should('contain.text', '33.26 km')
})
it('displays the possible actions for the feature', () => {
+ cy.wait('@profile-fixture')
testFeatItem()
})
})
@@ -99,12 +126,14 @@ describe('Draw "Line"', () => {
describe('When clicking button dock', () => {
it('displays the feature info in the map popup', () => {
testFeatItemDocking()
+ cy.wait('@profile-fixture')
testFeatItemMeasurements()
})
})
describe('When clicking button dropdown menu', () => {
it('displays the dropdown menu content for "Line"', () => {
+ cy.wait('@profile-fixture')
cy.get('[data-cy="featMenuPopup"] > button').should('exist')
cy.get('[data-cy="featMenuPopup"] > button').click()
diff --git a/cypress/e2e/footer-bar.cy.ts b/cypress/e2e/footer-bar.cy.ts
index 1496a958..70899ab0 100644
--- a/cypress/e2e/footer-bar.cy.ts
+++ b/cypress/e2e/footer-bar.cy.ts
@@ -31,7 +31,7 @@ describe('Footer bar', () => {
cy.get('[data-cy="infoOpenClose"]').find('button').click()
cy.get('[data-cy="infoPanel"]').should('exist')
cy.get('button[data-cy="drawButton"]').click()
- cy.get('[data-cy="infoPanel"]').should('not.exist')
+ cy.get('[data-cy="infoPanel"]').should('be.hidden')
})
})
@@ -72,7 +72,7 @@ describe('Footer bar', () => {
it('Other panels are closed', () => {
cy.get('[data-cy="styleSelector"]').should('not.exist')
cy.get('[data-cy="layerPanel"]').should('not.exist')
- cy.get('[data-cy="infoPanel"]').should('not.exist')
+ cy.get('[data-cy="infoPanel"]').should('be.hidden')
})
describe('When clicking on layers panel button', () => {
@@ -94,7 +94,7 @@ describe('Footer bar', () => {
})
it('Info panel is shown', () => {
- cy.get('[data-cy="infoPanel"]').should('exist')
+ cy.get('[data-cy="infoPanel"]').should('be.visible')
})
it('Other panels are closed', () => {
@@ -108,7 +108,7 @@ describe('Footer bar', () => {
})
it('closes the info panel', () => {
- cy.get('[data-cy="infoPanel"]').should('not.exist')
+ cy.get('[data-cy="infoPanel"]').should('be.hidden')
})
})
})
diff --git a/cypress/e2e/info/location-info.cy.ts b/cypress/e2e/info/location-info.cy.ts
new file mode 100644
index 00000000..a4c07125
--- /dev/null
+++ b/cypress/e2e/info/location-info.cy.ts
@@ -0,0 +1,163 @@
+describe('Location Info', () => {
+ beforeEach(() => {
+ cy.intercept('POST', 'short/create', {
+ statusCode: 200,
+ body: { short_url: 'http://localhost:8080/s/uSxF' },
+ }).as('shortUrl')
+
+ cy.intercept(
+ {
+ method: 'GET',
+ pathname: '/geocode/reverse',
+ },
+ req => {
+ const dist = Math.sqrt(
+ (parseFloat(req.query.easting.toString()) - 67887) ** 2 +
+ (parseFloat(req.query.northing.toString()) - 85410) ** 2
+ )
+ const categorizedDist =
+ dist > 5 ? (dist > 100 ? 1972.1284 : 394.2305) : 20.98493
+ req.reply({
+ statusCode: 200,
+ body: {
+ count: 1,
+ results: [
+ {
+ id_caclr_locality: '37',
+ id_caclr_street: '1147',
+ id_caclr_bat: '213956',
+ street: 'Bergstr',
+ number: '18',
+ locality: 'Roodt/Eisch/Test',
+ commune: 'Habscht',
+ postal_code: '8398',
+ country: 'Luxembourg',
+ country_code: 'lu',
+ distance: categorizedDist,
+ contributor: 'ACT',
+ geom: {
+ type: 'Point',
+ coordinates: [req.query.easting, req.query.northing],
+ },
+ geomlonlat: {
+ type: 'Point',
+ coordinates: [6.00041535, 49.697110053],
+ },
+ },
+ ],
+ },
+ })
+ }
+ )
+ })
+
+ describe('Open location info on position', () => {
+ describe('Display basic feature info for multiple layers', () => {
+ beforeEach(() => {
+ cy.visit('/?zoom=8')
+ })
+ it('should display coordinate and address information in the panel', () => {
+ cy.get('[data-cy="locationInfo"]').should('be.hidden')
+ cy.url().should('not.contain', 'crosshair=')
+ cy.window()
+ .its('olMap')
+ .then(function (olMap) {
+ const featureLayers = olMap
+ .getLayers()
+ .getArray()
+ .filter((l: any) => l.get('cyLayerType') === 'infoFeatureLayer')
+ const features = featureLayers
+ .map((l: any) => l.getSource().getFeatures())
+ .flat()
+ cy.wrap(features.length).should('equal', 0)
+ })
+
+ cy.get('div.ol-viewport').rightclick(350, 300, { force: true })
+ cy.get('[data-cy="locationInfo"]').should('be.visible')
+ cy.get('[data-cy="locationInfo"]').find('input').should('exist')
+ cy.get('[data-cy="locationInfo"]')
+ .find('input')
+ .invoke('val')
+ .should('contain', 'localhost:8080/s')
+ // 8 location infos (5 projections, elevation, address, distance)
+ cy.get('[data-cy="locationInfo"] > div > table > tbody > tr').should(
+ 'have.length',
+ 8
+ )
+ cy.get('[data-cy="locationInfo"] > div > table > tbody > tr')
+ .eq(6)
+ .find('td')
+ .should('contain.text', 'Roodt/Eisch/Test')
+ cy.get('[data-cy="locationInfo"] > div > table > tbody > tr')
+ .eq(7)
+ .find('td')
+ .should('contain.text', '1.97 km')
+ // check pointer
+ cy.window()
+ .its('olMap')
+ .then(function (olMap) {
+ const featureLayers = olMap
+ .getLayers()
+ .getArray()
+ .filter((l: any) => l.get('cyLayerType') === 'infoFeatureLayer')
+ const features = featureLayers
+ .map((l: any) => l.getSource().getFeatures())
+ .flat()
+ cy.wrap(features.length).should('equal', 1)
+ })
+
+ cy.url().should('contain', 'crosshair=true')
+ })
+
+ it('streetview should integrate smoothly in the panel', () => {
+ cy.get('div.ol-viewport').rightclick(350, 300, { force: true })
+ cy.get('[data-cy="streetviewOff"]').should('not.exist')
+ cy.get('[data-cy="streetviewOn"]').click()
+ cy.get('[data-cy="streetviewOff"]').should('exist')
+ cy.get('[data-cy="streetviewNoData"]').should('be.visible')
+ cy.get('[data-cy="streetviewNoData"]')
+ .find('span')
+ .should(
+ 'contain.text',
+ "Il n'y a pas de panorama Google disponible à cet endroit"
+ )
+ cy.window()
+ .its('olMap')
+ .then(function (olMap) {
+ const featureLayers = olMap
+ .getLayers()
+ .getArray()
+ .filter((l: any) => l.get('cyLayerType') === 'svFeatureLayer')
+ const features = featureLayers
+ .map((l: any) => l.getSource().getFeatures())
+ .flat()
+ cy.wrap(features.length).should('equal', 0)
+ })
+ cy.get('div.ol-viewport').rightclick(350, 50, { force: true })
+ cy.get('[data-cy="streetviewNoData"]').should('not.be.visible')
+ cy.get('[data-cy="streetviewLoading"]').should('not.be.visible')
+ cy.window()
+ .its('olMap')
+ .should(function (olMap) {
+ const featureLayers = olMap
+ .getLayers()
+ .getArray()
+ .filter((l: any) => l.get('cyLayerType') === 'svFeatureLayer')
+ const features = featureLayers
+ .map((l: any) => l.getSource().getFeatures())
+ .flat()
+ expect(features.length).to.equal(3)
+ })
+ cy.get('[data-cy="locationInfo"] > div > table > tbody > tr')
+ .last()
+ .find('td')
+ .should('contain.text', '20.98 m')
+ cy.get('div.ol-viewport').click(382, 82, { force: true })
+ cy.get('[data-cy="locationInfo"] > div > table > tbody > tr')
+ .last()
+ .find('td')
+ .should(el => expect(el).to.contain.text('394.23 m'))
+ })
+ })
+ })
+})
diff --git a/cypress/e2e/legends/legends.cy.ts b/cypress/e2e/legends/legends.cy.ts
index cf6452b4..0b124354 100644
--- a/cypress/e2e/legends/legends.cy.ts
+++ b/cypress/e2e/legends/legends.cy.ts
@@ -4,12 +4,17 @@ describe('Legends', () => {
'GET',
'/legends/get_html?lang=fr&name=pcn_parcelles%3Ashow&id=359',
{ fixture: 'legends_parcelles.html' }
- )
+ ).as('parcel-fixture')
cy.intercept(
'GET',
'/legends/get_html?lang=fr&name=energie%3Apotentiel_solaire&id=1813',
{ fixture: 'legends_potentiel_solaire.html' }
- )
+ ).as('solaire-fixture')
+ cy.intercept(
+ 'GET',
+ '/legends/get_html?lang=fr&name=act%3Aroadmap_vt&id=556',
+ { fixture: 'legends_bg_roadmap.html' }
+ ).as('bg-roadmap-fixture')
cy.visit('/')
})
@@ -24,6 +29,7 @@ describe('Legends', () => {
'contain.text',
"Aucune légende n'est disponible pour les couches sélectionnées."
)
+ cy.wait('@bg-roadmap-fixture')
cy.get('[data-cy="legendBgLayer"]').should('exist')
})
@@ -63,6 +69,8 @@ describe('Legends', () => {
})
it('displays the legends for both layers', () => {
+ cy.wait('@solaire-fixture')
+ cy.wait('@parcel-fixture')
cy.get('[data-cy="legendLayer"]').should('have.length', 2)
})
})
@@ -84,7 +92,9 @@ describe('Legends', () => {
})
it('displays the legends for both layers having legend', () => {
- cy.get('[data-cy="legendLayer"]').should('have.length', 2)
+ cy.wait('@parcel-fixture')
+ cy.wait('@solaire-fixture')
+ cy.get('[data-cy="legendLayer"]').its('length').should('be.equal', 2)
})
})
diff --git a/cypress/fixtures/legends_bg_roadmap.html b/cypress/fixtures/legends_bg_roadmap.html
new file mode 100644
index 00000000..f79267ba
--- /dev/null
+++ b/cypress/fixtures/legends_bg_roadmap.html
@@ -0,0 +1,16 @@
+
+
+
+
+
diff --git a/decs.d.ts b/decs.d.ts
index 01d54d1b..c6fd9092 100644
--- a/decs.d.ts
+++ b/decs.d.ts
@@ -1,3 +1,4 @@
declare module '@camptocamp/ogc-client'
declare module '@geoblocks/ol-maplibre-layer'
+declare module 'google.maps'
declare module 'file-saver'
diff --git a/package-lock.json b/package-lock.json
index 66364edf..caaaf236 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -42,6 +42,7 @@
"@types/d3-selection": "^3.0.10",
"@types/d3-shape": "^3.1.6",
"@types/d3-transition": "^3.0.8",
+ "@types/google.maps": "^3.58.1",
"@types/jsdom": "^20.0.1",
"@types/luxon": "^3.3.1",
"@types/node": "^18.11.9",
@@ -3295,6 +3296,12 @@
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true
},
+ "node_modules/@types/google.maps": {
+ "version": "3.58.1",
+ "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.58.1.tgz",
+ "integrity": "sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==",
+ "dev": true
+ },
"node_modules/@types/jsdom": {
"version": "20.0.1",
"resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz",
diff --git a/package.json b/package.json
index 6e5d8b90..d4b683c5 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"@types/d3-selection": "^3.0.10",
"@types/d3-shape": "^3.1.6",
"@types/d3-transition": "^3.0.8",
+ "@types/google.maps": "^3.58.1",
"@types/jsdom": "^20.0.1",
"@types/luxon": "^3.3.1",
"@types/node": "^18.11.9",
diff --git a/src/assets/images/streetview/arrow.png b/src/assets/images/streetview/arrow.png
new file mode 100644
index 00000000..a9b96053
Binary files /dev/null and b/src/assets/images/streetview/arrow.png differ
diff --git a/src/assets/images/streetview/direction_sv.png b/src/assets/images/streetview/direction_sv.png
new file mode 100644
index 00000000..a7158107
Binary files /dev/null and b/src/assets/images/streetview/direction_sv.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl1.png b/src/assets/images/streetview/direction_sv_zl1.png
new file mode 100644
index 00000000..00ad60f7
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl1.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl1_p0.png b/src/assets/images/streetview/direction_sv_zl1_p0.png
new file mode 100644
index 00000000..7fd5c85c
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl1_p0.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl1_p1.png b/src/assets/images/streetview/direction_sv_zl1_p1.png
new file mode 100644
index 00000000..188697e0
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl1_p1.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl1_p2.png b/src/assets/images/streetview/direction_sv_zl1_p2.png
new file mode 100644
index 00000000..bd6f835a
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl1_p2.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl1_p3.png b/src/assets/images/streetview/direction_sv_zl1_p3.png
new file mode 100644
index 00000000..4b568a41
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl1_p3.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl2.png b/src/assets/images/streetview/direction_sv_zl2.png
new file mode 100644
index 00000000..7aba1c28
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl2.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl2_p0.png b/src/assets/images/streetview/direction_sv_zl2_p0.png
new file mode 100644
index 00000000..dbe0acbe
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl2_p0.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl2_p1.png b/src/assets/images/streetview/direction_sv_zl2_p1.png
new file mode 100644
index 00000000..edc011eb
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl2_p1.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl2_p2.png b/src/assets/images/streetview/direction_sv_zl2_p2.png
new file mode 100644
index 00000000..345b1901
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl2_p2.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl2_p3.png b/src/assets/images/streetview/direction_sv_zl2_p3.png
new file mode 100644
index 00000000..9ab119e4
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl2_p3.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl3.png b/src/assets/images/streetview/direction_sv_zl3.png
new file mode 100644
index 00000000..7d4342ee
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl3.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl3_p0.png b/src/assets/images/streetview/direction_sv_zl3_p0.png
new file mode 100644
index 00000000..864fd284
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl3_p0.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl3_p1.png b/src/assets/images/streetview/direction_sv_zl3_p1.png
new file mode 100644
index 00000000..d58d8144
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl3_p1.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl3_p2.png b/src/assets/images/streetview/direction_sv_zl3_p2.png
new file mode 100644
index 00000000..2a87ba18
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl3_p2.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl3_p3.png b/src/assets/images/streetview/direction_sv_zl3_p3.png
new file mode 100644
index 00000000..fb515835
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl3_p3.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl4.png b/src/assets/images/streetview/direction_sv_zl4.png
new file mode 100644
index 00000000..7ee9c73c
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl4.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl4_p0.png b/src/assets/images/streetview/direction_sv_zl4_p0.png
new file mode 100644
index 00000000..f97d41e7
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl4_p0.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl4_p1.png b/src/assets/images/streetview/direction_sv_zl4_p1.png
new file mode 100644
index 00000000..cac7442b
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl4_p1.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl4_p2.png b/src/assets/images/streetview/direction_sv_zl4_p2.png
new file mode 100644
index 00000000..bd2c8eb1
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl4_p2.png differ
diff --git a/src/assets/images/streetview/direction_sv_zl4_p3.png b/src/assets/images/streetview/direction_sv_zl4_p3.png
new file mode 100644
index 00000000..c0e5c340
Binary files /dev/null and b/src/assets/images/streetview/direction_sv_zl4_p3.png differ
diff --git a/src/assets/images/streetview/streetview_placeholder.png b/src/assets/images/streetview/streetview_placeholder.png
new file mode 100644
index 00000000..0006b54a
Binary files /dev/null and b/src/assets/images/streetview/streetview_placeholder.png differ
diff --git a/src/assets/main.css b/src/assets/main.css
index 40709ca5..1c7d3ffb 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -108,10 +108,17 @@
content: '\E902';
}
- .lux-btn {
+ .lux-btn,
+ a.lux-btn {
@apply cursor-pointer bg-white border text-primary py-[6px] px-[12px] hover:bg-primary hover:text-white leading-snug focus:bg-[#e6e6e6] focus:border-[#8c8c8c] focus:[color:var(--color-primary)] focus:lux-outlined;
border: 1px solid var(--color-gray);
}
+ .lux-btn:disabled {
+ @apply cursor-not-allowed opacity-65 shadow-none;
+ filter: alpha(opacity=65);
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ }
.lux-btn-primary {
@apply lux-btn bg-primary text-white hover:bg-quaternary border-[1px] border-[color:var(--color-quaternary)];
@@ -234,6 +241,22 @@
@apply bg-transparent border-none gap-[1px] shadow-none m-0 py-0 float-right relative left-auto;
}
+ .lux-loader {
+ @apply border-[20px] border-solid border-white rounded-full;
+ @apply border-t-[20px] border-t-[color:var(--color-primary)];
+ @apply z-10 m-auto w-[200px] h-[200px];
+ animation: lux-loader 4s linear infinite;
+ }
+
+ @keyframes lux-loader {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
.lux-slider-line {
@apply absolute h-full w-[4px] left-[50%] bg-primary ml-[-2px] cursor-ew-resize block top-0;
}
@@ -397,6 +420,11 @@
@apply w-full text-black bg-white border border-black p-1;
}
+ .lux-info-table th,
+ .lux-info-table td {
+ @apply text-white text-left text-sm p-0;
+ }
+
.lux-tab {
@apply text-secondary text-2xl pt-2 pb-1 px-3 mr-2 hover:text-white hover:bg-primary cursor-pointer text-center uppercase bg-tertiary;
}
diff --git a/src/bundle/lib.ts b/src/bundle/lib.ts
index 0c91a8bc..63a5fd31 100644
--- a/src/bundle/lib.ts
+++ b/src/bundle/lib.ts
@@ -32,6 +32,7 @@ import FooterBar from '@/components/footer/footer-bar.vue'
import ToolbarDraw from '@/components/footer/toolbar-draw.vue'
import LayerPanel from '@/components/layer-panel/layer-panel.vue'
import LegendsPanel from '@/components/legends/legends-panel.vue'
+import LocationInfoPanel from '@/components/info/location-info.vue'
import SliderComparator from '@/components/slider/slider-comparator.vue'
import useBackgroundLayer from '@/composables/background-layer/background-layer.composable'
import useLayers from '@/composables/layers/layers.composable'
@@ -48,6 +49,7 @@ import { useProfileMeasuresv3Store } from './stores/profile-measures_v3.store'
import { useProfileRoutingv3Store } from './stores/profile-routing_v3.store'
import { useProfileInfosv3Store } from './stores/profile-infos_v3.store'
import { useDrawStore } from '@/stores/draw.store'
+import { useLocationInfoStore } from '@/stores/location-info.store'
import { useStyleStore } from '@/stores/style.store'
import { useThemeStore } from '@/stores/config.store'
import { useUserManagerStore } from '@/stores/user-manager.store'
@@ -152,6 +154,7 @@ export {
ToolbarDraw,
LayerPanel,
LegendsPanel,
+ LocationInfoPanel,
SliderComparator,
proxyUrlHelper,
styleUrlHelper,
@@ -170,6 +173,7 @@ export {
useProfileRoutingv3Store,
useProfileInfosv3Store,
useDrawStore,
+ useLocationInfoStore,
useStyleStore,
useThemeStore,
useUserManagerStore,
diff --git a/src/components/info/info-panel.vue b/src/components/info/info-panel.vue
index 72c66a1b..aeafb532 100644
--- a/src/components/info/info-panel.vue
+++ b/src/components/info/info-panel.vue
@@ -2,20 +2,28 @@
import { useTranslation } from 'i18next-vue'
import SidePanelLayout from '@/components/common/side-panel-layout.vue'
import { useAppStore } from '@/stores/app.store'
+import { useLocationInfoStore } from '@/stores/location-info.store'
import { storeToRefs } from 'pinia'
import { useFeatureInfoStore } from '@/stores/feature-info.store'
+import useMap from '@/composables/map/map.composable'
+import LocationInfo from './location-info.vue'
import FeatureInfo from '@/components/info/feature-info.vue'
-import { onUnmounted } from 'vue'
+import { watch } from 'vue'
const { t } = useTranslation()
const appStore = useAppStore()
+const { locationInfo } = storeToRefs(useLocationInfoStore())
+const map = useMap().olMap
+const { infoOpen } = storeToRefs(appStore)
const { clearContent } = useFeatureInfoStore()
const { featureInfoPanelContent, isLoading } = storeToRefs(
useFeatureInfoStore()
)
-onUnmounted(() => {
- clearContent()
+watch(infoOpen, isOpen => {
+ if (!isOpen) {
+ clearContent()
+ }
})
@@ -31,16 +39,26 @@ onUnmounted(() => {
-
-
- -
- {{ t(`A right click (tap and hold on mobile)...`, { ns: 'app' }) }}
-
- -
- {{ t(`A short click (tap on mobile)...`, { ns: 'app' }) }}
-
-
-
+
+
+
+
+
+
+
+
+
+ -
+ {{
+ t(`A right click (tap and hold on mobile)...`, { ns: 'app' })
+ }}
+
+ -
+ {{ t(`A short click (tap on mobile)...`, { ns: 'app' }) }}
+
+
+
+
+import { computed, Ref, ref, watch } from 'vue'
+import { useTranslation } from 'i18next-vue'
+import { storeToRefs } from 'pinia'
+import { useLocationInfoStore } from '@/stores/location-info.store'
+import { useUserManagerStore } from '@/stores/user-manager.store'
+import { AddressResult } from '@/stores/location-info.store.model'
+import { Coordinate } from 'ol/coordinate'
+import { Feature } from 'ol'
+import { Point } from 'ol/geom'
+import useMap from '@/composables/map/map.composable'
+import useLocationInfo from '@/composables/info/location-info.composable'
+import {
+ getQRUrl,
+ queryInfos,
+ INFO_PROJECTIONS,
+} from '@/services/info/location-info'
+import {
+ formatElevation,
+ formatLength,
+ formatAddress,
+ formatCoords,
+} from '@/services/common/formatting.utils'
+import { downloadUrl } from '@/services/utils'
+
+import StreetView from '@/components/info/street-view.vue'
+
+const { t } = useTranslation()
+const { locationInfo, isStreetviewActive, routingFeatureTemp } = storeToRefs(
+ useLocationInfoStore()
+)
+const { currentUser } = storeToRefs(useUserManagerStore())
+
+const map = useMap().getOlMap()
+
+const shortUrl: Ref = ref()
+const qrUrl: Ref = ref()
+const elevation: Ref = ref()
+const address: Ref = ref()
+const clickCoordinateLuref: Ref = ref()
+const formattedCoordinates: Ref<{ [k: string]: string }> = ref({})
+const downloadingRepport: Ref = ref(false)
+const isInBoxOfLidar: Ref = ref(false)
+const userRole: Ref = ref('ACT') //Tous Publics')
+const userType: Ref = ref('base')
+
+// initialise map listeners for location info
+useLocationInfo()
+// initial load of position infos if locationInfo is not initially undefined
+// async function, no need to await the completion, DOM will be updated via refs
+updateInfo(locationInfo.value)
+
+watch(locationInfo, updateInfo)
+
+async function updateInfo(location: Coordinate | undefined) {
+ if (location) {
+ const infos = await queryInfos(location, map.getView().getProjection())
+ shortUrl.value = infos.shortUrl
+ qrUrl.value = getQRUrl(infos.shortUrl)
+ clickCoordinateLuref.value = infos.clickCoordinateLuref
+ formattedCoordinates.value = Object.fromEntries(
+ Object.entries(INFO_PROJECTIONS).map(([crs, label]) => [
+ label,
+ formatCoords(location, map.getView().getProjection(), crs),
+ ])
+ )
+ elevation.value =
+ infos.elevation === null ? 'N/A' : formatElevation(infos.elevation)
+ address.value = infos.address
+ isInBoxOfLidar.value = infos.isInBoxOfLidar
+ }
+}
+
+watch(currentUser, user => {
+ userRole.value = user?.role || 'anonymous'
+ userType.value = user?.role || 'base'
+})
+
+const isRapportForageVirtuelAvailable = computed(() => userRole.value === 'ACT')
+const isCyclomediaAvailable = computed(
+ () =>
+ userType.value === 'etat' ||
+ userType.value === 'commune' ||
+ userRole.value === 'MinTour'
+)
+const isImagesObliquesAvailable = computed(() => true)
+
+const lidarUrl = computed(() =>
+ clickCoordinateLuref.value
+ ? `${import.meta.env.VITE_LIDAR_URL}?COORD_X=${
+ clickCoordinateLuref.value[0]
+ }&COORD_Y=${clickCoordinateLuref.value[1]}&COORD_Z=${parseInt(
+ elevation.value || '0'
+ )}`
+ : ''
+)
+const forageUrl = computed(() =>
+ clickCoordinateLuref.value
+ ? `${import.meta.env.VITE_FORAGE_URL}?x=${
+ clickCoordinateLuref.value[0]
+ }&y=${clickCoordinateLuref.value[1]}`
+ : ''
+)
+const cyclomediaUrl = computed(() =>
+ clickCoordinateLuref.value
+ ? `${import.meta.env.VITE_CYCLOMEDIA_URL}?q=${
+ clickCoordinateLuref.value[0]
+ };${clickCoordinateLuref.value[1]}`
+ : ''
+)
+const imagesObliquesUrl = computed(() =>
+ clickCoordinateLuref.value
+ ? `${import.meta.env.VITE_OBLIQUE_URL}?x=${
+ clickCoordinateLuref.value[0]
+ }&y=${clickCoordinateLuref.value[1]}&crs=2169`
+ : ''
+)
+
+function addRoutePoint() {
+ if (!locationInfo.value) {
+ return
+ }
+ const point = new Feature(new Point(locationInfo.value))
+ if (address.value && address.value.distance <= 100) {
+ point.set('label', formatAddress(address.value))
+ } else {
+ point.set('label', formattedCoordinates.value['Luref'])
+ }
+ routingFeatureTemp.value = point
+}
+
+function toggleStreetview() {
+ isStreetviewActive.value = !isStreetviewActive.value
+}
+
+async function downloadRapportForageVirtuel() {
+ downloadingRepport.value = true
+ try {
+ await downloadUrl(forageUrl.value, '')
+ } catch {
+ // TODO harmonize error
+ alert('Error downloading forage')
+ } finally {
+ downloadingRepport.value = false
+ }
+}
+
+watch(downloadingRepport, downloadingRepport => {
+ if (downloadingRepport) {
+ map.getViewport().style.cursor = 'wait'
+ } else {
+ map.getViewport().style.cursor = ''
+ }
+})
+
+
+
+
+
+
+ {{ t('Short Url', { ns: 'client' }) }}
+
+
+
+
![]()
+
+
+
+ {{ t('Location Coordinates', { ns: 'client' }) }}
+
+
+
+
+ {{ t(label as string) }} |
+ {{ coords }} |
+
+
+ {{ t('Elevation') }} |
+ {{ elevation }} |
+
+
+ {{ t('Adresse la plus proche') }} |
+ {{ formatAddress(address) }} |
+
+
+ {{ t('Distance approximative') }} |
+ {{ formatLength(address?.distance || null) }} |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/info/street-view.vue b/src/components/info/street-view.vue
new file mode 100644
index 00000000..a09317d3
--- /dev/null
+++ b/src/components/info/street-view.vue
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+ {{ t("Il n'y a pas de panorama google disponible à cet endroit") }}
+
+
+
+
+
+
+
+
diff --git a/src/components/map/map-container.vue b/src/components/map/map-container.vue
index 780165a1..80c94233 100644
--- a/src/components/map/map-container.vue
+++ b/src/components/map/map-container.vue
@@ -8,6 +8,7 @@ import { OlSynchronizer } from '@/composables/map/ol.synchronizer'
import { OlViewSynchronizer } from '@/composables/map/ol-view.synchronizer'
import { statePersistorMapService } from '@/services/state-persistor/state-persistor-map.service'
import { statePersistorFeaturesService } from '@/services/state-persistor/state-persistor-features.service'
+import { statePersistorLocationInfo } from '@/services/state-persistor/state-persistor-location-info'
import AttributionControl from '../map-controls/attribution-control.vue'
import LocationControl from '../map-controls/location-control.vue'
import Map3dControl from '../map-controls/map-3d.vue'
@@ -54,6 +55,7 @@ onMounted(() => {
new OlViewSynchronizer(olMap)
statePersistorMapService.bootstrap()
statePersistorFeaturesService.bootstrap()
+ statePersistorLocationInfo.bootstrap()
olMap.setTarget(mapContainer.value)
// Direct access to olMap for cypress
diff --git a/src/components/side-panel/side-panel.vue b/src/components/side-panel/side-panel.vue
index 54a56673..14cd2b0d 100644
--- a/src/components/side-panel/side-panel.vue
+++ b/src/components/side-panel/side-panel.vue
@@ -93,7 +93,7 @@ watch(infoOpen, infoOpen => {
-
+
diff --git a/src/composables/info/feature-info.composable.ts b/src/composables/info/feature-info.composable.ts
index bbae3994..f2aaae1a 100644
--- a/src/composables/info/feature-info.composable.ts
+++ b/src/composables/info/feature-info.composable.ts
@@ -22,6 +22,7 @@ import { DrawnFeature } from '@/services/draw/drawn-feature'
import { Pixel } from 'ol/pixel'
import { throttle } from '@/services/utils'
import { useMapStore } from '@/stores/map.store'
+import { useLocationInfoStore } from '@/stores/location-info.store'
import { getFeatureInfoJson } from '@/services/api/api-feature-info.service'
export default function useFeatureInfo() {
@@ -35,6 +36,9 @@ export default function useFeatureInfo() {
const { drawStateActive, editStateActive, drawnFeatures } = storeToRefs(
useDrawStore()
)
+ const { locationInfo, isStreetviewActive } = storeToRefs(
+ useLocationInfoStore()
+ )
const { maxZoom } = storeToRefs(useMapStore())
const responses = ref
([])
@@ -78,6 +82,8 @@ export default function useFeatureInfo() {
// routingOpen ||
// lidarOpen ||
// appActivetool.value.isActive() || => corresponds to: measureActive, streetviewActive
+ evt.originalEvent.button === 2 || // right click
+ (isStreetviewActive.value && locationInfo.value) ||
drawStateActive.value ||
editStateActive.value ||
isLoading.value
diff --git a/src/composables/info/location-info.composable.ts b/src/composables/info/location-info.composable.ts
new file mode 100644
index 00000000..687182f6
--- /dev/null
+++ b/src/composables/info/location-info.composable.ts
@@ -0,0 +1,134 @@
+import { watch } from 'vue'
+import { storeToRefs } from 'pinia'
+
+import useMap from '@/composables/map/map.composable'
+import { listen } from 'ol/events'
+import { Coordinate } from 'ol/coordinate'
+import { MapBrowserEvent } from 'ol'
+import { Feature } from 'ol'
+import { Point } from 'ol/geom'
+import VectorLayer from 'ol/layer/Vector'
+import VectorSource from 'ol/source/Vector'
+import StyleStyle from 'ol/style/Style'
+import StyleCircle from 'ol/style/Circle'
+import StyleFill from 'ol/style/Fill'
+import StyleStroke from 'ol/style/Stroke'
+import { useAppStore } from '@/stores/app.store'
+import { useLocationInfoStore } from '@/stores/location-info.store'
+import { useFeatureInfoStore } from '@/stores/feature-info.store'
+
+export const DEFAULT_INFO_ZINDEX = 1501
+export const INFO_FEATURE_LAYER_TYPE = 'infoFeatureLayer'
+
+export default function useLocationInfo() {
+ const map = useMap().getOlMap()
+ let holdTimeoutId: number | undefined = undefined
+ let startPixel: Coordinate | null = null
+ const { infoOpen } = storeToRefs(useAppStore())
+ const { locationInfo, hidePointer } = storeToRefs(useLocationInfoStore())
+ const { clearContent } = useFeatureInfoStore()
+
+ const infoFeatureLayer = new VectorLayer({
+ source: new VectorSource({
+ features: [] as Feature[],
+ }),
+ zIndex: DEFAULT_INFO_ZINDEX,
+ })
+ infoFeatureLayer.set('cyLayerType', INFO_FEATURE_LAYER_TYPE)
+ setInfoStyle(infoFeatureLayer)
+ map.addLayer(infoFeatureLayer)
+
+ watch(infoOpen, open => {
+ if (!open) {
+ locationInfo.value = undefined
+ }
+ })
+
+ watch(
+ [locationInfo, hidePointer],
+ ([location, doHide]) => {
+ infoFeatureLayer.getSource()?.clear()
+ if (location && !doHide) {
+ infoOpen.value = true
+ const feature = new Feature(new Point(location))
+ infoFeatureLayer.getSource()?.addFeature(feature)
+ }
+ },
+ { immediate: true }
+ )
+
+ map
+ .getViewport()
+ .addEventListener('contextmenu', event => event.preventDefault())
+
+ listen(map, 'pointerdown', event =>
+ handleClick(event as MapBrowserEvent)
+ )
+
+ function getClickCoordinate(event: MapBrowserEvent) {
+ return map.getEventCoordinate(event.originalEvent)
+ }
+
+ function handleClick(event: MapBrowserEvent) {
+ startPixel = event.pixel
+ if (event.originalEvent.button === 2) {
+ // if right mouse click
+ locationInfo.value = getClickCoordinate(event)
+ clearContent()
+ } else if (event.originalEvent.pointerType === 'touch') {
+ window.clearTimeout(holdTimeoutId)
+ holdTimeoutId = window.setTimeout(() => {
+ locationInfo.value = getClickCoordinate(event)
+ clearContent()
+ })
+ }
+ }
+
+ listen(map, 'pointerup', event => {
+ if (
+ startPixel &&
+ (event as MapBrowserEvent).originalEvent.button === 0
+ ) {
+ // if left mouse click
+ if (!hidePointer.value) {
+ locationInfo.value = undefined
+ }
+ }
+ window.clearTimeout(holdTimeoutId)
+ startPixel = null
+ })
+
+ listen(map, 'pointermove', event => {
+ if (startPixel !== null) {
+ const pixel = (event as MapBrowserEvent).pixel
+ const deltaX = Math.abs(startPixel[0] - pixel[0])
+ const deltaY = Math.abs(startPixel[1] - pixel[1])
+ if (deltaX + deltaY > 6) {
+ window.clearTimeout(holdTimeoutId)
+ startPixel = null
+ }
+ }
+ })
+
+ function setInfoStyle(layer: VectorLayer) {
+ const defaultFill = new StyleFill({
+ color: [255, 255, 0, 0.6],
+ })
+ const circleStroke = new StyleStroke({
+ color: [255, 155, 55, 1],
+ width: 3,
+ })
+
+ const pointStyle = new StyleCircle({
+ radius: 10,
+ fill: defaultFill,
+ stroke: circleStroke,
+ })
+
+ layer.setStyle([
+ new StyleStyle({
+ image: pointStyle,
+ }),
+ ])
+ }
+}
diff --git a/src/composables/info/street-view.composable.ts b/src/composables/info/street-view.composable.ts
new file mode 100644
index 00000000..bfde2167
--- /dev/null
+++ b/src/composables/info/street-view.composable.ts
@@ -0,0 +1,271 @@
+import { watch, nextTick, Ref } from 'vue'
+import { storeToRefs } from 'pinia'
+import useMap from '@/composables/map/map.composable'
+import { useAppStore } from '@/stores/app.store'
+import { useLocationInfoStore } from '@/stores/location-info.store'
+import { MapBrowserEvent } from 'ol'
+import BaseEvent from 'ol/events/Event'
+import { Coordinate } from 'ol/coordinate'
+import { Select } from 'ol/interaction'
+import { FeatureLike } from 'ol/Feature'
+import { fromLonLat, toLonLat } from 'ol/proj'
+import { containsCoordinate } from 'ol/extent'
+import { loadGoogleapis } from '@/services/info/street-view'
+import { SvCompassFeature } from '@/services/info/sv-compass-feature'
+import { SvDirectionFeature } from '@/services/info/sv-direction-feature'
+
+import {} from 'google.maps'
+
+import { Feature } from 'ol'
+import { Point } from 'ol/geom'
+import VectorLayer from 'ol/layer/Vector'
+import VectorSource from 'ol/source/Vector'
+
+export const DEFAULT_INFO_ZINDEX = 1502
+export const SV_FEATURE_LAYER_TYPE = 'svFeatureLayer'
+
+const SV_RADIUS = 90
+
+export default function useStreeView(streetViewDiv: Ref) {
+ const { infoOpen } = storeToRefs(useAppStore())
+ const {
+ locationInfo,
+ hidePointer,
+ isStreetviewActive,
+ noDataAtLocation,
+ streetViewLoading,
+ panoPositionChanging,
+ svFeature,
+ } = storeToRefs(useLocationInfoStore())
+
+ const map = useMap().getOlMap()
+
+ let googleMapService: typeof google.maps | null = null
+ let streetViewService: google.maps.StreetViewService | null = null
+ let panorama: google.maps.StreetViewPanorama | null = null
+ let panoramaLinksListener: google.maps.MapsEventListener | null = null
+ let panoramaPovListener: google.maps.MapsEventListener | null = null
+ const selectFeature = new Select({
+ filter: (feature /*, layer*/) => feature instanceof SvDirectionFeature,
+ })
+ selectFeature.on('select', handleNavigate)
+
+ const svPoint = new Point([0, 0])
+ const svCompassFeature = new SvCompassFeature()
+ svCompassFeature.setGeometry(svPoint)
+
+ const svFeatureLayer = new VectorLayer({
+ source: new VectorSource({
+ features: [] as Feature[],
+ }),
+ // altitudeMode: 'clampToGround',
+ zIndex: DEFAULT_INFO_ZINDEX,
+ })
+ svFeatureLayer.set('cyLayerType', SV_FEATURE_LAYER_TYPE)
+ map.addLayer(svFeatureLayer)
+
+ function setLocation(loc: Coordinate | undefined) {
+ if (loc && panorama !== null && !panoPositionChanging.value) {
+ const lonlat = toLonLat(loc, map.getView().getProjection())
+ streetViewService!.getPanorama(
+ {
+ location: {
+ lat: lonlat[1],
+ lng: lonlat[0],
+ },
+ radius: SV_RADIUS,
+ },
+ updatePanorama
+ )
+ }
+ }
+
+ function updatePanorama(
+ data: google.maps.StreetViewPanoramaData | null,
+ status: google.maps.StreetViewStatus
+ ) {
+ if (status === google.maps.StreetViewStatus.OK) {
+ noDataAtLocation.value = false
+ nextTick(() => {
+ panorama!.setPosition(data?.location?.latLng || null)
+ panorama!.setVisible(true)
+ })
+ } else {
+ noDataAtLocation.value = true
+ streetViewLoading.value = false
+ svFeature.value = undefined
+ panorama!.setVisible(false)
+ }
+ }
+
+ watch(infoOpen, open => {
+ if (open) {
+ if (isStreetviewActive.value && locationInfo.value) {
+ setSvFeatures(locationInfo.value)
+ }
+ } else {
+ svFeature.value = undefined
+ }
+ })
+
+ watch(locationInfo, loc => {
+ if (loc) {
+ if (isStreetviewActive.value && !panoPositionChanging.value) {
+ streetViewLoading.value = true
+ setLocation(loc)
+ }
+ } else {
+ svFeature.value = undefined
+ // force deactivate pointer style if it is still active from handleHover
+ map.getViewport().style.cursor = ''
+ }
+ })
+
+ watch([isStreetviewActive, streetViewDiv], async ([act, streetViewDiv]) => {
+ if (act && streetViewDiv) {
+ streetViewLoading.value = true
+ await loadGoogleapis()
+ if (window.hasOwnProperty('google')) {
+ // @ts-ignore
+ googleMapService = window.google.maps
+ }
+ // todo PIWIK
+ if (streetViewService === null) {
+ streetViewService = new googleMapService!.StreetViewService()
+ }
+ if (panorama === null) {
+ panorama = new googleMapService!.StreetViewPanorama(streetViewDiv, {
+ pov: {
+ heading: 0,
+ pitch: 0,
+ },
+ visible: false,
+ zoom: 1,
+ })
+ }
+ if (panoramaLinksListener === null) {
+ panoramaLinksListener = googleMapService!.event.addListener(
+ panorama,
+ 'links_changed',
+ () => handlePanoramaPositionChange(true)
+ )
+ }
+ if (panoramaPovListener === null) {
+ panoramaPovListener = googleMapService!.event.addListener(
+ panorama,
+ 'pov_changed',
+ () => handlePanoramaPositionChange(false)
+ )
+ }
+ setLocation(locationInfo.value)
+ } else {
+ if (panorama !== null) {
+ panorama.setVisible(false)
+ }
+ svFeature.value = undefined
+ if (panoramaPovListener) {
+ googleMapService!.event.removeListener(panoramaPovListener)
+ panoramaPovListener = null
+ }
+ if (panoramaLinksListener) {
+ googleMapService!.event.removeListener(panoramaLinksListener)
+ panoramaLinksListener = null
+ }
+ }
+ })
+
+ watch(svFeature, svf => {
+ svFeatureLayer.getSource()?.clear()
+ if (svf) {
+ svFeatureLayer.getSource()?.addFeature(svf.compass)
+ svf.directions.forEach((f: SvDirectionFeature) =>
+ svFeatureLayer.getSource()?.addFeature(f)
+ )
+ hidePointer.value = true
+ map.addInteraction(selectFeature)
+ map.on('pointermove', handleHover)
+ svFeatureLayer.once('postrender', () => {
+ streetViewLoading.value = false
+ })
+ } else {
+ map.un('pointermove', handleHover)
+ map.removeInteraction(selectFeature)
+ hidePointer.value = false
+ }
+ })
+
+ function handleNavigate(evt: BaseEvent) {
+ if (evt.target.getFeatures().getLength() !== 0) {
+ const nextDirection = evt.target
+ .getFeatures()
+ .getArray()[0] as SvDirectionFeature
+ if (nextDirection.pano) {
+ evt.target.getFeatures().clear()
+ panorama!.setPano(nextDirection.pano)
+ }
+ }
+ }
+
+ function handleHover(event: MapBrowserEvent) {
+ if (isStreetviewActive.value) {
+ const hit = map.forEachFeatureAtPixel(
+ event.pixel,
+ function (feature: FeatureLike) {
+ if (feature instanceof SvDirectionFeature) {
+ return true
+ // TODO: maybe show description (name of next SV node) on hover ??
+ }
+ return false
+ }
+ )
+ map.getViewport().style.cursor = hit ? 'pointer' : ''
+ }
+ }
+
+ function setSvFeatures(loc: Coordinate) {
+ svPoint.setCoordinates(loc)
+ const pov = panorama!.getPov()
+ svCompassFeature.heading = pov.heading
+ svCompassFeature.zoom = Math.floor(panorama!.getZoom())
+ svCompassFeature.pitch = pov.pitch
+ const dir = new SvDirectionFeature(svPoint, 3, '5', '5')
+ dir.heading
+ const navigationLinks = panorama!.getLinks() || []
+ svFeature.value = {
+ compass: svCompassFeature,
+ directions: navigationLinks.map(
+ link =>
+ new SvDirectionFeature(
+ svPoint,
+ link?.heading || 0,
+ link?.description || '',
+ link?.pano
+ )
+ ),
+ }
+ }
+
+ function handlePanoramaPositionChange(updateLocation: boolean) {
+ panoPositionChanging.value = true
+ const position = panorama!.getPosition()
+ if (position) {
+ const panoLonLat = [position.lng(), position.lat()]
+ const loc = fromLonLat(panoLonLat)
+ setSvFeatures(loc)
+
+ if (updateLocation) {
+ locationInfo.value = loc
+ }
+
+ if (
+ locationInfo.value &&
+ !containsCoordinate(map.getView().calculateExtent(map.getSize()), loc)
+ ) {
+ map.getView().setCenter(loc)
+ }
+ }
+ nextTick(() => {
+ panoPositionChanging.value = false
+ })
+ }
+}
diff --git a/src/services/common/formatting.utils.spec.ts b/src/services/common/formatting.utils.spec.ts
index fe137f11..2a4dea3e 100644
--- a/src/services/common/formatting.utils.spec.ts
+++ b/src/services/common/formatting.utils.spec.ts
@@ -5,6 +5,8 @@ import {
formatLength,
formatMeasure,
} from './formatting.utils'
+import { initProjections } from '@/services/projection.utils'
+import { formatCoords } from '@/services/common/formatting.utils'
vi.mock('i18next', () => ({
default: { t: vi.fn(key => key) },
@@ -138,4 +140,55 @@ describe('Formatting utils', () => {
expect(val).toEqual('')
})
})
+ describe('#formatCoords', () => {
+ initProjections()
+ it('format basic lonlat coordinates', () => {
+ const formattedCoords = formatCoords(
+ [677840, 6390169],
+ 'EPSG:3857',
+ 'EPSG:4326'
+ )
+ expect(formattedCoords).toEqual('6.08914 E | 49.674932 N')
+ })
+ it('format DMS lonlat coordinates', () => {
+ const formattedCoords = formatCoords(
+ [677840, 6390169],
+ 'EPSG:3857',
+ 'EPSG:4326:DMS'
+ )
+ expect(formattedCoords).toEqual('6° 5′ 20.9″ E | 49° 40′ 29.8″ N')
+ })
+ it('format DMm lonlat coordinates', () => {
+ const formattedCoords = formatCoords(
+ [677840, 6390169],
+ 'EPSG:3857',
+ 'EPSG:4326:DMm'
+ )
+ expect(formattedCoords).toEqual('6° 5.348419′ E | 49° 40.495935′ N')
+ })
+ it('format basic lux coordinate', () => {
+ const formattedCoords = formatCoords(
+ [677840, 6390169],
+ 'EPSG:3857',
+ 'EPSG:2169'
+ )
+ expect(formattedCoords).toEqual('74299 E | 82266 N')
+ })
+ it('format UTM 31 coordinate', () => {
+ const formattedCoords = formatCoords(
+ [643596, 6390169],
+ 'EPSG:3857',
+ 'EPSG:3263*'
+ )
+ expect(formattedCoords).toEqual('700672 | 5506204 (UTM31N)')
+ })
+ it('format UTM 32 coordinate', () => {
+ const formattedCoords = formatCoords(
+ [677840, 6390169],
+ 'EPSG:3857',
+ 'EPSG:3263*'
+ )
+ expect(formattedCoords).toEqual('289999 | 5506558 (UTM32N)')
+ })
+ })
})
diff --git a/src/services/common/formatting.utils.ts b/src/services/common/formatting.utils.ts
index ef0e46e6..652bf9e3 100644
--- a/src/services/common/formatting.utils.ts
+++ b/src/services/common/formatting.utils.ts
@@ -1,4 +1,9 @@
import i18next from 'i18next'
+import { AddressResult } from '@/stores/location-info.store.model'
+import { transform, toLonLat } from 'ol/proj'
+import { Coordinate } from 'ol/coordinate'
+import { Projection } from 'ol/proj'
+import { PROJECTION_WGS84 } from '@/composables/map/map.composable'
export type FormatMeasureType = 'elevation' | 'length' | 'area'
@@ -60,7 +65,7 @@ export function formatElevation(value: number | string, digits = 0): string {
* @param digits The digits to fixed
* @returns The formatted value, or the original value if invalid number
*/
-export function formatLength(value: number, digits = 2): string {
+export function formatLength(value: number | null, digits = 2): string {
// null covers API errors or unavailable data (eg. elevation)
if (value === null) {
return i18next.t('N/A', { ns: 'client' })
@@ -90,3 +95,88 @@ export function formatArea(value: number, digits = 2): string {
return ''
}
}
+
+/**
+ * Format an address from reverse address lookup service
+ * @param address AddressResult structure returned by the reverse search API
+ * @returns The formatted address as " , "
+ */
+export function formatAddress(address: AddressResult | undefined) {
+ if (address === undefined) {
+ return i18next.t('N/A', { ns: 'client' })
+ }
+ return `${address.number}, ${address.street}, ${address.postal_code} ${address.locality}`
+}
+
+/**
+ * Format coordinates in different CRS as an 'xxx E | yyy N' pair
+ * known targets:
+ * EPSG:2169, EPSG:4326, EPSG:3263* (UTM zones 31N and 32N)
+ * EPSG:4326:DMS (degree, minute, second), EPSG:4326:DMm (degree, minute.minute_decimals)
+ * @param coords The coordinate pair to be formatted
+ * @param fromCrs the CRS of the source coordinate pair
+ * @param format output format / CSR among:
+ * EPSG:2169, EPSG:4326, EPSG:3263*
+ * EPSG:4326:DMS, EPSG:4326:DMm
+ * @returns The formatted Coordinate as a string
+ */
+export function formatCoords(
+ coords: Coordinate,
+ fromCrs: Projection | string,
+ format: string
+) {
+ const lonLanFormat = format.split(':')[2]
+ let toCrs = format
+ let utmZone = undefined
+ if (lonLanFormat !== undefined) {
+ toCrs = format.slice(0, format.lastIndexOf(':'))
+ } else if (format.endsWith('*')) {
+ const lonlat = toLonLat(coords, fromCrs)
+ utmZone = lonlat[0] <= 6 ? 'UTM31N' : 'UTM32N'
+ toCrs = format.replace('3*', utmZone.slice(3, 5))
+ }
+ const projectedCoords = transform(coords, fromCrs, toCrs)
+ const isDegrees = toCrs === PROJECTION_WGS84
+ const hemispheres = projectedCoords.map((coord: number, axis: number) => {
+ const axisNegative = (isDegrees ? normalizeDegrees(coord) : coord) < 0
+ return axisNegative ? 'WS'[axis] : 'EN'[axis]
+ })
+ const formattedCoords = projectedCoords.map(
+ (coord: number) =>
+ (
+ (isDegrees
+ ? formatDegrees(coord, lonLanFormat)
+ : Math.abs(Math.round(coord)).toString())
+ )
+ )
+ if (utmZone !== undefined) {
+ return `${formattedCoords.join(' | ')} (${utmZone})`
+ }
+ return formattedCoords
+ .map((coord: string, axis: number) => `${coord} ${hemispheres[axis]}`)
+ .join(' | ')
+}
+export function normalizeDegrees(degrees: number) {
+ return ((degrees + 180) % 360) - 180
+}
+export function formatDegrees(degrees: number, format: string) {
+ const normalizedDegrees = ((degrees + 180) % 360) - 180
+ const absDegrees = Math.abs(normalizedDegrees)
+ if (format === undefined) {
+ return Math.round(absDegrees * 1e6) / 1e6
+ }
+ const intDegrees = Math.floor(absDegrees)
+ const minutes = (absDegrees % 1) * 60
+ // convert to degree, minute, decimals format
+ if (format === 'DMm') {
+ const roundedMinutes = Math.round(minutes * 1e6) / 1e6
+ return `${intDegrees}\u00b0 ${roundedMinutes}\u2032`
+ }
+ if (format === 'DMS') {
+ // convert to degree, minute, second format
+ const intMinutes = Math.floor(minutes)
+ const seconds = Math.round((minutes % 1) * 600) / 10
+ return `${intDegrees}\u00b0 ${intMinutes}\u2032 ${seconds}\u2033`
+ }
+ return 'N/A'
+}
diff --git a/src/services/info/location-info.ts b/src/services/info/location-info.ts
new file mode 100644
index 00000000..181fd084
--- /dev/null
+++ b/src/services/info/location-info.ts
@@ -0,0 +1,73 @@
+import { urlStorage } from '@/services/state-persistor/storage/url-storage'
+import { transform } from 'ol/proj'
+import { Coordinate } from 'ol/coordinate'
+import { Projection } from 'ol/proj'
+import { containsCoordinate } from 'ol/extent'
+import { PROJECTION_LUX } from '@/composables/map/map.composable'
+
+import { getElevation } from '@/components/draw/feature-measurements-helper'
+
+export const INFO_PROJECTIONS = {
+ 'EPSG:2169': 'Luref',
+ 'EPSG:4326': 'Lon/Lat WGS84',
+ 'EPSG:4326:DMS': 'Lon/Lat WGS84 DMS',
+ 'EPSG:4326:DMm': 'Lon/Lat WGS84 DM',
+ 'EPSG:3263*': 'WGS84 UTM',
+}
+
+export const LIDAR_EXTENT = [46602, 53725, 106944, 141219]
+export const LIDAR_EXTENT_SRS = 'EPSG:2169'
+
+export async function queryInfos(location: Coordinate, fromCrs: Projection) {
+ const clickCoordinateLuref = transform(location, fromCrs, PROJECTION_LUX)
+ const [shortUrl, elevation, address] = (
+ await Promise.allSettled([
+ createShortUrl(location),
+ getElevation(location),
+ getNearestAddress(clickCoordinateLuref),
+ ])
+ ).map(r => (r.status === 'fulfilled' ? r.value : undefined))
+ return {
+ shortUrl,
+ elevation,
+ address,
+ clickCoordinateLuref,
+ isInBoxOfLidar: isInBoxOfLidar(clickCoordinateLuref),
+ }
+}
+
+function isInBoxOfLidar(clickCoordinateLuref: Coordinate) {
+ let testCoordinate = clickCoordinateLuref
+ if (PROJECTION_LUX !== LIDAR_EXTENT_SRS) {
+ testCoordinate = transform(
+ clickCoordinateLuref,
+ PROJECTION_LUX,
+ LIDAR_EXTENT_SRS
+ )
+ }
+ return containsCoordinate(LIDAR_EXTENT, testCoordinate)
+}
+
+export async function createShortUrl(optCoordinate: Coordinate | undefined) {
+ const shortUrl = (await urlStorage.getShortUrl(optCoordinate)).short_url
+ // workaround for incorrect route settings in migration
+ // TODO: remove when migration route settings are fixed
+ return shortUrl.replace(
+ 'http://g-o.lu/migration/',
+ import.meta.env.VITE_V3_API_HOST + 's/'
+ )
+}
+
+export function getQRUrl(shortUrl: string | undefined) {
+ return shortUrl && `${import.meta.env.VITE_QR_URL}?url=${shortUrl}`
+}
+
+export async function getNearestAddress(coords: Coordinate) {
+ const resp = await fetch(
+ `${import.meta.env.VITE_ADDRESS_URL}?easting=${coords[0]}&northing=${
+ coords[1]
+ }`
+ )
+ const json = await resp.json()
+ return json.results[0]
+}
diff --git a/src/services/info/street-view.ts b/src/services/info/street-view.ts
new file mode 100644
index 00000000..fdbfc787
--- /dev/null
+++ b/src/services/info/street-view.ts
@@ -0,0 +1,15 @@
+export async function loadGoogleapis() {
+ return await new Promise((resolve /*, reject*/) => {
+ if (!window.hasOwnProperty('google')) {
+ const script = document.createElement('script')
+ script.src =
+ 'https://maps.googleapis.com/maps/api/js?v=3&key=AIzaSyCObzX7dJqeGm5Wv2VwS4JzNyEtLsOgWX8'
+ script.onload = function () {
+ resolve('loaded')
+ }
+ document.getElementsByTagName('head')[0].appendChild(script)
+ } else {
+ resolve('loaded')
+ }
+ })
+}
diff --git a/src/services/info/sv-compass-feature.ts b/src/services/info/sv-compass-feature.ts
new file mode 100644
index 00000000..496ab664
--- /dev/null
+++ b/src/services/info/sv-compass-feature.ts
@@ -0,0 +1,44 @@
+import { Feature } from 'ol'
+import { FeatureLike } from 'ol/Feature'
+import { Style, Icon } from 'ol/style'
+import directionUrls from '@/services/info/sv-direction-urls'
+
+export class SvCompassFeature extends Feature {
+ zoom: number
+ heading: number
+ pitch: number
+
+ getStyleFunction() {
+ return function (feature: FeatureLike): Style[] {
+ let curZoom = (feature as SvCompassFeature).zoom
+ if (curZoom < 1) {
+ curZoom = 1
+ } else if (curZoom > 4) {
+ curZoom = 4
+ }
+ const curPitch = Math.abs((feature as SvCompassFeature).pitch)
+ let pitch = 0
+ if (curPitch >= 0 && curPitch <= 23) {
+ pitch = 0
+ } else if (curPitch > 23 && curPitch <= 45) {
+ pitch = 1
+ }
+ if (curPitch > 45 && curPitch <= 68) {
+ pitch = 2
+ }
+ if (curPitch > 68) {
+ pitch = 3
+ }
+ const directionArrowKey: string = `direction_sv_zl${curZoom}_p${pitch}`
+
+ return [
+ new Style({
+ image: new Icon({
+ src: directionUrls.get(directionArrowKey),
+ rotation: ((feature as SvCompassFeature).heading * Math.PI) / 180,
+ }),
+ }),
+ ]
+ }
+ }
+}
diff --git a/src/services/info/sv-direction-feature.ts b/src/services/info/sv-direction-feature.ts
new file mode 100644
index 00000000..fead4c9e
--- /dev/null
+++ b/src/services/info/sv-direction-feature.ts
@@ -0,0 +1,39 @@
+import { Feature } from 'ol'
+import { FeatureLike } from 'ol/Feature'
+import { Geometry } from 'ol/geom'
+import { Style, Icon } from 'ol/style'
+import arrowUrl from '@/assets/images/streetview/arrow.png'
+
+export class SvDirectionFeature extends Feature {
+ // By putting the properties below as public into the constructor, they are both declared
+ // and initialized in the constructor
+ // heading: number
+ // description: string
+ // pano: string
+
+ constructor(
+ geometry: Geometry,
+ public heading: number,
+ public description: string,
+ public pano: string | null | undefined
+ ) {
+ super(geometry)
+ this.setStyle(this.createStyleFunction())
+ }
+
+ private createStyleFunction() {
+ return function (feature: FeatureLike) {
+ return [
+ new Style({
+ image: new Icon({
+ anchor: [0.5, 50],
+ anchorXUnits: 'fraction',
+ anchorYUnits: 'pixels',
+ src: arrowUrl,
+ rotation: ((feature as SvDirectionFeature).heading * Math.PI) / 180,
+ }),
+ }),
+ ]
+ }
+ }
+}
diff --git a/src/services/info/sv-direction-urls.ts b/src/services/info/sv-direction-urls.ts
new file mode 100644
index 00000000..d1cd1f60
--- /dev/null
+++ b/src/services/info/sv-direction-urls.ts
@@ -0,0 +1,53 @@
+// generated by
+// z_seq_1_to_4 = [...Array(5).keys()].slice(1)
+// p_seq_0_to_4 = [...Array(5).keys()]
+// keys = z_seq_1_to_4.flatMap(z=>p_seq_0_to_4.map(p=> `direction_sv_zl${z}${p<4?'_p' + p:''}`))
+// console.log(keys.map(k => `import ${k} from '@/assets/images/streetview/${k}.png'`).join('\n'))
+
+import direction_sv_zl1_p0 from '@/assets/images/streetview/direction_sv_zl1_p0.png'
+import direction_sv_zl1_p1 from '@/assets/images/streetview/direction_sv_zl1_p1.png'
+import direction_sv_zl1_p2 from '@/assets/images/streetview/direction_sv_zl1_p2.png'
+import direction_sv_zl1_p3 from '@/assets/images/streetview/direction_sv_zl1_p3.png'
+import direction_sv_zl1 from '@/assets/images/streetview/direction_sv_zl1.png'
+import direction_sv_zl2_p0 from '@/assets/images/streetview/direction_sv_zl2_p0.png'
+import direction_sv_zl2_p1 from '@/assets/images/streetview/direction_sv_zl2_p1.png'
+import direction_sv_zl2_p2 from '@/assets/images/streetview/direction_sv_zl2_p2.png'
+import direction_sv_zl2_p3 from '@/assets/images/streetview/direction_sv_zl2_p3.png'
+import direction_sv_zl2 from '@/assets/images/streetview/direction_sv_zl2.png'
+import direction_sv_zl3_p0 from '@/assets/images/streetview/direction_sv_zl3_p0.png'
+import direction_sv_zl3_p1 from '@/assets/images/streetview/direction_sv_zl3_p1.png'
+import direction_sv_zl3_p2 from '@/assets/images/streetview/direction_sv_zl3_p2.png'
+import direction_sv_zl3_p3 from '@/assets/images/streetview/direction_sv_zl3_p3.png'
+import direction_sv_zl3 from '@/assets/images/streetview/direction_sv_zl3.png'
+import direction_sv_zl4_p0 from '@/assets/images/streetview/direction_sv_zl4_p0.png'
+import direction_sv_zl4_p1 from '@/assets/images/streetview/direction_sv_zl4_p1.png'
+import direction_sv_zl4_p2 from '@/assets/images/streetview/direction_sv_zl4_p2.png'
+import direction_sv_zl4_p3 from '@/assets/images/streetview/direction_sv_zl4_p3.png'
+import direction_sv_zl4 from '@/assets/images/streetview/direction_sv_zl4.png'
+
+// console.log(keys.join(',\n'))
+
+const direction_dict = {
+ direction_sv_zl1_p0,
+ direction_sv_zl1_p1,
+ direction_sv_zl1_p2,
+ direction_sv_zl1_p3,
+ direction_sv_zl1,
+ direction_sv_zl2_p0,
+ direction_sv_zl2_p1,
+ direction_sv_zl2_p2,
+ direction_sv_zl2_p3,
+ direction_sv_zl2,
+ direction_sv_zl3_p0,
+ direction_sv_zl3_p1,
+ direction_sv_zl3_p2,
+ direction_sv_zl3_p3,
+ direction_sv_zl3,
+ direction_sv_zl4_p0,
+ direction_sv_zl4_p1,
+ direction_sv_zl4_p2,
+ direction_sv_zl4_p3,
+ direction_sv_zl4,
+}
+
+export default new Map(Object.entries(direction_dict))
diff --git a/src/services/state-persistor/state-persistor-location-info.ts b/src/services/state-persistor/state-persistor-location-info.ts
new file mode 100644
index 00000000..943c366a
--- /dev/null
+++ b/src/services/state-persistor/state-persistor-location-info.ts
@@ -0,0 +1,56 @@
+import { watch, watchEffect, WatchStopHandle } from 'vue'
+import { storeToRefs } from 'pinia'
+import {
+ SP_KEY_CROSSHAIR,
+ SP_KEY_X,
+ SP_KEY_Y,
+ StatePersistorService,
+} from './state-persistor.model'
+import { storageHelper } from './storage/storage.helper'
+import useMap from '@/composables/map/map.composable'
+import { useLocationInfoStore } from '@/stores/location-info.store'
+
+class StatePersistorLocationInfo implements StatePersistorService {
+ bootstrap() {
+ this.restore()
+ let stop: WatchStopHandle
+ // eslint-disable-next-line prefer-const
+ stop = watchEffect(() => {
+ this.persist()
+ stop && stop() // test if exists, for HMR support
+ })
+ }
+
+ persist() {
+ const { locationInfo } = storeToRefs(useLocationInfoStore())
+
+ watch(
+ locationInfo,
+ value => {
+ if (value === undefined) {
+ storageHelper.removeItem(SP_KEY_CROSSHAIR)
+ } else {
+ storageHelper.setValue(SP_KEY_CROSSHAIR, true)
+ storageHelper.setValue(SP_KEY_X, value[0])
+ storageHelper.setValue(SP_KEY_Y, value[1])
+ }
+ },
+ { immediate: true }
+ )
+ }
+
+ restore() {
+ const { locationInfo } = storeToRefs(useLocationInfoStore())
+ const map = useMap().getOlMap()
+
+ const location = storageHelper.getValue(SP_KEY_CROSSHAIR)
+ // This represents the behaviour in V3, where restoring from permalink
+ // always displays the info of the map center
+ // This is not a very logical behaviour ??
+ if (location && location !== 'false') {
+ locationInfo.value = map.getView().getCenter()
+ }
+ }
+}
+
+export const statePersistorLocationInfo = new StatePersistorLocationInfo()
diff --git a/src/services/state-persistor/state-persistor.model.ts b/src/services/state-persistor/state-persistor.model.ts
index 7452d81a..2ede555f 100644
--- a/src/services/state-persistor/state-persistor.model.ts
+++ b/src/services/state-persistor/state-persistor.model.ts
@@ -24,6 +24,7 @@ export const SP_KEY_ROTATION = 'rotation'
export const SP_KEY_SRS = 'SRS' // TODO:
export const SP_KEY_X = 'X'
export const SP_KEY_Y = 'Y'
+export const SP_KEY_CROSSHAIR = 'crosshair'
export const SP_KEY_SERIAL = 'serial'
export const SP_KEY_SERIAL_LAYERS = 'serialLayer'
export const SP_KEYS_STYLE = [
diff --git a/src/services/state-persistor/storage/url-storage.ts b/src/services/state-persistor/storage/url-storage.ts
index 9bdc3b85..996a70d3 100644
--- a/src/services/state-persistor/storage/url-storage.ts
+++ b/src/services/state-persistor/storage/url-storage.ts
@@ -1,3 +1,11 @@
+import {
+ SP_KEY_LOCALFORAGE,
+ SP_KEY_APPLOGIN,
+ SP_KEY_IPV6,
+ SP_KEY_EMBEDDED_SERVER,
+ SP_KEY_EMBEDDED_SERVER_PROTOCOL,
+} from '@/services/state-persistor/state-persistor.model'
+
export class UrlStorage implements Storage {
private snappedUrl: URL
@@ -18,6 +26,52 @@ export class UrlStorage implements Storage {
throw new Error('Method key() not implemented. ' + index)
}
+ getStrippedUrl(optCoordinate: number[] | undefined) {
+ // stripped by embedded app parameters
+ const url = new URL(window.location.toString())
+ const params = new URLSearchParams(url.search)
+
+ if (optCoordinate !== undefined) {
+ params.set('X', Math.round(optCoordinate[0]).toString())
+ params.set('Y', Math.round(optCoordinate[1]).toString())
+ }
+ params.delete(SP_KEY_LOCALFORAGE)
+ params.delete(SP_KEY_APPLOGIN)
+ params.delete(SP_KEY_IPV6)
+ params.delete(SP_KEY_EMBEDDED_SERVER)
+ params.delete(SP_KEY_EMBEDDED_SERVER_PROTOCOL)
+
+ url.search = params.toString()
+
+ return url.toString()
+ }
+
+ async getShortUrl(optCoordinate: number[] | undefined) {
+ const strippedUrl = this.getStrippedUrl(optCoordinate)
+ // convert github pages and vite ports localhost 4173 or 5173
+ // to hosts accepted by the shortURL entrypoint
+ // TODO: remove when there is a v4 API available
+ .replace(
+ /https:\/\/geoportail-luxembourg.github.io\/luxembourg-geoportail\/.+\//,
+ import.meta.env.VITE_V3_API_HOST
+ )
+ .replace(/http:\/\/localhost:[45]173\//, import.meta.env.VITE_V3_API_HOST)
+
+ const data = new URLSearchParams()
+
+ data.set('url', strippedUrl)
+
+ const response = await fetch(import.meta.env.VITE_SHORT_URL, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: data.toString(),
+ })
+
+ return await response.json()
+ }
+
getSnappedUrl() {
return this.snappedUrl
}
diff --git a/src/services/utils.ts b/src/services/utils.ts
index 209b6e1d..e2c075eb 100644
--- a/src/services/utils.ts
+++ b/src/services/utils.ts
@@ -146,12 +146,28 @@ export function sanitizeFilename(filename: string) {
return filename.replace(/\s+/g, '_').replace(/[^a-z0-9\-_]/gi, '') || '_'
}
+export async function downloadUrl(
+ url: string | URL | Request,
+ filename: string
+) {
+ const response = await fetch(url)
+ if (!response.ok) {
+ throw new Error()
+ }
+ const blob = await response.blob()
+ downloadBlob(filename, blob)
+}
+
export function downloadFile(
filename: string,
content: BlobPart,
contentType = 'text/plain'
) {
const blob = new Blob([content], { type: contentType })
+ downloadBlob(filename, blob)
+}
+
+export function downloadBlob(filename: string, blob: Blob) {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
diff --git a/src/stores/location-info.store.model.ts b/src/stores/location-info.store.model.ts
new file mode 100644
index 00000000..1b26e3dc
--- /dev/null
+++ b/src/stores/location-info.store.model.ts
@@ -0,0 +1,16 @@
+import { SvCompassFeature } from '@/services/info/sv-compass-feature'
+import { SvDirectionFeature } from '@/services/info/sv-direction-feature'
+
+export interface SvFeature {
+ compass: SvCompassFeature
+ directions: SvDirectionFeature[]
+}
+
+export interface AddressResult {
+ formattedAddress: string
+ distance: number
+ number: number
+ street: string
+ postal_code: number
+ locality: string
+}
diff --git a/src/stores/location-info.store.ts b/src/stores/location-info.store.ts
new file mode 100644
index 00000000..467622a0
--- /dev/null
+++ b/src/stores/location-info.store.ts
@@ -0,0 +1,31 @@
+import { defineStore, acceptHMRUpdate } from 'pinia'
+import { ref, Ref } from 'vue'
+import { SvFeature } from '@/stores/location-info.store.model'
+import { Coordinate } from 'ol/coordinate'
+import { Feature } from 'ol'
+
+export const useLocationInfoStore = defineStore('info', () => {
+ const locationInfo: Ref = ref(undefined)
+ const hidePointer: Ref = ref(false)
+ const isStreetviewActive: Ref = ref(false)
+ const noDataAtLocation = ref(true)
+ const streetViewLoading: Ref = ref(false)
+ const panoPositionChanging: Ref = ref(false)
+ const svFeature: Ref = ref(undefined)
+ const routingFeatureTemp: Ref = ref(undefined)
+
+ return {
+ locationInfo,
+ hidePointer,
+ isStreetviewActive,
+ noDataAtLocation,
+ streetViewLoading,
+ panoPositionChanging,
+ svFeature,
+ routingFeatureTemp,
+ }
+})
+
+if (import.meta.hot) {
+ import.meta.hot.accept(acceptHMRUpdate(useLocationInfoStore, import.meta.hot))
+}
diff --git a/tailwind.config.cjs b/tailwind.config.cjs
index 37d9fe8f..501013b8 100644
--- a/tailwind.config.cjs
+++ b/tailwind.config.cjs
@@ -170,6 +170,8 @@ module.exports = {
measure: '"\\e021"',
print: '"\\e02f"',
share: '"\\e02a"',
+ streetview:
+ 'url("/src/assets/images/streetview/streetview_placeholder.png")',
download: 'url("/src/assets/images/palette.svg")',
upload:
"url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 640 512'%3E%3C!--! Font Awesome Pro 6.4.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --%3E%3Cpath style='fill:white;' d='M144 480C64.5 480 0 415.5 0 336c0-62.8 40.2-116.2 96.2-135.9c-.1-2.7-.2-5.4-.2-8.1c0-88.4 71.6-160 160-160c59.3 0 111 32.2 138.7 80.2C409.9 102 428.3 96 448 96c53 0 96 43 96 96c0 12.2-2.3 23.8-6.4 34.6C596 238.4 640 290.1 640 352c0 70.7-57.3 128-128 128H144zm79-217c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l39-39V392c0 13.3 10.7 24 24 24s24-10.7 24-24V257.9l39 39c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-80-80c-9.4-9.4-24.6-9.4-33.9 0l-80 80z'/%3E%3C/svg%3E\")",