DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How To Build Web Service Using Spring Boot 2.x
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 4)
  • Develop a Secure CRUD Application Using Angular and Spring Boot
  • How To Build Self-Hosted RSS Feed Reader Using Spring Boot and Redis

Trending

  • Java's Quiet Revolution: Thriving in the Serverless Kubernetes Era
  • How to Build Local LLM RAG Apps With Ollama, DeepSeek-R1, and SingleStore
  • Fixing Common Oracle Database Problems
  • Integrating Security as Code: A Necessity for DevSecOps
  1. DZone
  2. Coding
  3. Frameworks
  4. Bing Maps With Angular in a Spring Boot Application

Bing Maps With Angular in a Spring Boot Application

How to integrate Bing Maps with Angular to show different site properties at different points in time with a Spring Boot backend.

By 
Sven Loesekann user avatar
Sven Loesekann
·
Apr. 05, 21 · Tutorial
Likes (7)
Comment
Save
Tweet
Share
8.7K Views

Join the DZone community and get the full member experience.

Join For Free

The AngularAndSpringWithMaps project shows how to integrate Bing Maps, Angular, and Spring Boot with a Gradle build. The property data of the sites is stored with JPA in H2/PostgreSQL databases. 

The purpose of the AngularAndSpringWithMaps project is to show the site properties at different points in time. To choose the site and then choose the time and have the site properties displayed in a map. New properties can be added and deleted on the map and then persisted. This article will show how to store and display company sites.

The Backend

The site properties are stored in these Entities:

  • CompanySite -> the site that contains the properties at the location for the year. All the necessary contained entities are loaded.
  • Polygon -> a property at the CompanySite with multiple Rings; a Polygon can contain holes.
  • Ring -> a ring of location points that makes up a property or a hole in a property (could be a lake).
  • Location -> a location point of a ring.

The initial test data is provided by Liquibase with the files in this directory. For information about setting up Liquibase with Spring Boot, these articles (article 1, article 2) help a lot. For loading the initial data this article can help.

The CompanySiteRepository to get the site for the year:

Java
 




xxxxxxxxxx
1


 
1
public interface CompanySiteRepository extends JpaRepository<CompanySite, Long>{
2
    @Query("select cs from CompanySite cs where lower(cs.title) like %:title% and cs.atDate >= :from and cs.atDate <= :to")
3
    List<CompanySite> findByTitleFromTo(@Param("title") String title, @Param("from") LocalDate from,  @Param("to") LocalDate to);   
4
}


In lines 2-3, the companySite with a name containing the name string and the year is selected.

The company sites are loaded with the CompanySiteService: 

Java
 




xxxxxxxxxx
1
19


 
1
public List<CompanySite> findCompanySiteByTitleAndYear(String title, Long year) {
2
    if (title == null || title.length() < 2) {
3
        return List.of();
4
    }
5
    LocalDate beginOfYear = LocalDate.of(year.intValue(), 1, 1);
6
    LocalDate endOfYear = LocalDate.of(year.intValue(), 12, 31);
7
    return this.companySiteRepository
8
      .findByTitleFromTo(title.toLowerCase(), beginOfYear, endOfYear)
9
      .stream().peek(companySite -> this.orderCompanySite(companySite))
10
      .collect(Collectors.toList());
11
}
12

          
13
private CompanySite orderCompanySite(CompanySite companySite) {
14
    companySite.getPolygons()
15
        .forEach(polygon -> polygon.getRings()
16
            .forEach(ring -> ring.setLocations(new LinkedHashSet<Location>(ring.getLocations().stream()                                                                           .sorted((Location l1, Location l2) -> l1.getOrderId().compareTo(l2.getOrderId()))
17
        .collect(Collectors.toList())))));
18
    return companySite;
19
}


In lines 2-6, empty titles or titles shorter than two characters are filtered out and the beginning and end of the year are set.

In lines 7-10, the companySite are selected from the DB and the entity tree is ordered with the orderCompanySite method.

In lines 13-18, the companySite entity locations are ordered to get the property borders. This is done in code because the locations are selected by JPA.

The REST endpoint is implemented in CompanySiteController:

Java
 




xxxxxxxxxx
1
22


 
1
@RestController
2
@RequestMapping("rest/companySite")
3
public class CompanySiteController {
4
    private static final Logger LOGGER = LoggerFactory.getLogger(CompanySite.class);
5
    private final CompanySiteService companySiteService;
6

          
7
    public CompanySiteController(CompanySiteService companySiteService) {
8
        this.companySiteService = companySiteService;
9
    }
10

          
11
    @RequestMapping(value = "/title/{title}/year/{year}", 
12
     method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
13
    public ResponseEntity<List<CompanySiteDto>> getCompanySiteByTitle(
14
      @PathVariable("title") String title, @PathVariable("year") Long year) {
15
        List<CompanySiteDto> companySiteDtos = this.companySiteService.
16
          findCompanySiteByTitleAndYear(title, year)
17
                .stream().map(companySite -> 
18
                   EntityDtoMapper.mapToDto(companySite))
19
          .collect(Collectors.toList());
20
        return new ResponseEntity<List<CompanySiteDto>>(
21
          companySiteDtos, HttpStatus.OK);
22
    }
23
  ...
24
}


In lines 1-3, the base REST endpoint is defined.

In lines 5-9, the CompanySiteService is injected with the constructor.

In lines 11-14, the REST endpoint to get a company site by title and year is defined. The variables are defined in the @RequestMapping and read in parameters with @PathVariable.

In lines 15-19, the companySites are read with the CompanySiteService and then mapped in a stream with the EntityToDtoMapper to dtos for the front-end.

In lines 20-21, the result is returned in a ResponseEntity with the HTTP status ok.

The Front-End

The documentation for Bing Maps can be found here. Bing provides types for the API. Bing Maps support the data structures polygon/ring/location that are supported in the backend. For the components Material is used.

In the package.json file, the libraries are built for Ivy postinstall and the types are added:

JSON
 




xxxxxxxxxx
1
22


 
1
"scripts": {
2
    "ng": "ng",
3
    "start": "ng serve --hmr --proxy-config proxy.conf.js",
4
    "build": "ng build --prod --localize",
5
    "test": "ng test --browsers ChromeHeadless --watch=false",
6
    "test-chromium": "ng test --browsers ChromiumHeadless --watch=false",
7
    "test-local": "ng test --browsers Chromium --watch=true",
8
    "lint": "ng lint",
9
    "e2e": "ng e2e",
10
    "postinstall": "ngcc",
11
    "prebuild": "rimraf ./dist && mkdirp ./dist",
12
    "postbuild": "npm run deploy",
13
    "predeploy": "rimraf ../../main/resources/static/*",
14
    "deploy": "cpx 'base/*' dist/testproject/ && cpx 'dist/testproject/**' ../../main/resources/static/"
15
  },
16
  "private": true,
17
  "dependencies": {
18
    ...
19
    "bingmaps": "^2.0.3",
20
    ...
21
}


In line 10, the libraries are built for Ivy after the install. 

In line 19, the types for bingmaps are added for TypeScript.

The map is displayed in the CompanySite component with this template:

HTML
 




xxxxxxxxxx
1
39


 
1
<div>
2
    <form class="example-form" [formGroup]="componentForm">
3
        <div class="form-container form-container-input">
4
            <div>
5
                <div>
6
                    <mat-form-field class="example-full-width"> 
7
                        <input type="text" placeholder="Pick one" 
8
                               aria-label="Number" matInput                              
9
                               formControlName="{{COMPANY_SITE}}" 
10
                               [matAutocomplete]="auto"> 
11
                        <mat-autocomplete autoActiveFirstOption
12
                                          #auto="matAutocomplete"
13
                                          [displayWith]="displayTitle"> 
14
                            <mat-option 
15
                             *ngFor="let option of companySiteOptions | async"
16
                                        [value]="option">
17
                                {{option.title}} 
18
                            </mat-option> 
19
                        </mat-autocomplete> 
20
                    </mat-form-field>
21
                </div>
22
                <div>
23
                    <mat-slider class="my-slider" thumbLabel
24
                                [displayWith]="formatLabel" step="10"
25
                                min="1970" max="2020" 
26
                                formControlName="{{SLIDER_YEAR}}">
27
                    </mat-slider>
28
                    <span class="my-year"
29
                          i18n="@@companysite.slideryear">
30
                      Year: {{ componentForm.get('sliderYear').value }}
31
                  </span>
32
                </div>
33
            </div>          
34
        </div>
35
        ...
36
    </form>
37
    <div #bingMap class="bing-map-container"></div>
38
</div>


In line 2, the formGroup componentForm is set as reactive form for the component.

In lines 6-20, a <mat-form-field> wraps a <input> with a <mat-autocomplete> feature. The <input> is connected to the formControl companySite. The <mat-autocomplete> is connected to the input with #auto and the matAutocomplete property. The <mat-options> of the <mat-autocomplete> show the titles of the companySiteOptions observable. That is all that is needed for a typeahead with Material components.

In lines 23-26, <mat-slider> is added to select the year it starts with 1970 and ends with 2020 and is connected to the formControl sliderYear.

In lines 27-30, a <span> is added to display the year of the slider.

In line 36, a <div> is added with #bingMap to provide the container that displays the map.

The CompanySite component is set up like this:

TypeScript
 




xxxxxxxxxx
1
34


 
1
Component({
2
    selector: 'app-company-site',
3
    templateUrl: './company-site.component.html',
4
    styleUrls: ['./company-site.component.scss']
5
})
6
export class CompanySiteComponent implements OnInit, AfterViewInit, OnDestroy {
7
    @ViewChild('bingMap')
8
    bingMapContainer: ElementRef;
9

          
10
    newLocations: NewLocation[] = [];
11
    map: Microsoft.Maps.Map = null;
12
    resetInProgress = false;
13

          
14
    companySiteOptions: Observable<CompanySite[]>;
15
    componentForm = this.formBuilder.group({
16
        companySite: ['Finkenwerder', Validators.required],
17
        sliderYear: [2020],
18
        property: ['add Property', Validators.required]
19
    });
20

          
21
    readonly COMPANY_SITE = 'companySite';
22
    readonly SLIDER_YEAR = 'sliderYear';
23
    readonly PROPERTY = 'property';
24
    private mainConfiguration: MainConfiguration = null;
25
    private readonly containerInitSubject = new Subject<Container>();
26
    private containerInitSubjectSubscription: Subscription;
27
    private companySiteSubscription: Subscription;
28
    private sliderYearSubscription: Subscription;
29

          
30
    constructor(public dialog: MatDialog,
31
        private formBuilder: FormBuilder,
32
        private bingMapsService: BingMapsService,
33
        private companySiteService: CompanySiteService,
34
        private configurationService: ConfigurationService) { }
35
  ...
36
}


In lines 1-6, the Component is defined with the needed live cycle callback interfaces.

In lines 7-8, the elementRef for the map is injected.

In line 11, the map property is defined.

In line 14, the observable for the autocomplete input is defined.

In lines 15-19, the reactive form with initial values is created. 

In line 25, the containerInitSubject is created that emits when the initial values are available.

In lines 26-28, the hot subscriptions are defined to be unsubscribed in the onDestroy.

In lines 30-34, the needed elements get injected in the constructor. The FormBuilder has been used to create the reactive form.

To read the companySite data the CompanySiteService is used: 

TypeScript
 




xxxxxxxxxx
1
12


 
1
@Injectable()
2
export class CompanySiteService {
3

          
4
  constructor(private http: HttpClient) { }
5
  //...
6
  public findByTitleAndYear(title: string, year: number):
7
    Observable<CompanySite[]> {
8
    return this.http.
9
        get<CompanySite[]>(`/rest/companySite/title/${title}/year/${year}`);
10
  }
11
  //...
12
}


This is a simple service that uses the HttpClient to get the matching CompanySites for the title and year.

The BingMapsService is used to load the newest version and initialize the mapcontrol component from Bing:

TypeScript
 




x


 
1
@Injectable({
2
  providedIn: 'root',
3
})
4
export class BingMapsService {
5
    private initialized = false;
6

          
7
    public initialize(apiKey: string): Observable<boolean> {
8
        if (this.initialized) {
9
            return of(true);
10
        }
11
        const callBackName = `bingmapsLib${new Date().getMilliseconds()}`;
12
        const scriptUrl = `https://www.bing.com/api/maps/mapcontrol?callback=${callBackName}&key=${apiKey}`;
13
        const script = document.createElement('script');
14
        script.type = 'text/javascript';
15
        script.async = true;
16
        script.defer = true;
17
        script.src = scriptUrl;
18
        script.charset = 'utf-8';
19
        document.head.appendChild(script);
20
        const scriptPromise = new Promise<boolean>((resolve, reject) => {
21
            (window)[callBackName] = () => {
22
                this.initialized = true;
23
                resolve(true);
24
            };
25
            script.onerror = (error: Event) => { 
26
              console.log(error); 
27
              reject(false); 
28
            };
29
        });
30
        return from(scriptPromise);
31
    }
32
}


In lines 1-5, the singleton service is defined with the initialized property set to false.

In lines 8-10, it checks if Bing Maps has already been initialized.

In lines 11-12, a unique callback name for the successful load of Bing Maps is created and the URL with the name and apiKey is set.

In lines 13-19, the new script tag in the document head is created.

In lines 20-29, the scriptPromise is setup. The Bing Maps callback resolves and sets initialized to true. The onerror script callback logs and rejects.

In line 30, an observable is created from the promise and gets returned.

The CompanySite is initialized like this: 

TypeScript
 




xxxxxxxxxx
1
91
101


 
1
constructor(public dialog: MatDialog,
2
    private formBuilder: FormBuilder,
3
    private bingMapsService: BingMapsService,
4
    private companySiteService: CompanySiteService,
5
    private configurationService: ConfigurationService) { }
6

          
7
ngOnInit(): void {
8
    this.companySiteOptions = this.componentForm.valueChanges.pipe(
9
        debounceTime(300),
10
        switchMap(() =>
11
            iif(() => (!this.getCompanySiteTitle() 
12
                       || this.getCompanySiteTitle().length < 3
13
                       || !this.componentForm.get(this.SLIDER_YEAR).value),
14
                of<CompanySite[]>([]),
15
                    this.companySiteService
16
                .findByTitleAndYear(this.getCompanySiteTitle(),
17
                    this.componentForm.get(this.SLIDER_YEAR).value))));
18
        this.companySiteSubscription = 
19
          this.componentForm.controls[this.COMPANY_SITE].valueChanges
20
            .pipe(debounceTime(500),
21
                filter(companySite => typeof companySite === 'string'),
22
                switchMap(companySite =>
23
                    this.companySiteService.
24
                       findByTitleAndYear((companySite as CompanySite).title,
25
                    this.componentForm.
26
                       controls[this.SLIDER_YEAR].value as number)),
27
                filter(companySite => companySite?.length 
28
                       && companySite?.length > 0))
29
            .subscribe(companySite => this.updateMap(companySite[0]));
30
        this.sliderYearSubscription =
31
          this.componentForm.controls[this.SLIDER_YEAR].valueChanges
32
            .pipe(debounceTime(500),
33
                filter(year => !(typeof this.componentForm.
34
                       get(this.COMPANY_SITE).value === 'string')),
35
                switchMap(year => this.companySiteService.
36
                       findByTitleAndYear(this.getCompanySiteTitle(), 
37
                       year as number)),
38
                filter(companySite => companySite?.length 
39
                       && companySite.length > 0 
40
                       && companySite[0].polygons.length > 0))
41
            .subscribe(companySite => this.updateMap(companySite[0]));
42
        forkJoin([this.configurationService.importConfiguration(),
43
        this.companySiteService.findByTitleAndYear(this.getCompanySiteTitle(),                       
44
            this.componentForm.
45
                       controls[this.SLIDER_YEAR].value)])
46
                       .subscribe(values => {
47
                this.mainConfiguration = values[0];
48
                this.containerInitSubject.next({ companySite: values[1][0],
49
                       mainConfiguration: values[0] } as Container);
50
    });
51
}
52

          
53
ngAfterViewInit(): void {
54
    this.containerInitSubjectSubscription = 
55
        this.containerInitSubject
56
            .pipe(filter(myContainer => !!myContainer 
57
                         && !!myContainer.companySite
58
                && !!myContainer.companySite.polygons 
59
                         && !!myContainer.mainConfiguration),
60
                flatMap(myContainer => this.bingMapsService
61
                        .initialize(myContainer.mainConfiguration.mapKey)
62
                    .pipe(flatMap(() => of(myContainer)))))
63
            .subscribe(container => {
64
                const mapOptions = container.companySite.polygons.length < 1 ?
65
                    {} as Microsoft.Maps.IMapLoadOptions
66
                    : {
67
                        center: new Microsoft.Maps.Location(
68
                         container.companySite.
69
                          polygons[0].centerLocation.latitude,
70
                            container.companySite.
71
                          polygons[0].centerLocation.longitude)
72
                    } as Microsoft.Maps.IMapLoadOptions;
73
                this.map = new Microsoft.Maps.Map(
74
                  this.bingMapContainer.nativeElement as HTMLElement,
75
                  mapOptions);
76
                this.componentForm.
77
                controls[this.COMPANY_SITE].setValue(container.companySite);
78
                container.companySite.polygons.
79
                    forEach(polygon => this.addPolygon(polygon));
80
                Microsoft.Maps.Events.
81
                    addHandler(this.map, 'click', (e) => this.onMapClick(e));
82
        });
83
}
84

          
85
ngOnDestroy(): void {
86
    this.containerInitSubject.complete();
87
    this.containerInitSubjectSubscription.unsubscribe();
88
    this.companySiteSubscription.unsubscribe();
89
    this.sliderYearSubscription.unsubscribe();
90
    this.map.dispose();
91
}
92
private updateMap(companySite: CompanySite): void {
93
    if (this.map) {
94
        this.map.setOptions({
95
            center: new Microsoft.Maps.Location(
96
              companySite.polygons[0].centerLocation.latitude,
97
                    companySite.polygons[0].centerLocation.longitude),
98
        } as Microsoft.Maps.IMapLoadOptions);
99
        this.map.entities.clear();
100
        companySite.polygons.forEach(polygon => this.addPolygon(polygon));
101
    }
102
}


ngOnInit Method

In lines 8-17, the mat-autocomplete is the service call that gets the options for the auto complete. The reactive form refreshes the options on value change and debounces it. Then there is a minimum validity check and either the matching companySites are retrieved from the server or the empty observable is returned.

In lines 18-29, the newly selected companySites of the auto complete are debounced and filtered for selected sites. Then they are retrieved from the server and filtered for empty responses. Then the updateMap method is called to display the new site on the map.

In lines 30-41, the values of the mat-slider component are debounced and there is a filter that ensures that it only works if a companySite is selected. Then the companySites for the selected year are retrieved from the server and are filtered for empty responses. Then the updateMap method is called to display the site at the selected year on the map.

In lines 42-50, forkJoin is used to send the requests for the MainConfigration with the Bing Maps key and the initial CompanySite concurrently. The responses come in an array where the mainConfiguration is in first element and the companySites in the second. They are set in the property and in the Container object. The Container object is send with next in the containerInitSubject.

ngAfterViewInit Method

In the ngAfterViewInit method, the the map of bing is initialized because the view has to be available for it. In lines 54-59, the containerInitSubject is used to make sure the companySite and the configuration is send. A validation check is done. Then flatMap is used with the BingMapsService is used to initialize the map. 

In lines 64-73, the IMapLoadOptions interface is created with the current center for the new Map.

In lines 73-75, the new map is created on the bingContainer property with the IMapLoadOptions.

In line 77, the companySite is set in the form.

In lines 78-79, the the polygons are updated for the map.

In lines 80-81, the listener for clicks on the map is set. 

ngOnDestroy Method

In lines 86-90, the containerInitSubject gets cleared with next and the hot subscribtions are unsubscribed. The the map gets disposed.

updateMap Method

In line 93, it is checked that the map is initialized. 

In lines 94-98, the IMapLoadOptions of the Bing Map are updated.

In lines 99-100, the entities of the map are cleared and the polygons are recreated.

Conclusion

Spring Boot with JPA and H2/Postgresql makes storing map data easy. 

Bing Maps provides nice types for the TypeScript interface. That makes it much easier to use the API and with the types the API is easy to understand. Integrating Bing Maps was pretty straight forward. It needs to be added in the ngAfterViewInit because the ElementRef has to be available. The Angular Material components are easy to use and to integrate with RxJS and reactive forms. 

The next article will describe how to to add and remove properties to a companySite by clicking on the map.

Finally

The testdata for the year 2020 shows the property of a factory where the product of the article image is made.

Spring Framework Bing Spring Boot Property (programming) AngularJS Database application Test data

Opinions expressed by DZone contributors are their own.

Related

  • How To Build Web Service Using Spring Boot 2.x
  • Best Performance Practices for Hibernate 5 and Spring Boot 2 (Part 4)
  • Develop a Secure CRUD Application Using Angular and Spring Boot
  • How To Build Self-Hosted RSS Feed Reader Using Spring Boot and Redis

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!