Skip to content

Commit ec6940a

Browse files
authored
Merge pull request #3540 from kshepherd/geospatial-maps-main
Geospatial maps for item pages, search, browse
2 parents 9c23b4c + 6f7f545 commit ec6940a

File tree

44 files changed

+1707
-6
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1707
-6
lines changed

angular.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@
5858
"input": "src/themes/dspace/styles/theme.scss",
5959
"inject": false,
6060
"bundleName": "dspace-theme"
61-
}
61+
},
62+
"node_modules/leaflet/dist/leaflet.css",
63+
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
64+
"node_modules/leaflet.markercluster/dist/MarkerCluster.css"
6265
],
6366
"scripts": [],
6467
"baseHref": "/"

config/config.example.yml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,6 @@ notifyMetrics:
546546
config: 'NOTIFY.outgoing.delivered'
547547
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
548548

549-
550549
# Live Region configuration
551550
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
552551
# Live regions are perceivable regions of a web page that are typically updated as a
@@ -560,3 +559,37 @@ liveRegion:
560559
messageTimeOutDurationMs: 30000
561560
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
562561
isVisible: false
562+
563+
# Geospatial Map display options
564+
geospatialMapViewer:
565+
# Which fields to use for parsing as geospatial points in search maps
566+
# (note, the item page field component allows any field(s) to be used
567+
# and is set as an input when declaring the component)
568+
spatialMetadataFields:
569+
- 'dcterms.spatial'
570+
# Which discovery configuration to use for 'geospatial search', used
571+
# in the browse map
572+
spatialFacetDiscoveryConfiguration: 'geospatial'
573+
# Which filter / facet name to use for faceted geospatial search
574+
# used in the browse map
575+
spatialPointFilterName: 'point'
576+
# Whether item page geospatial metadata should be displayed
577+
# (assumes they are wrapped in a test for this config in the template as
578+
# per the default templates supplied with DSpace for untyped-item and publication)
579+
enableItemPageFields: false
580+
# Whether the browse map should be enabled and included in the browse menu
581+
enableBrowseMap: false
582+
# Whether a 'map view' mode should be included alongside list and grid views
583+
# in search result pages
584+
enableSearchViewMode: false
585+
# The tile provider(s) to use for the map tiles drawn in the leaflet maps.
586+
# (see https://leaflet-extras.github.io/leaflet-providers/preview/) for a full list
587+
tileProviders:
588+
- 'OpenStreetMap.Mapnik'
589+
# Starting centre point for the map, as lat and lng coordinates. This is useful
590+
# to set the centre of the map when the map is first loaded and if there are no
591+
# points, shapes or markers to display.
592+
# Defaults to the centre of Istanbul
593+
defaultCentrePoint:
594+
lat: 41.015137
595+
lng: 28.979530

package-lock.json

Lines changed: 27 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@
114114
"@ngrx/store": "^18.1.1",
115115
"@ngx-translate/core": "^16.0.3",
116116
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
117+
"@terraformer/wkt": "^2.2.1",
117118
"altcha": "^0.9.0",
118119
"angulartics2": "^12.2.0",
119120
"axios": "^1.7.9",
@@ -140,6 +141,9 @@
140141
"json5": "^2.2.3",
141142
"jsonschema": "1.5.0",
142143
"jwt-decode": "^3.1.2",
144+
"leaflet": "^1.9.4",
145+
"leaflet-providers": "^2.0.0",
146+
"leaflet.markercluster": "^1.5.3",
143147
"lodash": "^4.17.21",
144148
"lru-cache": "^7.14.1",
145149
"markdown-it": "^13.0.1",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<div class="container">
2+
<h1>{{ 'browse.metadata.map' | translate }}</h1>
3+
@if (isPlatformBrowser(platformId)) {
4+
<ds-geospatial-map [facetValues]="facetValues$"
5+
[currentScope]="this.scope$|async"
6+
[layout]="'browse'"
7+
style="width: 100%;">
8+
</ds-geospatial-map>
9+
}
10+
</div>
11+

src/app/browse-by/browse-by-geospatial-data/browse-by-geospatial-data.component.scss

Whitespace-only changes.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
waitForAsync,
6+
} from '@angular/core/testing';
7+
import { ActivatedRoute } from '@angular/router';
8+
import { StoreModule } from '@ngrx/store';
9+
import { TranslateModule } from '@ngx-translate/core';
10+
import { of as observableOf } from 'rxjs';
11+
12+
import { environment } from '../../../environments/environment';
13+
import { buildPaginatedList } from '../../core/data/paginated-list.model';
14+
import { PageInfo } from '../../core/shared/page-info.model';
15+
import { SearchService } from '../../core/shared/search/search.service';
16+
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
17+
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
18+
import { FacetValue } from '../../shared/search/models/facet-value.model';
19+
import { FilterType } from '../../shared/search/models/filter-type.model';
20+
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
21+
import { SearchFilterConfig } from '../../shared/search/models/search-filter-config.model';
22+
import { SearchServiceStub } from '../../shared/testing/search-service.stub';
23+
import { BrowseByGeospatialDataComponent } from './browse-by-geospatial-data.component';
24+
25+
// create route stub
26+
const scope = 'test scope';
27+
const activatedRouteStub = {
28+
queryParams: observableOf({
29+
scope: scope,
30+
}),
31+
};
32+
33+
// Mock search filter config
34+
const mockFilterConfig = Object.assign(new SearchFilterConfig(), {
35+
name: 'point',
36+
type: FilterType.text,
37+
hasFacets: true,
38+
isOpenByDefault: false,
39+
pageSize: 2,
40+
minValue: 200,
41+
maxValue: 3000,
42+
});
43+
44+
// Mock facet values with and without point data
45+
const facetValue: FacetValue = {
46+
label: 'test',
47+
value: 'test',
48+
count: 20,
49+
_links: {
50+
self: { href: 'selectedValue-self-link2' },
51+
search: { href: `` },
52+
},
53+
};
54+
const pointFacetValue: FacetValue = {
55+
label: 'test point',
56+
value: 'Point ( +174.000000 -042.000000 )',
57+
count: 20,
58+
_links: {
59+
self: { href: 'selectedValue-self-link' },
60+
search: { href: `` },
61+
},
62+
};
63+
const mockValues = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [facetValue]));
64+
const mockPointValues = createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [pointFacetValue]));
65+
66+
// Expected search options used in getFacetValuesFor call
67+
const expectedSearchOptions: PaginatedSearchOptions = Object.assign({
68+
'configuration': environment.geospatialMapViewer.spatialFacetDiscoveryConfiguration,
69+
'scope': scope,
70+
'facetLimit': 99999,
71+
});
72+
73+
// Mock search config service returns mock search filter config on getConfig()
74+
const mockSearchConfigService = jasmine.createSpyObj('searchConfigurationService', {
75+
getConfig: createSuccessfulRemoteDataObject$([mockFilterConfig]),
76+
});
77+
let searchService: SearchServiceStub = new SearchServiceStub();
78+
79+
// initialize testing environment
80+
describe('BrowseByGeospatialDataComponent', () => {
81+
let component: BrowseByGeospatialDataComponent;
82+
let fixture: ComponentFixture<BrowseByGeospatialDataComponent>;
83+
84+
beforeEach(waitForAsync(() => {
85+
TestBed.configureTestingModule({
86+
imports: [ TranslateModule.forRoot(), StoreModule.forRoot(), BrowseByGeospatialDataComponent],
87+
providers: [
88+
{ provide: SearchService, useValue: searchService },
89+
{ provide: SearchConfigurationService, useValue: mockSearchConfigService },
90+
{ provide: ActivatedRoute, useValue: activatedRouteStub },
91+
],
92+
schemas: [NO_ERRORS_SCHEMA],
93+
})
94+
.compileComponents();
95+
}));
96+
97+
beforeEach(() => {
98+
fixture = TestBed.createComponent(BrowseByGeospatialDataComponent);
99+
component = fixture.componentInstance;
100+
});
101+
102+
it('component should be created successfully', () => {
103+
expect(component).toBeTruthy();
104+
});
105+
describe('BrowseByGeospatialDataComponent component with valid facet values', () => {
106+
beforeEach(() => {
107+
fixture = TestBed.createComponent(BrowseByGeospatialDataComponent);
108+
component = fixture.componentInstance;
109+
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockPointValues);
110+
component.scope$ = observableOf('');
111+
component.ngOnInit();
112+
fixture.detectChanges();
113+
});
114+
115+
it('should call searchConfigService.getConfig() after init', waitForAsync(() => {
116+
expect(mockSearchConfigService.getConfig).toHaveBeenCalled();
117+
}));
118+
119+
it('should call searchService.getFacetValuesFor() with expected parameters', waitForAsync(() => {
120+
component.getFacetValues().subscribe(() => {
121+
expect(searchService.getFacetValuesFor).toHaveBeenCalledWith(mockFilterConfig, 1, expectedSearchOptions, null, true);
122+
});
123+
}));
124+
});
125+
126+
describe('BrowseByGeospatialDataComponent component with invalid facet values (no point data)', () => {
127+
beforeEach(() => {
128+
fixture = TestBed.createComponent(BrowseByGeospatialDataComponent);
129+
component = fixture.componentInstance;
130+
spyOn(searchService, 'getFacetValuesFor').and.returnValue(mockValues);
131+
component.scope$ = observableOf('');
132+
component.ngOnInit();
133+
fixture.detectChanges();
134+
});
135+
136+
it('should call searchConfigService.getConfig() after init', waitForAsync(() => {
137+
expect(mockSearchConfigService.getConfig).toHaveBeenCalled();
138+
}));
139+
140+
it('should call searchService.getFacetValuesFor() with expected parameters', waitForAsync(() => {
141+
component.getFacetValues().subscribe(() => {
142+
expect(searchService.getFacetValuesFor).toHaveBeenCalledWith(mockFilterConfig, 1, expectedSearchOptions, null, true);
143+
});
144+
}));
145+
});
146+
147+
});

0 commit comments

Comments
 (0)