This is the third article of the series. The first article is about integrating Bing Maps with Angular and Spring Boot with Jpa. The second article is about adding a new shape to the map and storing it in the Spring Boot backend.
This article is about removing properties of a map with a modal panel to confirm and delete it in the database. The project AngularAndSpringWithMaps is used as an example. To remove a property the property has to be clicked on.
The Angular Front-End
In the company-site.componten.ts file, click handlers are added to the Bing Map:
private addPolygon(polygon: Polygon): void {
//console.log(this.map.getCenter());
const polygonRings = polygon.rings.map(myRing =>
myRing.locations.map(myLocation => new Microsoft.Maps.Location(myLocation.latitude, myLocation.longitude)));
const mapPolygon = new Microsoft.Maps.Polygon(polygonRings);
mapPolygon.metadata = {
companySiteId: (this.componentForm.controls[this.COMPANY_SITE].value as CompanySite).id,
polygonId: polygon.id
} as PolygonMetaData;
Microsoft.Maps.Events.addHandler(mapPolygon, 'click', (e) => this.onPolygonDblClick(e));
this.map.entities.push(mapPolygon);
}
private onPolygonDblClick(e: Microsoft.Maps.IMouseEventArgs | Microsoft.Maps.IPrimitiveChangedEventArgs): void {
if ((e as Microsoft.Maps.IMouseEventArgs).targetType === 'polygon'
&& (e as Microsoft.Maps.IMouseEventArgs).eventName === 'click') {
//console.log((e as Microsoft.Maps.IMouseEventArgs).target);
const myPolygon = ((e as Microsoft.Maps.IMouseEventArgs).target) as Microsoft.Maps.Polygon;
this.openDeleteDialog(myPolygon.metadata as PolygonMetaData);
}
}
In line 1, the addPolygon method is defined which is called when a property is added.
In lines 3-6, the polygon is converted in a Bing Maps polygon with rings.
In lines 7-10, metadata is added to the Bing Maps polygon. The companySite id and polygon id are needed to delete the entities in the database.
In lines 11-14, the onPolygonDblClick method is added as an event handler for the polygon on Bing Maps for a dblclick event.
In line 15, the polygon is added to the map.
In lines 16-17, the onPolygonDblClick handler is defined with the event of the map.
In lines 18-19, the events are filtered for dblclick events on a ‘polygon’.
In lines 20-22, the map polygon is retrieved of the event.
In line 23, the openDeleteDialog method is called with the polygon metadata to confirm the delete of the polygon.
The modal dialog to confirm the deletion of the polygon is handled with the openDeleteDialog method:
private openDeleteDialog(polygonMetaData: PolygonMetaData): void {
const myPolygon = (this.componentForm.controls[this.COMPANY_SITE].value as CompanySite).polygons.filter(polygon =>
polygon.id = polygonMetaData.polygonId);
if (myPolygon && myPolygon.length > 0) {
const dialogRef = this.dialog.open(PolygonDeleteDialogComponent, {
width: '350px',
data: { polygonName: myPolygon[0].title, polygonId: polygonMetaData.polygonId } as DialogMetaData
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed ' + result);
if (result === MyDialogResult.delete) {
//console.log(polygonMetaData);
this.companySiteService
.deletePolygon(polygonMetaData.companySiteId,
polygonMetaData.polygonId)
.pipe(switchMap(() => this.companySiteService
.findById(polygonMetaData.companySiteId)))
.subscribe(myCompanySite => { this.componentForm.controls[this.COMPANY_SITE].setValue(myCompanySite);
this.clearMapPins();
this.updateMap(myCompanySite);
});
}
});
}
}
In line 2-5, the polygon for the entity is filtered out of the companySite and the existence is checked.
In line 6-10, the modal dialog of Angular Material is opened. The content of the modal is in the PolygonDeleteDialogComponent and the data property gets the DialogMetaData.
In lines 11-13, the modal dialog result is subscribed and filtered for the delete result.
In lines 15-20, the CompanySiteService is used to delete the polygon and then reload the companySite.
In lines 21-24, the reloaded CompanySite is updated in the reactive form.
In line 23, possible pins are removed from the map.
In line 24, the map is updated with the updateMap method.
The Polygon Delete Modal
The PolygonDeleteDialog component has the content for the Angular Material modal panel:
export enum MyDialogResult {
ok, cancel, delete
}
export interface DialogMetaData {
polygonName: string;
polygonId: number;
}
@Component({
selector: 'app-polygon-delete-dialog',
templateUrl: './polygon-delete-dialog.component.html',
styleUrls: ['./polygon-delete-dialog.component.scss']
})
export class PolygonDeleteDialogComponent {
dialogResults = MyDialogResult;
isTestData = false;
constructor(public dialogRef: MatDialogRef<PolygonDeleteDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogMetaData) {
this.isTestData = data.polygonId < 1000;
}
cancelClick(): void {
this.dialogRef.close();
}
}
In lines 1-3, the enum for the dialog results is defined.
In lines 4-7, the interface for the dialog metadata is defined.
In lines 9-14, the PolygonDeleteDialogComponent is defined.
In line 15, the dialog results are provided as property.
In line 16, the isTestData property is initialized.
In lines 18-2,1 dialogRef and data properties are set and the isTestData property is set.
In lines 23-25, the cancelClick method is created to close the dialog on the cancel click.
The PolygonDeleteDialog template shows the content of the modal:
<h1 mat-dialog-title *ngIf="!isTestData" i18n="@@DeleteDialog.Header">
Delete Property</h1>
<h1 mat-dialog-title *ngIf="isTestData" i18n="@@DeleteDialog.Header.Testdata">Testdata can not be deleted</h1>
<div mat-dialog-content *ngIf="!isTestData">
<p i18n="@@DeleteDialog.Content">Do you want to delete this property?</p>
<p>{{data.polygonName}}</p>
</div>
<div mat-dialog-content *ngIf="isTestData">
<p i18n="@@DeleteDialog.Content.Testdata">To return please click cancel.</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="cancelClick()" cdkFocusInitial i18n="@@DeleteDialog.Cancel">Cancel</button>
<button mat-button *ngIf="!isTestData" [mat-dialog-close]="dialogResults.delete" i18n="@@DeleteDialog.Delete">Delete</button>
</div>
In lines 1-4, the dialog title is displayed according to the isTestData property.
In lines 5-11, the dialog content is displayed according to the isTestData property.
In lines 13-14, the cancel button is displayed with cdkFocusInitial and the modal is closed with the cancelClick() method.
In lines 15-17, the delete button is displayed with isTestData to check that no test data is deleted and the modal is closed with the DialogResults.delete result value.
The Spring Boot Backend
The REST endpoint is in the CompanySiteController:
@RestController
@RequestMapping("rest/companySite")
public class CompanySiteController {
private static final Logger LOGGER = LoggerFactory.getLogger(CompanySite.class);
private final CompanySiteService companySiteService;
private final EntityDtoMapper entityDtoMapper;
public CompanySiteController(CompanySiteService companySiteService, EntityDtoMapper entityDtoMapper) {
this.companySiteService = companySiteService;
this.entityDtoMapper = entityDtoMapper;
}
//...
@RequestMapping(value = "/id/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CompanySiteDto> getCompanySiteById(@PathVariable("id") Long id) {
CompanySite companySite = this.companySiteService.findCompanySiteById(id)
.orElseThrow(() -> new ResourceNotFoundException(String.format("No CompanySite found for id: %d", id)));
return new ResponseEntity<CompanySiteDto>(this.entityDtoMapper.mapToDto(companySite), HttpStatus.OK);
}
//...
@RequestMapping(value="/id/{companySiteId}/polygon/id/{polygonId}",method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Boolean> deletePolygon(@PathVariable Long companySiteId, @PathVariable Long polygonId) {
LOGGER.info("companySiteId: {} polygonId: {}", companySiteId, polygonId);
return new ResponseEntity<Boolean>(this.companySiteService.deletePolygon(companySiteId, polygonId), HttpStatus.OK);
}
}
In lines 1-9, the CompanySiteController is defined.
In lines 11-14, the REST endpoint getCompanySiteById is defined with the Path Variable id as id.
In lines 15-19, the CompanySiteService is used to get the companySite by id and return it. If it is not found it throws an exception.
In lines 23-27, the REST endpoint deletePolygon is defined with the Path Variable ‘id‘ and Path Variable polygonId.
In lines 30-31, the CompanySiteService is used to delete the polygon of the companySite.
The CompanySiteService reads the CompanySite and deletes the Polygon:
@Transactional
@Service
public class CompanySiteService {
private final CompanySiteRepository companySiteRepository;
private final PolygonRepository polygonRepository;
private final RingRepository ringRepository;
private final LocationRepository locationRepository;
public CompanySiteService(CompanySiteRepository companySiteRepository, PolygonRepository polygonRepository,
RingRepository ringRepository, LocationRepository locationRepository) {
this.companySiteRepository = companySiteRepository;
this.polygonRepository = polygonRepository;
this.ringRepository = ringRepository;
this.locationRepository = locationRepository;
}
//...
public Optional<CompanySite> findCompanySiteById(Long id) {
return Optional.ofNullable(id).flatMap(myId -> this.companySiteRepository.findById(myId));
}
//...
public boolean deletePolygon(Long companySiteId, Long polygonId) {
Optional<CompanySite> companySiteOpt = this.companySiteRepository.findById(companySiteId);
if (companySiteOpt.isEmpty()) {
return false;
}
Optional<Polygon> polygonOpt = companySiteOpt.get().getPolygons().stream()
.filter(myPolygon -> myPolygon.getId() >= 1000L && myPolygon.getId().equals(polygonId)).findFirst();
if (polygonOpt.isEmpty()) {
return false;
}
companySiteOpt.get().getPolygons().remove(polygonOpt.get());
polygonOpt.get().setCompanySite(null);
Set<Ring> ringsToDelete = polygonOpt.get().getRings();
polygonOpt.get().setRings(null);
Set<Location> locationsToDelete = new HashSet<>();
ringsToDelete.forEach(myRing -> {
myRing.setPolygon(null);
locationsToDelete.addAll(myRing.getLocations());
myRing.setLocations(null);
});
locationsToDelete.forEach(myLocation -> {
myLocation.setRing(null);
});
this.locationRepository.deleteAll(locationsToDelete);
this.ringRepository.deleteAll(ringsToDelete);
this.polygonRepository.delete(polygonOpt.get());
return true;
}
}
In lines 1-17, the CompanySiteService is defined.
In lines 19-23, the method findBySiteId retrieves the CompanySite with the CompanySiteRepository and uses map to order the Locations.
In lines 25-30, the deletePolygon method is defined and the CompanySite is retrieved with the repository by id and false is returned if the CompanySite is not found.
In lines 31-36, the Polygon to delete is filtered from the CompanySite and it is checked that there is no test data (id < 1000). If the polygon is not found false is returned.
In line 37, the Polygon is removed from the CompanySite.
In lines 38-49, the references between the entities are removed and the entities to delete are added to the sets to remove.
In lines 50-52, the sets locationsToDelete and ringsToDelete are removed and the polygon is removed.
Conclusion
Developing this project was easier than expected. The TypeScript support of Bing Maps helped a lot. Angular with Material provides a framework that enables fast development. Spring Boot with Jpa and Liquibase is a very good backend framework that saves time. A big thank you to all the teams that provide such tools.