Skip to content

Commit f1561ef

Browse files
committed
Improve CRS, layer switcher and basemap handling
1 parent 149d658 commit f1561ef

File tree

8 files changed

+98
-37
lines changed

8 files changed

+98
-37
lines changed

basemaps.config.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const WMS = 'TileWMS';
66
const XYZ = 'XYZ';
77

88
// All options (except for 'is') follow the OpenLayers options for the respective source class.
9+
// Projections (except for EPSG:3857 and EPSG:4326) must be listed in the `crs` array in the config.js.
910
const BASEMAPS = {
1011
earth: [
1112
{
@@ -22,7 +23,7 @@ const BASEMAPS = {
2223
is: WMS,
2324
title: 'USGS Europa',
2425
attributions: USGS_ATTRIBUTION,
25-
projection: "EPSG:4326",
26+
projection: 'EPSG:4326',
2627
params: {
2728
FORMAT: 'image/png',
2829
LAYERS: 'GALILEO_VOYAGER'
@@ -35,7 +36,7 @@ const BASEMAPS = {
3536
is: WMS,
3637
title: 'USGS Mars',
3738
attributions: USGS_ATTRIBUTION,
38-
projection: "EPSG:4326",
39+
projection: 'EPSG:4326',
3940
params: {
4041
FORMAT: 'image/png',
4142
LAYERS: 'MDIM21'
@@ -48,7 +49,7 @@ const BASEMAPS = {
4849
is: WMS,
4950
title: 'USGS Moon',
5051
attributions: USGS_ATTRIBUTION,
51-
projection: "EPSG:4326",
52+
projection: 'EPSG:4326',
5253
params: {
5354
FORMAT: 'image/png',
5455
LAYERS: 'LROC_WAC'

config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,6 @@ module.exports = {
4646
requestQueryParameters: {},
4747
socialSharing: ['email', 'bsky', 'mastodon', 'x'],
4848
preprocessSTAC: null,
49-
authConfig: null
49+
authConfig: null,
50+
crs: {}
5051
};

config.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@
231231
"$ref": "https://stac-extensions.github.io/authentication/v1.1.0/schema.json"
232232
}
233233
]
234+
},
235+
"crs": {
236+
"type": [
237+
"object"
238+
]
234239
}
235240
}
236241
}

docs/options.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ The following ways to set config options are possible:
4545
- [buildTileUrlTemplate](#buildtileurltemplate)
4646
- [useTileLayerAsFallback](#usetilelayerasfallback)
4747
- [displayGeoTiffByDefault](#displaygeotiffbydefault)
48+
- [crs](#crs)
4849
- [User Interface](#user-interface)
4950
- [itemsPerPage](#itemsperpage)
5051
- [maxItemsPerPage](#maxitemsperpage)
@@ -373,6 +374,23 @@ To clarify the behavior, please have a look at the following table:
373374
If set to `true`, the map also shows non-cloud-optimized GeoTiff files by default. Otherwise (`false`, default), it only shows COGs and you can only enforce showing GeoTiffs to be loaded with the "Show on map" button but they are never loaded automatically.
374375
Loading non-cloud-optimized GeoTiffs only works reliably for smaller files (< 1MB). It may also work for larger files, but it is depending a lot on the underlying client hardware and software.
375376

377+
### crs
378+
379+
An object of coordinate reference systems that the system needs to know.
380+
The key is the code for the CRS, the value is the CRS definition as OGC WKT string (WKT2 is not supported).
381+
`EPSG:3857` (Web Mercator) and `EPSG:4326` (WGS 84) don't need to be registered, they are included by default.
382+
383+
This is primarily useful for CRS that are used for the basemaps (see `basemaps.config.js`).
384+
All CRS not listed here will be requested from an external service over HTTP, which is slower.
385+
386+
Example for EPSG:2056:
387+
388+
```js
389+
{
390+
'EPSG:2056': 'PROJCS["CH1903+ / LV95",GEOGCS["CH1903+",DATUM["CH1903+",SPHEROID["Bessel 1841",6377397.155,299.1528128,AUTHORITY["EPSG","7004"]],AUTHORITY["EPSG","6150"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4150"]],PROJECTION["Hotine_Oblique_Mercator_Azimuth_Center"],PARAMETER["latitude_of_center",46.9524055555556],PARAMETER["longitude_of_center",7.43958333333333],PARAMETER["azimuth",90],PARAMETER["rectified_grid_angle",90],PARAMETER["scale_factor",1],PARAMETER["false_easting",2600000],PARAMETER["false_northing",1200000],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS["Easting",EAST],AXIS["Northing",NORTH],AUTHORITY["EPSG","2056"]]'
391+
}
392+
```
393+
376394
## User Interface
377395

378396
### itemsPerPage

src/components/Map.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,10 @@ import LayerControl from './maps/LayerControl.vue';
2727
import TextControl from './maps/TextControl.vue';
2828
import { mapGetters } from 'vuex';
2929
import { BPopover } from 'bootstrap-vue';
30-
import proj4 from 'proj4';
3130
import Select from 'ol/interaction/Select';
32-
import {register} from 'ol/proj/proj4.js';
3331
import StacLayer from 'ol-stac';
3432
import { getStacObjectsForEvent, getStyle } from 'ol-stac/util.js';
3533
36-
register(proj4); // required to support source reprojection
37-
3834
const selectStyle = getStyle('#ff0000', 2, null);
3935
4036
export default {

src/components/maps/LayerControl.vue

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@
66
:target="id" container="#stac-browser"
77
>
88
<div class="layercontrol">
9-
<section v-if="hasLayers">
10-
<h5>{{ $t('mapping.layers.title') }}</h5>
11-
<LayerControlGroup :map="map" :group="layerGroup" />
12-
</section>
139
<section>
1410
<h5>{{ $t('mapping.layers.base') }}</h5>
1511
<span v-if="baseLayers.length === 0">{{ $t('mapping.nobasemap') }}</span>
@@ -19,17 +15,23 @@
1915
</b-form-radio>
2016
</b-form-radio-group>
2117
</section>
18+
<section v-if="hasLayers">
19+
<h5>{{ $t('mapping.layers.title') }}</h5>
20+
<LayerControlGroup :map="map" :group="layerGroup" />
21+
</section>
2222
</div>
2323
</b-popover>
2424
</div>
2525
</template>
2626

2727
<script>
28-
//import View from 'ol/View';
28+
import View from 'ol/View';
2929
import ControlMixin from './ControlMixin';
3030
import LayerControlMixin from './LayerControlMixin';
3131
import { BFormRadio, BFormRadioGroup, BIconLayersFill, BPopover } from 'bootstrap-vue';
32-
//import { transformWithProjections } from 'ol/proj';
32+
import { transformWithProjections } from 'ol/proj';
33+
import Group from 'ol/layer/Group';
34+
import { Vector } from 'ol/source';
3335
3436
export default {
3537
name: 'LayerControl',
@@ -66,23 +68,52 @@ export default {
6668
return;
6769
}
6870
// todo: switching between base layers with different projections is not working yet
69-
//let projection;
71+
let projection;
7072
for (const data of this.baseLayers) {
7173
data.layer.setVisible(data.id === newId);
72-
//projection = data.layer.getSource().getProjection();
74+
if (data.id === newId) {
75+
projection = data.layer.getSource().getProjection();
76+
}
77+
}
78+
const view = this.map.getView();
79+
const currentProjection = view.getProjection();
80+
if (currentProjection !== projection) {
81+
this.map.setView(new View({
82+
showFullExtent: true,
83+
projection,
84+
zoom: view.getZoom(),
85+
center: transformWithProjections(view.getCenter(), currentProjection, projection)
86+
}));
87+
this.reprojectLayers(this.map.getLayers(), currentProjection, projection);
7388
}
74-
// const view = this.map.getView();
75-
// if (view.getProjection() !== projection) {
76-
// this.map.setView(new View({
77-
// showFullExtent: true,
78-
// projection,
79-
// zoom: view.getZoom(),
80-
// center: transformWithProjections(view.getCenter(), view.getProjection(), projection)
81-
// }));
82-
// }
8389
}
8490
},
8591
methods: {
92+
reprojectLayers(layers, sourceProjection, targetProjection) {
93+
for (const layer of layers.getArray()) {
94+
if (layer.get('base')) {
95+
continue;
96+
}
97+
if (layer instanceof Group) {
98+
this.reprojectLayers(layer.getLayers(), sourceProjection, targetProjection);
99+
continue;
100+
}
101+
const source = layer.getSource();
102+
if (source instanceof Vector) {
103+
// Handle vector layers
104+
const currentProjection = source.getProjection() || sourceProjection;
105+
const features = source.getFeatures();
106+
for (const feature of features) {
107+
const geometry = feature.getGeometry();
108+
if (geometry) {
109+
geometry.transform(currentProjection, targetProjection);
110+
}
111+
}
112+
source.refresh();
113+
}
114+
// else: todo: Handle other layer types if needed
115+
}
116+
},
86117
update() {
87118
this.layerGroup = this.map.getLayerGroup();
88119
this.baseLayers = [];

src/components/maps/MapMixin.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
1-
import configureBasemap from '../../../basemaps.config';
21
import Utils from '../../utils';
32
import { mapState } from 'vuex';
43
import Map from 'ol/Map.js';
54
import View from 'ol/View.js';
65
import { defaults } from 'ol/interaction/defaults';
7-
import TileLayer from 'ol/layer/WebGLTile.js';
86
import ZoomControl from 'ol/control/Zoom.js';
97
import AttributionControl from 'ol/control/Attribution.js';
108
import FullScreenControl from 'ol/control/FullScreen.js';
119
import { stacRequest } from '../../store/utils';
1210

11+
import configureBasemap from '../../../basemaps.config';
12+
import CONFIG from '../../../config';
13+
import proj4 from 'proj4';
14+
import {register} from 'ol/proj/proj4.js';
15+
// Register pre-defined CRS from config in proj4
16+
if (Utils.isObject(CONFIG.crs)) {
17+
for (const code in CONFIG.crs) {
18+
proj4.defs(code, CONFIG.crs[code]);
19+
}
20+
}
21+
register(proj4); // required to support source reprojection
22+
1323
export default {
1424
computed: {
1525
...mapState(['buildTileUrlTemplate', 'crossOriginMedia', 'displayGeoTiffByDefault', 'useTileLayerAsFallback']),
@@ -53,7 +63,7 @@ export default {
5363
if (ix >= 0) {
5464
visibleLayer = ix;
5565
}
56-
const currentBasemap = this.basemaps[visibleLayer];
66+
const currentBasemap = this.basemaps[visibleLayer];
5767
if (currentBasemap?.projection) {
5868
projection = currentBasemap?.projection;
5969
}
@@ -125,20 +135,23 @@ export default {
125135
async addBasemaps(basemaps, visibleLayer = 0) {
126136
const promises = basemaps.map(async (options) => {
127137
try {
128-
const cls = (await import(`ol/source/${options.is}.js`)).default;
129-
return {
130-
source: new cls(options),
138+
const layerClassName = options.is === 'VectorTile' ? 'VectorTile' : 'WebGLTile';
139+
const [{default: sourceCls}, {default: layerCls}] = await Promise.all([
140+
import(`ol/source/${options.is}.js`),
141+
import(`ol/layer/${layerClassName}.js`)
142+
]);
143+
return new layerCls({
144+
source: new sourceCls(options),
131145
title: options.title,
132146
base: true
133-
};
147+
});
134148
} catch (error) {
135149
console.error(`Failed to load basemap source for ${options.is}`, error);
136150
return null;
137151
}
138152
});
139153
(await Promise.all(promises))
140154
.filter(options => Utils.isObject(options))
141-
.map(options => new TileLayer(options))
142155
.forEach((layer, i) => {
143156
layer.setVisible(i === visibleLayer);
144157
this.map.addLayer(layer);

src/components/maps/MapSelect.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,7 @@ import {shiftKeyOnly} from 'ol/events/condition.js';
1818
import ExtentInteraction from 'ol/interaction/Extent';
1919
import { containsXY } from 'ol/extent';
2020
import { transformExtent } from 'ol/proj';
21-
import { register } from 'ol/proj/proj4.js';
2221
import Style, { createDefaultStyle } from 'ol/style/Style';
23-
import proj4 from 'proj4';
2422
import VectorSource from 'ol/source/Vector';
2523
import GeoJSON from 'ol/format/GeoJSON';
2624
import Fill from 'ol/style/Fill';
@@ -29,8 +27,6 @@ import create from 'stac-js';
2927
import { toGeoJSON } from 'stac-js/src/geo.js';
3028
import mask from '@turf/mask';
3129
32-
register(proj4); // required to support source reprojection
33-
3430
export default {
3531
name: 'MapSelect',
3632
components: {

0 commit comments

Comments
 (0)