diff --git a/package-lock.json b/package-lock.json
index 9dcae99f4..3da544812 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,8 @@
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.81.1",
"mapml-extension": "git+https://github.com/Maps4HTML/mapml-extension",
+ "media-query-parser": "^3.0.2",
+ "media-query-solver": "^0.1.3",
"path": "^0.12.7",
"playwright": "^1.39.0",
"proj4": "^2.6.2",
@@ -2006,6 +2008,32 @@
"node": ">=8"
}
},
+ "node_modules/media-query-parser": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/media-query-parser/-/media-query-parser-3.0.2.tgz",
+ "integrity": "sha512-3WLXSFQZVuo1d9xpt72Ul0khXy72qgtz1KqDBO6KbmyvMsBsQRcePhYYR2U5QBSOz/u5pb5zj2s2FmgJe8jaTw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.5.0"
+ }
+ },
+ "node_modules/media-query-solver": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/media-query-solver/-/media-query-solver-0.1.3.tgz",
+ "integrity": "sha512-OkL4tpvexcAOG09rNdbAO1MebJzRa2s0RK7hTO9gVAL1jefbm6rLG9jcRYbwO1M3N8UGhHymMtOXK2ciDy3r8w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "media-query-solver": "dist/cjs/cli.js"
+ },
+ "engines": {
+ "node": ">=16.0.0 || ^14.13.1"
+ },
+ "peerDependencies": {
+ "media-query-parser": "^3.0.2"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"dev": true,
diff --git a/package.json b/package.json
index 6c10babc8..0431be43e 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,9 @@
"grunt-prettier": "^2.2.0",
"leaflet": "^1.9.4",
"leaflet.locatecontrol": "^0.81.1",
+ "mapml-extension": "git+https://github.com/Maps4HTML/mapml-extension",
+ "media-query-parser": "^3.0.2",
+ "media-query-solver": "^0.1.3",
"path": "^0.12.7",
"playwright": "^1.39.0",
"proj4": "^2.6.2",
diff --git a/src/mapml-viewer.js b/src/mapml-viewer.js
index 5aab491dc..b69b012c5 100644
--- a/src/mapml-viewer.js
+++ b/src/mapml-viewer.js
@@ -10,6 +10,9 @@ import Proj from 'proj4leaflet/src/proj4leaflet.js';
import { Util } from './mapml/utils/Util.js';
import { DOMTokenList } from './mapml/utils/DOMTokenList.js';
+import { parseMediaQueryList } from 'media-query-parser';
+import { solveMediaQueryList } from 'media-query-solver';
+
import { HTMLLayerElement } from './map-layer.js';
import { LayerDashElement } from './layer-.js';
import { HTMLMapCaptionElement } from './map-caption.js';
@@ -986,7 +989,298 @@ export class HTMLMapmlViewerElement extends HTMLElement {
}
});
}
+ getTestQuery() {
+ // Retrieve the map extent
+ const extent = this.extent;
+
+ // Extract the PCRS values for the bounding box
+ const topLeftEasting = Math.trunc(extent.topLeft.pcrs.horizontal);
+ const topLeftNorthing = Math.trunc(extent.topLeft.pcrs.vertical);
+ const bottomRightEasting = Math.trunc(extent.bottomRight.pcrs.horizontal);
+ const bottomRightNorthing = Math.trunc(extent.bottomRight.pcrs.vertical);
+
+ // Format the media query string to detect overlap:
+ // (xminm < xmaxq) and (xmaxm > xminq) and (yminm < ymaxq) and (ymaxm > yminq)
+ const query = `(map-projection: OSMTILE) and (map-zoom < 14) and (map-top-left-easting < ${bottomRightEasting}) and (map-bottom-right-easting > ${topLeftEasting}) and (map-bottom-right-northing < ${topLeftNorthing}) and (map-top-left-northing > ${bottomRightNorthing})`;
+
+ console.log(query);
+ let matcher = this.matchMedia(query);
+ const logResults = (e) => {
+ if (e.target.matches) {
+ layer.checked = true;
+ layer.removeAttribute('hidden');
+ } else {
+ layer.checked = false;
+ layer.hidden = true;
+ }
+ console.log('The query matches the map extent: ' + e.target.matches);
+ };
+ matcher.addEventListener('change', logResults);
+
+ // create a layer to visually represent the query as the map moves
+ let f = `
+${query}
+${topLeftEasting} ${topLeftNorthing}
+ ${bottomRightEasting} ${topLeftNorthing} ${bottomRightEasting} ${bottomRightNorthing} ${topLeftEasting} ${bottomRightNorthing}
+ ${topLeftEasting} ${topLeftNorthing}`;
+
+ const parser = new DOMParser();
+ const layer = parser
+ .parseFromString(f, 'text/html')
+ .querySelector('map-layer');
+ this.appendChild(layer);
+ return { matcher, logResults };
+ }
+ matchMedia(query) {
+ // useful features for maps: prefers-color-scheme, prefers-lang, projection, zoom, extent
+ const parsedQuery = parseMediaQueryList(query);
+
+ // less obviously useful: aspect-ratio, orientation, (device) resolution, overflow-block, overflow-inline
+
+ const map = this;
+ const features = {
+ 'prefers-lang': {
+ type: 'discrete',
+ get values() {
+ return [navigator.language.substring(0, 2)];
+ }
+ },
+ 'map-projection': {
+ type: 'discrete',
+ get values() {
+ return [map.projection.toLowerCase()];
+ }
+ },
+ 'map-zoom': {
+ type: 'range',
+ valueType: 'integer',
+ canBeNegative: false,
+ canBeZero: true,
+ get extraValues() {
+ return {
+ min: 0,
+ max: map.zoom
+ };
+ }
+ },
+ 'map-top-left-easting': {
+ type: 'range',
+ valueType: 'integer',
+ canBeNegative: true,
+ canBeZero: true,
+ get values() {
+ return [Math.trunc(map.extent.topLeft.pcrs.horizontal)];
+ }
+ },
+ 'map-top-left-northing': {
+ type: 'range',
+ valueType: 'integer',
+ canBeNegative: true,
+ canBeZero: true,
+ get values() {
+ return [Math.trunc(map.extent.topLeft.pcrs.vertical)];
+ }
+ },
+ 'map-bottom-right-easting': {
+ type: 'range',
+ valueType: 'integer',
+ canBeNegative: true,
+ canBeZero: true,
+ get values() {
+ return [Math.trunc(map.extent.bottomRight.pcrs.horizontal)];
+ }
+ },
+ 'map-bottom-right-northing': {
+ type: 'range',
+ valueType: 'integer',
+ canBeNegative: true,
+ canBeZero: true,
+ get values() {
+ return [Math.trunc(map.extent.bottomRight.pcrs.vertical)];
+ }
+ },
+ 'prefers-color-scheme': {
+ type: 'discrete',
+ get values() {
+ return [
+ window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light'
+ ];
+ }
+ },
+ 'prefers-map-content': {
+ type: 'discrete',
+ get values() {
+ return M.options.contentPreference;
+ }
+ }
+ };
+
+ const solveUnknownFeature = (featureNode) => {
+ let feature = featureNode.feature;
+ let queryValue = featureNode.value.value;
+ if (feature === 'prefers-lang') {
+ return features['prefers-lang'].values.includes(queryValue).toString();
+ } else if (
+ feature === 'map-zoom' ||
+ feature === 'map-top-left-easting' ||
+ feature === 'map-top-left-northing' ||
+ feature === 'map-bottom-right-easting' ||
+ feature === 'map-bottom-right-northing'
+ ) {
+ return solveRangeFeature(featureNode);
+ } else if (feature === 'map-projection') {
+ return features['map-projection'].values
+ .some((p) => p === queryValue)
+ .toString();
+ } else if (feature === 'prefers-color-scheme') {
+ return features['prefers-color-scheme'].values
+ .some((s) => s === queryValue)
+ .toString();
+ } else if (feature === 'prefers-map-content') {
+ return features[feature].values
+ .some((pref) => pref === queryValue)
+ .toString();
+ }
+ return 'false';
+ };
+ let matches =
+ solveMediaQueryList(parsedQuery, {
+ features,
+ solveUnknownFeature
+ }) === 'true'
+ ? true
+ : false;
+
+ function solveRangeFeature(featureNode) {
+ const { context, feature, value, op } = featureNode;
+
+ if (!feature.startsWith('map-')) {
+ return 'unknown';
+ }
+
+ const currentValue = getMapFeatureValue(feature);
+
+ if (currentValue === undefined) {
+ return 'unknown';
+ }
+
+ if (context === 'value') {
+ // Plain case: :
+ // Example: (map-zoom: 15)
+ return currentValue === value.value ? 'true' : 'false';
+ }
+
+ if (context === 'range') {
+ // Range case:
+ // Example: (0 <= map-zoom < 15)
+ switch (op) {
+ case '<':
+ return currentValue < value.value ? 'true' : 'false';
+ case '<=':
+ return currentValue <= value.value ? 'true' : 'false';
+ case '>':
+ return currentValue > value.value ? 'true' : 'false';
+ case '>=':
+ return currentValue >= value.value ? 'true' : 'false';
+ case '=':
+ return currentValue === value.value ? 'true' : 'false';
+ default:
+ return 'unknown';
+ }
+ }
+
+ return 'unknown'; // If the context is neither "value" nor "range"
+ }
+
+ function getMapFeatureValue(feature) {
+ switch (feature) {
+ case 'map-zoom':
+ return map.zoom;
+ case 'map-top-left-easting':
+ return Math.trunc(map.extent.topLeft.pcrs.horizontal);
+ case 'map-top-left-northing':
+ return Math.trunc(map.extent.topLeft.pcrs.vertical);
+ case 'map-bottom-right-easting':
+ return Math.trunc(map.extent.bottomRight.pcrs.horizontal);
+ case 'map-bottom-right-northing':
+ return Math.trunc(map.extent.bottomRight.pcrs.vertical);
+ default:
+ return undefined; // Unsupported or unknown feature
+ }
+ }
+
+ // Make mediaQueryList an EventTarget for dispatching events
+ const mediaQueryList = Object.assign(new EventTarget(), {
+ matches,
+ media: query,
+ listeners: [],
+ // this is a client facing api
+ addEventListener(event, listener) {
+ if (event === 'change') {
+ this.listeners.push(listener);
+
+ // Start observing properties only if there is at least one listener
+ if (this.listeners.length !== 0) {
+ observeProperties();
+ }
+ EventTarget.prototype.addEventListener.call(this, event, listener);
+ }
+ },
+
+ // this is a client facing api
+ removeEventListener(event, listener) {
+ if (event === 'change') {
+ this.listeners = this.listeners.filter((l) => l !== listener);
+
+ // Stop observing if there are no more listeners
+ if (this.listeners.length === 0) {
+ stopObserving();
+ }
+ EventTarget.prototype.removeEventListener.call(this, event, listener);
+ }
+ }
+ });
+
+ const observeProperties = () => {
+ const notifyIfChanged = () => {
+ const newMatches =
+ solveMediaQueryList(parsedQuery, {
+ features,
+ solveUnknownFeature
+ }) === 'true'
+ ? true
+ : false;
+ if (newMatches !== mediaQueryList.matches) {
+ mediaQueryList.matches = newMatches;
+
+ // Dispatch a "change" event to notify listeners of the update
+ mediaQueryList.dispatchEvent(new Event('change'));
+ }
+ };
+ notifyIfChanged.bind(this);
+ // Subscribe to internal events for changes in projection, zoom, and extent
+ this.addEventListener('map-projectionchange', notifyIfChanged);
+ this.addEventListener('map-moveend', notifyIfChanged);
+ const colorSchemeQuery = window.matchMedia(
+ '(prefers-color-scheme: dark)'
+ );
+ colorSchemeQuery.addEventListener('change', notifyIfChanged);
+
+ // Stop observing function
+ stopObserving = () => {
+ this.removeEventListener('map-projectionchange', notifyIfChanged);
+ this.removeEventListener('map-moveend', notifyIfChanged);
+ colorSchemeQuery.removeEventListener('change', notifyIfChanged);
+ };
+ };
+
+ let stopObserving; // Declare here so it can be assigned within observeProperties
+
+ return mediaQueryList;
+ }
locate(options) {
//options: https://leafletjs.com/reference.html#locate-options
if (this._geolocationButton) {