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 mapPinsToAddmapPinsToRemove, 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.