- Added geocoding functionality:
- Implemented address verification and geolocation in `CreateNewNode`. - Integrated `GeoApiService` with error handling using `GeocodingException`. - Updated `NodeController` for geocoding and user node creation. - Refactored related components and store for enhanced geolocation support.
This commit is contained in:
parent
d7bc62c713
commit
f657ff2c80
9 changed files with 250 additions and 39 deletions
|
|
@ -99,8 +99,8 @@ function handleErrorResponse(data, requestingStore, request) {
|
||||||
error.errorObj = errorObj;
|
error.errorObj = errorObj;
|
||||||
|
|
||||||
|
|
||||||
if (request.expectedException === null || Array.isArray(request.expectResponse) && !request.expectedException.includes(data.error.title) || (typeof request.expectedException === 'string' && !data.error.title !== request.expectedException)) {
|
if (request.expectedException === null || (Array.isArray(request.expectResponse) && !request.expectedException.includes(data.error.title)) || (typeof request.expectedException === 'string' && data.error.title !== request.expectedException)) {
|
||||||
logger.error(errorObj);
|
logger.error(errorObj, request.expectedException);
|
||||||
const errorStore = useErrorStore();
|
const errorStore = useErrorStore();
|
||||||
void errorStore.addError(errorObj, {store: requestingStore, request: request});
|
void errorStore.addError(errorObj, {store: requestingStore, request: request});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="create-new-node-container">
|
<div class="create-new-node-container">
|
||||||
<h3 class="sub-header">Create new supplier</h3>
|
<h3 class="sub-header">Create new supplier</h3>
|
||||||
<form @submit.prevent="send">
|
<form @submit.prevent>
|
||||||
<div class="create-new-node-form-container">
|
<div class="create-new-node-form-container">
|
||||||
<div class="input-field-caption">Name:</div>
|
<div class="input-field-caption">Name:</div>
|
||||||
<div><div class="text-container"><input class="input-field" v-model="nodeName"/></div></div>
|
<div>
|
||||||
|
<div class="text-container"><input class="input-field" v-model="nodeName"/></div>
|
||||||
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div class="input-field-caption">Address:</div>
|
<div class="input-field-caption">Address:</div>
|
||||||
<div><div class="text-container"><input class="input-field" v-model="nodeAddress"/></div></div>
|
|
||||||
<div>
|
<div>
|
||||||
<basic-button icon="SealCheck">Verify address</basic-button>
|
<div class="text-container"><input class="input-field" v-model="nodeAddress" @input="checkChange"/></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-field-caption">Coordinates:</div>
|
<div>
|
||||||
<div>{{ nodeCoordinates }}</div>
|
<basic-button icon="SealCheck" @click.prevent="verifyAddress" :disabled="addressVerified" @submit.prevent>Check address</basic-button>
|
||||||
|
<Toast ref="toast"/>
|
||||||
|
</div>
|
||||||
|
<div class="input-field-caption" v-if="addressVerified">Country:</div>
|
||||||
|
<div class="country-field" v-if="addressVerified">
|
||||||
|
<flag :iso="nodeIso"/>
|
||||||
|
{{ nodeCountry }}
|
||||||
|
</div>
|
||||||
|
<div v-if="addressVerified"></div>
|
||||||
|
<div class="input-field-caption" v-if="nodeCoordinates && addressVerified">Coordinates:</div>
|
||||||
|
<div class="coordinate-field" v-if="nodeCoordinates && addressVerified">{{ coordinatesDMS }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="create-new-node-map">
|
<div class="supplier-map" v-if="nodeCoordinates && addressVerified">
|
||||||
<img width="300px" src="https://www.galerie-braunbehrens.de/wp-content/uploads/2020/06/placeholder-google-maps.jpg" alt="map">
|
<open-street-map-embed :coordinates="nodeCoordinates" :zoom="15" width="100%" height="300px"
|
||||||
|
custom-filter="grayscale(0.8) sepia(0.5) hue-rotate(180deg) saturate(0.5) brightness(1.0)"></open-street-map-embed>
|
||||||
</div>
|
</div>
|
||||||
<div class="create-new-node-footer">
|
<div class="create-new-node-footer">
|
||||||
<basic-button variant="primary" :show-icon="false" :disabled="unverified">OK</basic-button>
|
<basic-button @click.prevent="send" variant="primary" :show-icon="false" :disabled="unverified">Create</basic-button>
|
||||||
<basic-button @click.prevent="cancel" variant="secondary" :show-icon="false">Cancel</basic-button>
|
<basic-button @click.prevent="cancel" variant="secondary" :show-icon="false">Cancel</basic-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -28,27 +40,105 @@
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import BasicButton from "@/components/UI/BasicButton.vue";
|
import BasicButton from "@/components/UI/BasicButton.vue";
|
||||||
|
import {mapStores} from "pinia";
|
||||||
|
import {useNodeStore} from "@/store/node.js";
|
||||||
|
import OpenStreetMapEmbed from "@/components/UI/OpenStreetMapEmbed.vue";
|
||||||
|
import Toast from "@/components/UI/Toast.vue";
|
||||||
|
import Flag from "@/components/UI/Flag.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "CreateNewNode",
|
name: "CreateNewNode",
|
||||||
components: {BasicButton},
|
components: {Flag, Toast, OpenStreetMapEmbed, BasicButton},
|
||||||
|
emits: ['created', 'close'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
nodeName: "",
|
nodeName: "",
|
||||||
nodeAddress: "",
|
nodeAddress: "",
|
||||||
nodeCoordinates: "",
|
nodeCoordinates: null,
|
||||||
addressVerified: false
|
nodeCountry: null,
|
||||||
|
nodeIso: null,
|
||||||
|
addressVerified: false,
|
||||||
|
verifiedAddress: null,
|
||||||
|
node: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
coordinatesDMS() {
|
||||||
|
|
||||||
|
if (this.nodeCoordinates != null && typeof this.nodeCoordinates === 'object') {
|
||||||
|
return `${this.convertToDMS(this.nodeCoordinates?.latitude, 'lat')}, ${this.convertToDMS(this.nodeCoordinates?.longitude, 'lng')}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
...mapStores(useNodeStore),
|
||||||
unverified() {
|
unverified() {
|
||||||
return !this.addressVerified;
|
return !this.addressVerified || !this.nodeName;
|
||||||
}
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
send() {
|
convertToDMS(coordinate, type) {
|
||||||
console.log("Sending...");
|
|
||||||
|
|
||||||
|
if (!coordinate)
|
||||||
|
return '';
|
||||||
|
|
||||||
|
let direction;
|
||||||
|
if (type === 'lat') {
|
||||||
|
direction = coordinate >= 0 ? 'N' : 'S';
|
||||||
|
} else {
|
||||||
|
direction = coordinate >= 0 ? 'E' : 'W';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbeite mit Absolutwert
|
||||||
|
const abs = Math.abs(coordinate);
|
||||||
|
|
||||||
|
// Grad (ganzzahliger Teil)
|
||||||
|
const degrees = Math.floor(abs);
|
||||||
|
|
||||||
|
// Minuten
|
||||||
|
const minutesFloat = (abs - degrees) * 60;
|
||||||
|
const minutes = minutesFloat.toFixed(4);
|
||||||
|
|
||||||
|
|
||||||
|
return `${degrees}° ${minutes}' ${direction}`;
|
||||||
|
},
|
||||||
|
checkChange() {
|
||||||
|
this.addressVerified = this.nodeAddress === this.verifiedAddress;
|
||||||
|
},
|
||||||
|
async verifyAddress() {
|
||||||
|
const {node: node, error: error} = await this.nodeStore.locate(this.nodeAddress);
|
||||||
|
|
||||||
|
this.nodeCoordinates = null;
|
||||||
|
this.nodeIso = null;
|
||||||
|
this.nodeCountry = null;
|
||||||
|
this.addressVerified = false;
|
||||||
|
this.verifiedAddress = null;
|
||||||
|
|
||||||
|
if (error !== null) {
|
||||||
|
this.$refs.toast.addToast({
|
||||||
|
icon: 'warning',
|
||||||
|
message: error.message,
|
||||||
|
title: "Cannot locate address.",
|
||||||
|
variant: 'exception',
|
||||||
|
duration: 8000
|
||||||
|
})
|
||||||
|
} else if (node) {
|
||||||
|
this.nodeCoordinates = node.location;
|
||||||
|
this.nodeAddress = node.address;
|
||||||
|
|
||||||
|
this.nodeIso = node.country.iso_code;
|
||||||
|
this.nodeCountry = node.country.name;
|
||||||
|
|
||||||
|
this.addressVerified = true;
|
||||||
|
this.verifiedAddress = node.address;
|
||||||
|
this.node = node;
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async send() {
|
||||||
|
const node = await this.nodeStore.addNode(this.nodeName, this.nodeAddress, this.nodeCoordinates, this.node.country);
|
||||||
|
this.$emit("created", node);
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.$emit("close");
|
this.$emit("close");
|
||||||
|
|
@ -64,6 +154,14 @@ export default {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.supplier-map {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex: 1;
|
||||||
|
padding: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.sub-header {
|
.sub-header {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
|
|
@ -71,7 +169,7 @@ export default {
|
||||||
margin-bottom: 1.6rem;
|
margin-bottom: 1.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-field-caption{
|
.input-field-caption {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
|
|
@ -79,6 +177,30 @@ export default {
|
||||||
color: #001D33
|
color: #001D33
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.coordinate-field {
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
resize: none;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -87,6 +209,8 @@ export default {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
color: #002F54;
|
color: #002F54;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-container {
|
.text-container {
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<modal :close-on-backdrop="false" :state="newSupplierModalState" @close="closeModal('newSupplier')">
|
<modal :close-on-backdrop="false" :state="newSupplierModalState" @close="closeModal('newSupplier')">
|
||||||
<create-new-node @close="closeModal('newSupplier')"></create-new-node>
|
<create-new-node @created="addCreatedNode" @close="closeModal('newSupplier')"></create-new-node>
|
||||||
</modal>
|
</modal>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -153,6 +153,10 @@ export default {
|
||||||
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
this.assistantStore.getMaterialsAndSuppliers(this.partNumberField);
|
||||||
this.partNumberField = '';
|
this.partNumberField = '';
|
||||||
},
|
},
|
||||||
|
addCreatedNode(supplier) {
|
||||||
|
this.closeModal('newSupplier')
|
||||||
|
this.assistantStore.addSupplier(supplier);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.nodeStore.loadNodes();
|
this.nodeStore.loadNodes();
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import {defineStore} from 'pinia'
|
||||||
import {config} from '@/config'
|
import {config} from '@/config'
|
||||||
import {useErrorStore} from "@/store/error.js";
|
import {useErrorStore} from "@/store/error.js";
|
||||||
import performRequest from "@/backend.js";
|
import performRequest from "@/backend.js";
|
||||||
|
import logger from "@/logger.js";
|
||||||
|
|
||||||
|
|
||||||
export const useNodeStore = defineStore('node', {
|
export const useNodeStore = defineStore('node', {
|
||||||
|
|
@ -29,6 +30,28 @@ export const useNodeStore = defineStore('node', {
|
||||||
this.query.type = 'list';
|
this.query.type = 'list';
|
||||||
await this.loadNodes();
|
await this.loadNodes();
|
||||||
},
|
},
|
||||||
|
async locate(address) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
let error = null;
|
||||||
|
params.append('address', address);
|
||||||
|
const data = await performRequest(this, "GET", `${config.backendUrl}/nodes/locate/${params.size === 0 ? '' : '?'}${params.toString()}`, null, true, 'Geocoding error').catch(e => {
|
||||||
|
logger.log("geo locate exception", e.errorObj);
|
||||||
|
error = e.errorObj;
|
||||||
|
});
|
||||||
|
return {node: data?.data, error: error};
|
||||||
|
},
|
||||||
|
async addNode(name, address, location, country ) {
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
name: name,
|
||||||
|
address: address,
|
||||||
|
location: location,
|
||||||
|
country: country
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await performRequest(this, "PUT", `${config.backendUrl}/nodes/`, body, true);
|
||||||
|
return data.data;
|
||||||
|
},
|
||||||
async loadNodes() {
|
async loadNodes() {
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
|
|
@ -42,10 +65,10 @@ export const useNodeStore = defineStore('node', {
|
||||||
if (this.query.includeUserNode)
|
if (this.query.includeUserNode)
|
||||||
params.append('include_user_node', this.query.includeUserNode);
|
params.append('include_user_node', this.query.includeUserNode);
|
||||||
|
|
||||||
if(this.query?.page)
|
if (this.query?.page)
|
||||||
params.append('page', this.query.page);
|
params.append('page', this.query.page);
|
||||||
|
|
||||||
if(this.query?.pageSize)
|
if (this.query?.pageSize)
|
||||||
params.append('limit', this.query.pageSize);
|
params.append('limit', this.query.pageSize);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import de.avatic.lcc.dto.error.ErrorDTO;
|
||||||
import de.avatic.lcc.dto.error.ErrorResponseDTO;
|
import de.avatic.lcc.dto.error.ErrorResponseDTO;
|
||||||
import de.avatic.lcc.util.exception.base.BadRequestException;
|
import de.avatic.lcc.util.exception.base.BadRequestException;
|
||||||
import de.avatic.lcc.util.exception.base.ForbiddenException;
|
import de.avatic.lcc.util.exception.base.ForbiddenException;
|
||||||
|
import de.avatic.lcc.util.exception.internalerror.GeocodingException;
|
||||||
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
|
import de.avatic.lcc.util.exception.internalerror.PremiseValidationError;
|
||||||
import jakarta.validation.ConstraintViolation;
|
import jakarta.validation.ConstraintViolation;
|
||||||
import jakarta.validation.ConstraintViolationException;
|
import jakarta.validation.ConstraintViolationException;
|
||||||
|
|
@ -14,7 +15,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
|
||||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
@ -130,7 +130,7 @@ public class GlobalExceptionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(PremiseValidationError.class)
|
@ExceptionHandler(PremiseValidationError.class)
|
||||||
public ResponseEntity<ErrorResponseDTO> handleGenericException(PremiseValidationError exception) {
|
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(PremiseValidationError exception) {
|
||||||
ErrorDTO error = new ErrorDTO(
|
ErrorDTO error = new ErrorDTO(
|
||||||
exception.getClass().getName(),
|
exception.getClass().getName(),
|
||||||
"Premiss validation error",
|
"Premiss validation error",
|
||||||
|
|
@ -141,8 +141,20 @@ public class GlobalExceptionHandler {
|
||||||
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.INTERNAL_SERVER_ERROR);
|
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(GeocodingException.class)
|
||||||
|
public ResponseEntity<ErrorResponseDTO> handleGeocodingException(GeocodingException exception) {
|
||||||
|
ErrorDTO error = new ErrorDTO(
|
||||||
|
exception.getClass().getName(),
|
||||||
|
"Geocoding error",
|
||||||
|
exception.getMessage(),
|
||||||
|
Arrays.asList(exception.getStackTrace())
|
||||||
|
);
|
||||||
|
|
||||||
|
return new ResponseEntity<>(new ErrorResponseDTO(error), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ErrorResponseDTO> handleGenericException(Exception exception) {
|
public ResponseEntity<ErrorResponseDTO> handlePremiseValidationException(Exception exception) {
|
||||||
ErrorDTO error = new ErrorDTO(
|
ErrorDTO error = new ErrorDTO(
|
||||||
exception.getClass().getName(),
|
exception.getClass().getName(),
|
||||||
"Internal Server Error",
|
"Internal Server Error",
|
||||||
|
|
|
||||||
|
|
@ -55,35 +55,34 @@ public class NodeController {
|
||||||
return ResponseEntity.ok(nodeService.searchNode(filter, limit, nodeType, includeUserNode));
|
return ResponseEntity.ok(nodeService.searchNode(filter, limit, nodeType, includeUserNode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping({"/{id}","/{id}/"})
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
|
@PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
|
||||||
public ResponseEntity<NodeDetailDTO> getNode(@PathVariable Integer id) {
|
public ResponseEntity<NodeDetailDTO> getNode(@PathVariable Integer id) {
|
||||||
return ResponseEntity.ok(nodeService.getNode(id));
|
return ResponseEntity.ok(nodeService.getNode(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping({"/{id}","/{id}/"})
|
||||||
@PreAuthorize("hasRole('SUPER')")
|
@PreAuthorize("hasRole('SUPER')")
|
||||||
public ResponseEntity<Integer> deleteNode(@PathVariable Integer id) {
|
public ResponseEntity<Integer> deleteNode(@PathVariable Integer id) {
|
||||||
return ResponseEntity.ok(nodeService.deleteNode(id));
|
return ResponseEntity.ok(nodeService.deleteNode(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping({"/{id}","/{id}/"})
|
||||||
@PreAuthorize("hasRole('SUPER')")
|
@PreAuthorize("hasRole('SUPER')")
|
||||||
public ResponseEntity<Integer> updateNode(@PathVariable Integer id, @RequestBody NodeUpdateDTO node) {
|
public ResponseEntity<Integer> updateNode(@PathVariable Integer id, @RequestBody NodeUpdateDTO node) {
|
||||||
Check.equals(id, node.getId());
|
Check.equals(id, node.getId());
|
||||||
return ResponseEntity.ok(nodeService.updateNode(node));
|
return ResponseEntity.ok(nodeService.updateNode(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/locate")
|
@GetMapping({"/locate", "/locate/"})
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING')")
|
@PreAuthorize("hasAnyRole('SUPER', 'FREIGHT', 'PACKAGING', 'CALCULATION')")
|
||||||
public ResponseEntity<Location> locateNode(@RequestParam String address) {
|
public ResponseEntity<NodeDTO> locateNode(@RequestParam String address) {
|
||||||
return ResponseEntity.ok(geoApiService.locate(address));
|
return ResponseEntity.ok(geoApiService.locateNode(address));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/")
|
@PutMapping({"","/"})
|
||||||
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
@PreAuthorize("hasAnyRole('SUPER', 'CALCULATION')")
|
||||||
public ResponseEntity<Void> addUserNode(@RequestBody AddUserNodeDTO node) {
|
public ResponseEntity<NodeDTO> addUserNode(@RequestBody AddUserNodeDTO node) {
|
||||||
userNodeService.addUserNode(node);
|
return ResponseEntity.ok(userNodeService.addUserNode(node));
|
||||||
return ResponseEntity.ok().build();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
package de.avatic.lcc.service.api;
|
package de.avatic.lcc.service.api;
|
||||||
|
|
||||||
|
import de.avatic.lcc.dto.generic.NodeDTO;
|
||||||
|
import de.avatic.lcc.dto.generic.NodeType;
|
||||||
|
import de.avatic.lcc.model.azuremaps.geocoding.Feature;
|
||||||
import de.avatic.lcc.model.azuremaps.geocoding.GeocodingResponse;
|
import de.avatic.lcc.model.azuremaps.geocoding.GeocodingResponse;
|
||||||
import de.avatic.lcc.model.azuremaps.geocoding.GeocodingResult;
|
import de.avatic.lcc.model.azuremaps.geocoding.GeocodingResult;
|
||||||
import de.avatic.lcc.model.azuremaps.geocoding.Feature;
|
import de.avatic.lcc.model.country.IsoCode;
|
||||||
import de.avatic.lcc.model.nodes.Location;
|
import de.avatic.lcc.model.nodes.Location;
|
||||||
|
import de.avatic.lcc.repositories.country.CountryRepository;
|
||||||
|
import de.avatic.lcc.service.transformer.generic.CountryTransformer;
|
||||||
|
import de.avatic.lcc.service.transformer.generic.LocationTransformer;
|
||||||
|
import de.avatic.lcc.util.exception.internalerror.GeocodingException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
@ -12,6 +19,7 @@ import org.springframework.web.client.RestTemplate;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GeoApiService {
|
public class GeoApiService {
|
||||||
|
|
@ -21,11 +29,17 @@ public class GeoApiService {
|
||||||
|
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
private final String subscriptionKey;
|
private final String subscriptionKey;
|
||||||
|
private final LocationTransformer locationTransformer;
|
||||||
|
private final CountryRepository countryRepository;
|
||||||
|
private final CountryTransformer countryTransformer;
|
||||||
|
|
||||||
public GeoApiService(RestTemplate restTemplate,
|
public GeoApiService(RestTemplate restTemplate,
|
||||||
@Value("${azure.maps.subscription.key}") String subscriptionKey) {
|
@Value("${azure.maps.subscription.key}") String subscriptionKey, LocationTransformer locationTransformer, CountryRepository countryRepository, CountryTransformer countryTransformer) {
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
this.subscriptionKey = subscriptionKey;
|
this.subscriptionKey = subscriptionKey;
|
||||||
|
this.locationTransformer = locationTransformer;
|
||||||
|
this.countryRepository = countryRepository;
|
||||||
|
this.countryTransformer = countryTransformer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GeocodingResult geocode(String address) {
|
public GeocodingResult geocode(String address) {
|
||||||
|
|
@ -57,7 +71,7 @@ public class GeoApiService {
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Failed to geocode address: {}", address, e);
|
logger.error("Failed to geocode address: {}", address, e);
|
||||||
throw new RuntimeException("Geocoding failed for: " + address, e);
|
throw new GeocodingException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,4 +79,22 @@ public class GeoApiService {
|
||||||
GeocodingResult result = geocode(address);
|
GeocodingResult result = geocode(address);
|
||||||
return result != null ? result.getLocation() : null;
|
return result != null ? result.getLocation() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NodeDTO locateNode(String address) {
|
||||||
|
NodeDTO node = new NodeDTO();
|
||||||
|
|
||||||
|
GeocodingResult result = geocode(address);
|
||||||
|
|
||||||
|
if (result == null || result.getAddress() == null || result.getLocation() == null) throw new GeocodingException();
|
||||||
|
|
||||||
|
node.setUserNode(true);
|
||||||
|
node.setDeprecated(false);
|
||||||
|
node.setTypes(List.of(NodeType.SOURCE));
|
||||||
|
|
||||||
|
node.setAddress(result.getAddress().getFormattedAddress());
|
||||||
|
node.setCountry(countryTransformer.toCountryDTO(countryRepository.getByIsoCode(IsoCode.valueOf(result.getAddress().getCountryRegion().getIso())).orElseThrow()));
|
||||||
|
node.setLocation(locationTransformer.toLocationDTO(result.getLocation()));
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package de.avatic.lcc.service.transformer.generic;
|
package de.avatic.lcc.service.transformer.generic;
|
||||||
|
|
||||||
import de.avatic.lcc.dto.generic.LocationDTO;
|
import de.avatic.lcc.dto.generic.LocationDTO;
|
||||||
|
import de.avatic.lcc.model.nodes.Location;
|
||||||
import de.avatic.lcc.model.nodes.Node;
|
import de.avatic.lcc.model.nodes.Node;
|
||||||
import de.avatic.lcc.model.premises.route.RouteNode;
|
import de.avatic.lcc.model.premises.route.RouteNode;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
@ -11,6 +12,10 @@ public class LocationTransformer {
|
||||||
return new LocationDTO(entity.getGeoLat().doubleValue(), entity.getGeoLng().doubleValue());
|
return new LocationDTO(entity.getGeoLat().doubleValue(), entity.getGeoLng().doubleValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LocationDTO toLocationDTO(Location entity) {
|
||||||
|
return new LocationDTO(entity.getLatitude(), entity.getLongitude());
|
||||||
|
}
|
||||||
|
|
||||||
public LocationDTO toLocationDTO(RouteNode entity) {
|
public LocationDTO toLocationDTO(RouteNode entity) {
|
||||||
return new LocationDTO(entity.getGeoLat().doubleValue(), entity.getGeoLng().doubleValue());
|
return new LocationDTO(entity.getGeoLat().doubleValue(), entity.getGeoLng().doubleValue());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package de.avatic.lcc.util.exception.internalerror;
|
||||||
|
|
||||||
|
import de.avatic.lcc.util.exception.base.InternalErrorException;
|
||||||
|
|
||||||
|
public class GeocodingException extends InternalErrorException {
|
||||||
|
|
||||||
|
public GeocodingException() {
|
||||||
|
super("Geocoding failed", "Unable to locate the entered address");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue