This is the second part of a series. In the first part, the setup of the AngularAndSpringWithMaps project was described. In this part, weโll describe adding and reseting properties.
Create a New Property at a companySite
To create a new property the user can click on the map. The first click is the center of the property and the following click defines the shape of the property. The locations are shown on the map with icons in the CompanySiteComponent:
ngAfterViewInit(): void {
//...
Microsoft.Maps.Events.addHandler(this.map, 'click', (e) => this.onMapClick(e));
//...
}
private onMapClick(e: Microsoft.Maps.IMouseEventArgs | Microsoft.Maps.IMapTypeChangeEventArgs): void {
if ((e as Microsoft.Maps.IMouseEventArgs).location) {
const myLocation = { id: this.newLocations.length + 1,
location: (e as Microsoft.Maps.IMouseEventArgs).location, selected: true };
this.newLocations.push(myLocation);
this.map.entities.push(new Microsoft.Maps.Pushpin(myLocation.location, {
title: '' + myLocation.id,
icon: 'https://bingmapsisdk.blob.core.windows.net/isdksamples/defaultPushpin.png',
anchor: new Microsoft.Maps.Point(12, 39)
}));
}
}
In line 3, the onMapClick
method is registered in ngAfterViewInit
with Bing Maps.
In line 10, we check that the event has a location property.
In lines 11-13, a new location is created.
In line 14, the new location is added to the newLocation
array of the component.
In lines 15-20, the new location is wrapped in a PushPin
and the PushPin
options are added.
Editing New Locations
So that we can edit the new locations, they are displayed in a Material list. They can be enabled and disabled to correct the borders. Disabled locations disappear on the map. The list and the buttons are on the CompanySite template:
<div class="form-container form-container-scroll">
<mat-selection-list class="new-locations" (selectionChange)="newLocationsChanged($event)">
<mat-list-option *ngFor="let newLocation of newLocations" [selected]="newLocation.selected">
{{newLocation.id}}: {{newLocation.location.latitude}} - {{newLocation.location.longitude}}
</mat-list-option>
</mat-selection-list>
</div>
In lines 2-3, the Material list is created and the newLocationsChanged(...)
method is added.
In lines 4-9, the Material list options are created from the newLocations
property of the CompanySite
component. The selected property of the locations in the newLocation
array is used for the checkbox.
The newLocationsChanged
method is on the CompanySite component:
newLocationsChanged(e: MatSelectionListChange): void {
for (let i = 0; i < e.source.options.length; i++) {
const myOption = e.source.options.toArray()[i];
if (e.options.includes(myOption)) {
this.newLocations[i].selected = e.options[e.options.indexOf(myOption)].selected;
}
}
this.updateMapPushPins();
}
//...
private updateMapPushPins(): void {
const mapPinsToAdd: Microsoft.Maps.Pushpin[] = [];
const mapPinsToRemove: Microsoft.Maps.Pushpin[] = [];
const mapPins: Microsoft.Maps.Pushpin[] = [];
for (let i = 0; i < this.map.entities.getLength(); i++) {
if (typeof (this.map.entities.get(i) as Microsoft.Maps.Pushpin).getIcon === 'function'
&& typeof (this.map.entities.get(i) as Microsoft.Maps.Pushpin).getTitle === 'function') {
mapPins.push(this.map.entities.get(i) as Microsoft.Maps.Pushpin);
}
}
if (this.newLocations.length === 0) {
mapPins.forEach(myPin => mapPinsToRemove.push(myPin));
} else {
this.newLocations.forEach(newLocation => {
const myMapPin = mapPins.filter(mapPin => mapPin.getLocation().latitude === newLocation.location.latitude
&& mapPin.getLocation().longitude === newLocation.location.longitude);
if (!!myMapPin && myMapPin.length > 0 && !newLocation.selected) {
mapPinsToRemove.push(myMapPin[0]);
}
if (!myMapPin || myMapPin.length === 0 && newLocation.selected) {
mapPinsToAdd.push(new Microsoft.Maps.Pushpin(newLocation.location, {
title: '' + newLocation.id,
icon: 'https://bingmapsisdk.blob.core.windows.net/isdksamples/defaultPushpin.png',
anchor: new Microsoft.Maps.Point(12, 39)
}));
}
});
}
mapPinsToRemove.forEach(myPin => this.map.entities.remove(myPin));
mapPinsToAdd.forEach(myPin => this.map.entities.add(myPin));
}
In lines 2-8, the newLocations
array is updated with the selected values of the locations of the list, to add changed selections.
In line 9, the updateMapPushPins
method is called.
In lines 13-15, the arrays mapPinsToAdd
, mapPinsToRemove
, and mapPins
are created.
In lines 16-24, the mapPins
array is filled with all the pins of the Bing Map.
In lines 25-26, we check if the newLocations
array is empty, then the all pins of the map are added to the mapPinsToRemove
array.
In lines 29-33, the mapPin
for the currentLocation
array entry is filtered out.
In lines 34-36, we check that mapPin
is up and running and deselected โ we then add it to the mapPinsToRemove
array.
In lines 37-44, we check if mapPin
is non-existent and has been selected โ then we create it with the newLocation.location
and add it to the mapPinsToAdd
array.
In line 47, the pins in the pinsToRemove
array are removed from the map.
In line 48, the pins in the pinsToAdd
array are added to the map.
The buttons for the new property are in the template:
<div *ngIf="newLocations.length > 0" class=" form-container-buttons">
<button mat-raised-button color="primary" (click)="upsertCompanySite()"
*ngIf="newLocationsValid()" i18n="@@companySite.add">Add</button>
<button mat-raised-button (click)="clearMapPins()" i18n="@@companySite.clear">Clear</button>
<button mat-raised-button (click)="resetDb()" [disabled]="resetInProgress"
i18n="@@companySite.resetDb">Reset DB</button>
</div>
In line 1, the div is shown if the newLocations
are not empty.
In lines 2-3, the button is added to add the newLocations
.
In lines 4-5, we add the button for removing mapPins
.
In lines 6-7, the button to reset the DB is added โ the resetInProgress
property disables it if DB reset is in progress.
The methods are in the component:
upsertCompanySite(): void {
if (typeof this.componentForm.get(this.COMPANY_SITE).value === 'string') {
console.log('should create new company site: ' + this.componentForm.get(this.COMPANY_SITE).value);
} else {
const myCompanySite = this.componentForm.controls[this.COMPANY_SITE].value as CompanySite;
const newRing = {
primaryRing: true,
locations: this.newLocations.filter(myNewLocation => myNewLocation !== this.newLocations[0]
&& myNewLocation.selected)
.map(myNewLocation => ({
latitude: myNewLocation.location.latitude,
longitude: myNewLocation.location.longitude
} as Location))
} as Ring;
const newPolygon = {
borderColor: '#00FFFF', fillColor: '#FFFFFF', latitude: this.newLocations[0].location.latitude,
longitude: this.newLocations[0].location.longitude,
title: this.componentForm.controls[this.PROPERTY].value, rings: [newRing]
} as Polygon;
myCompanySite.polygons.push(newPolygon); this.companySiteService.upsertCompanySite(myCompanySite).subscribe(newCompanySite => { this.componentForm.controls[this.COMPANY_SITE].setValue(newCompanySite);
this.clearMapPins();
this.updateMap(newCompanySite);
});
}
}
In lines 1-5, we checked that the companySite
has already stored to the database.
In lines 6-7, the companySite
is set.
In lines 8-24, the new ring for Bing Maps is created and the new polygon for Bing Maps is created. The first newLocation
is the polygon center and only the selected mapPins
are added to the ring.
In line 25 ,the new polygon is added to the companySite
.
In lines 26- 32, the companySite
is sent to the backend to be stored. It then gets returned and the companySite
is updated, the mapPins
are cleared, and the new polygon(property)
is displayed on the map.
In lines 35-40, the clearMapPins
method is called to empty the newLocaltions
array and updates the map with the clearMapPins()
method.
In lines 42-43, the resetDb
method is declared and the resetInProgress
property is set to true to prevent it from being called more than once (disable in the template).
In line 44, the resetDb
method is called on the companySite
service to reset the database.
In lines 45-49, the test companySite
is reloaded.
In lines 50-55, the test companySite
is set in the reactive form. The mapPins
are cleared. The map is updated with the test companySite
and the resetInProgress
property is set to false again.
This is the CompanySiteService that is used:
@Injectable()
export class CompanySiteService {
constructor(private http: HttpClient) { }
//...
public upsertCompanySite(companySite: CompanySite): Observable<CompanySite> {
return this.http.post<CompanySite>('/rest/companySite', companySite);
}
public resetDb(): Observable<boolean> {
return this.http.delete<boolean>('/rest/companySite/reset');
}
//...
}
In lines 1-4, the CompanySite
service is defined. It is a local service for the map module.
In lines 6-9, the upsertCompanySite
method is defined to create/update companySites
.
In lines 11-13, the resetDb
method is defined to reset the database to the test data. It returns only a boolean.
In lines 15-18, the delete
Polygon method is defined to delete a polygon in a companySite
. The companySiteId
and the polygonId
are URL parameters.
Storing the New Property
The companySite
REST endpoint is created in the backend with 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(method=RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<CompanySiteDto> upsertCompanySite(@RequestBody CompanySiteDto companySiteDto) {
CompanySite companySite = this.companySiteService.findCompanySiteById(companySiteDto.getId()).orElse(new CompanySite());
companySite = this.companySiteService.upsertCompanySite(this.entityDtoMapper.mapToEntity(companySiteDto, companySite));
return new ResponseEntity<CompanySiteDto>(this.entityDtoMapper.mapToDto(companySite), HttpStatus.OK);
}
@RequestMapping(value="/reset",method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Boolean> resetDb() {
return new ResponseEntity<Boolean>(this.companySiteService.resetDb(), HttpStatus.OK);
}
//...
}
In lines 2-11, we define the controller for the REST endpoint for adding properties and reseting the properties.
In lines 13-16, the POST endpoint for the companySite
is created. The companySite
is put in the companySiteDto
.
In lines 17-19, companyService
is used to find an existing CompanySite
in the DB or returns a new CompanySite
.
In lines 20-22, the companyService
is used to store the values of the companySiteDto
in the companySite
entity with the EntityToDtoMapper. The EntityToDtoMapper is a class that maps a DTO in an entity and entity to DTO.
In lines 23-25, the ResponseEntity
is created with the EntityToDtoMapper to map the companySite
entity to the companySiteDto
.
In lines 28-30, the reset endpoint for reseting the DB is created with a boolean as the result.
In lines 31-32, the companySiteService
is used to reset the DB and return the ResponseEntity
.
The companySite
is stored/updated in the DB with the CompanySiteService:
@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 CompanySite upsertCompanySite(CompanySite companySite) {
return this.companySiteRepository.save(companySite);
}
//...
public boolean resetDb() {
List<CompanySite> allCompanySites = this.companySiteRepository.findAll();
List<CompanySite> companySitesToDelete = allCompanySites.stream()
.filter(companySite -> companySite.getId() >= 1000).collect(Collectors.toList());
List<Polygon> allPolygons = this.polygonRepository.findAll();
List<Polygon> polygonsToDelete = allPolygons.stream().filter(polygon -> polygon.getId() >= 1000)
.collect(Collectors.toList());
allCompanySites.forEach(myCompanySite -> myCompanySite.getPolygons().removeAll(polygonsToDelete));
List<Ring> allRings = this.ringRepository.findAll();
List<Ring> ringsToDelete = allRings.stream().filter(ring -> ring.getId() >= 1000).collect(Collectors.toList());
allPolygons.forEach(myPolygon -> myPolygon.getRings().removeAll(ringsToDelete));
List<Location> allLocations = this.locationRepository.findAll();
List<Location> locationsToDelete = allLocations.stream().filter(location -> location.getId() >= 1000)
.collect(Collectors.toList());
locationsToDelete.forEach(myLocaton -> myLocaton.setRing(null));
allRings.forEach(myRing -> myRing.getLocations().removeAll(locationsToDelete));
this.locationRepository.deleteAll(locationsToDelete);
this.ringRepository.deleteAll(ringsToDelete);
this.polygonRepository.deleteAll(polygonsToDelete);
this.companySiteRepository.deleteAll(companySitesToDelete);
return true;
}
//...
}
In lines 1-17, the transactional CompanySiteService
is created with the dependencies that are needed. Spring injects the dependencies automatically in the constructor.
In lines 17-19, the upsetCompanySite
method is created to store the companySite
to the DB with a Spring JPA Repository. The @OneToMany
annotations of the entities have cascade = CascadeType.ALL
and orphanRemoval = true
. That makes sure that entities that are no longer in the entity tree are removed from the DB. To put the Locations
in a useful order the method orderCompanySite
is used to sort the returned entity.
In lines 17-21, the upsetCompanySite
method is created to store the companySite
to the DB with a Spring JPA Repository. The @OneToMany
annotations of the entities have cascade = CascadeType.ALL
and orphanRemoval = true
. That makes sure that entities that are no longer in the entity tree are removed from the DB. To put the Locations
in a useful order the method orderCompanySite
is used to sort the returned entity.
In line 21, the resetDb
method is created.
In lines 21-44, something simple happens. The primary keys of the entities are created by a database sequence starting with 1000 and the test data primary keys are smaller than 1000. From the CompanySite Entity down all related entities with a primary key starting from 1000 are removed. The entities to remove are stored in lists.
In lines 42-44, the lists of the entities to remove can be deleted on the DB with JPA because the dependencies are removed.
Conclusion
The TypeScript support of Bing Maps makes working with the API much easier. Angular with the Material components enables fast development of features to edit and store the shapes on the map. Spring Boot with REST support and JPA enables easy development of the backend to store the shapes in the database.
In the next article, a property gets removed from the map with a modal panel to confirm the deletion.