diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..93506a1d2 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# Enter commit ids here that you want to ignore using git blame + +# Prettier Formatting +12dcdf7535d7f21d986734c252fe73b05ad1fa08 diff --git a/.github/actions/sync-repo/entrypoint.sh b/.github/actions/sync-repo/entrypoint.sh index a7ba15ef5..377f79395 100755 --- a/.github/actions/sync-repo/entrypoint.sh +++ b/.github/actions/sync-repo/entrypoint.sh @@ -26,6 +26,9 @@ git config --global user.name "$INPUT_USER_NAME" git clone --single-branch --branch $INPUT_DESTINATION_BRANCH "https://$API_TOKEN_GITHUB@github.com/$INPUT_DESTINATION_REPO.git" "$CLONE_DIR" echo "Removing old destination files" +# Ensure the (possibly nested) destination folder exists so a first-time +# sync into a new subtree doesn't fail on the copy below. +mkdir -p "$CLONE_DIR/$INPUT_DESTINATION_FOLDER" rm -rf $CLONE_DIR/$INPUT_DESTINATION_FOLDER/* echo "Copying contents to git repo" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..812cbee85 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,7 @@ +This project is "MapML.js", the central polyfill implementation of the MapML vocabulary. MapML is implemented as a suite of autonomous custom elements that come together as a package. + +For instructions on the markup rules of MapML, which are especially important to follow when _generating_ either standalone MapML documents or MapML html markup within an HTML document, see the .github/skills folder child folders named `map-something-markup` generally, where `map-something` is the custom element name. These skills may also be useful when considering the design of new markup-driven features. + +This project uses playwright for e2e tests. There are multiple generations of test, so consistency is lacking. We are trying to move away from testing the content rendered by Leaflet where possible, towards even screenshot based tests, even though they can be tricky. + +Further information will go below, as appropriate. \ No newline at end of file diff --git a/.github/skills/map-a-markup/SKILLS.md b/.github/skills/map-a-markup/SKILLS.md new file mode 100644 index 000000000..3a69c6198 --- /dev/null +++ b/.github/skills/map-a-markup/SKILLS.md @@ -0,0 +1,165 @@ +--- +name: mapml-a-markup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page, especially to wrap `` content, either in whole or in part, just like how you might use `` to wrap all or part of a paragraph of text in HTML. +--- + +The `` element is a proposal to extend the Web to include links between maps and locations. +This element allows you to wrap parts of coordinates or entire geometries, making a link out of the location/area that is wrapped. When a feature geometry or geometry part is +wrapped in a `` element, it creates a blue outline that is 1 pixel wide around the feature (by default), that lets the user know it's a "linked feature". + +## Attributes + +### `href` + - The URL that the wrapped location points to. Note - If the `type` of the `` is text/mapml + you can provide fragments, more on fragments below. + +--- + +### `target` + - This is where the linked URL will be displayed. See table below for more details. + - Defaults to `_self`, in the absence of a valid value. + +--- + +### `type` + - This is the mime type of the linked URL's format. Options are `text/html` & `text/mapml`. + - Defaults to `text/mapml`, in the absence of a valid type value. + +--- + +### `inplace` + - The `inplace` attribute is a boolean attribute - `` + - When present, the default view-changing behavior is overridden and the map view does not change. + +--- + +## Target Behavior for `text/mapml` + +| Target Value | Behavior | +|-------------- |------------------------------------------------------- | +| _self | Replaces the current layer with the linked URL layer. | +| _blank | Adds the linked URL layer to the map. | +| _parent | Replace all the layers with the linked URL layer. | +| _top | Navigate the webpage to the linked URL. | + +--- + +## Target Behavior for `text/html` + +| Target Value | Behavior | +|-------------- |----------------------------------------- | +| _self | Navigate the webpage to the linked URL. | +| _blank | Open the linked URL in a new tab. | +| _parent | Navigate the webpage to the linked URL. | +| _top | Navigate the webpage to the linked URL. | + +--- + +## Location fragments + +If the `type` attribute's value is `text/mapml`, you have the ability add a location fragment +to the URL. This will pan & zoom the map to the given location. + +Fragments are in the following format `#zoom, longitude, latitude`. + +URL's solely defined in terms of location fragments pan and zoom the map to the given location regardless of the target value. +i.e. `...` will pan to latitude: 30, longitude: 20 and zoom to level 1. + +--- + +## Examples + +### Styling Linked Features + +To style linked features simply target the `map-a` class in your CSS, once a link is clicked you can target the +`map-a-visited` class. See the example below: + +```html + + + .map-a { + stroke: red; + } + .map-a-visited { + stroke: green; + } + + + +

Basic

+
+ + + + 2771 3106 2946 3113 2954 3210 2815 3192 2771 3106 + + + +
+
+``` + +### Wrapping a Feature Type + Location Fragment + +```html + + +

Basic

+
+ + + + 2771 3106 2946 3113 2954 3210 2815 3192 2771 3106 + + + +
+``` + +This will replace the current layer with the layer within externalMapML.mapml, once it's added the map will then goto +zoomlevel: 2, longitude: -98, latitude: 37. + +### Wrapping a point coordinate with `target="_blank"` + +```html + + +

_blank target

+
+ + + 2771 3106 2946 3113 2954 3210 2815 3192 2771 3106 + + +
+``` + +In this example, a point will be created at (2954, 3210) which, once clicked, adds a new layer to the map. + +### Nested `` definition and behavior + +```html + + +

Advanced Example

+
+ + + + + 2771 3106 2946 3113 2954 3210 2815 3192 2771 3106 + + + + 11 11 12 11 12 12 11 12 + + + + + +
+``` +In this advanced example there are multiple nested ``. The simple behavior is, the closest `` is the link +behavior that the given location/area will adopt. + +--- \ No newline at end of file diff --git a/.github/skills/map-caption-markup/SKILLS.md b/.github/skills/map-caption-markup/SKILLS.md new file mode 100644 index 000000000..a127ed66f --- /dev/null +++ b/.github/skills/map-caption-markup/SKILLS.md @@ -0,0 +1,13 @@ +--- +name: mapml-caption-markup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page, especially when generating a `` element, to describe the map's purpose, if possible and known. +--- + +This element is especially important for screen reader users to understand the purpose of a map. It is like 'alt-text' for a map. + +The `` element is a child of `` and is used to define +a simple text string that is not visually rendered (at this time). +The caption should be read by screen readers when the `` is focused, +as it generates the `` value, if no aria-label +has been specified by the HTML author. `` may be the first or last +element child of ``. diff --git a/.github/skills/map-extent-markup/SKILL.md b/.github/skills/map-extent-markup/SKILL.md new file mode 100644 index 000000000..64a895b60 --- /dev/null +++ b/.github/skills/map-extent-markup/SKILL.md @@ -0,0 +1,137 @@ +--- +name: mapml-extent-markup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page. +--- + +The `` element is a hypertext control that is associated to and represents the +rectangle of the map viewport, from the user's perspective. Map authors use it +to compose server requests for layer content. Requests are composed using +URL templates processed by the browser as the map moves and requires new content +to paint. The URL templates each contain one or more variable references, with each +variable reference denoted by the name of the variable enclosed in braces `{}`. + +Variables are created by the map author using the `` element. There are +several types of ``, allowing the map author to reference the corners +of the extent, its width and height, and its zoom. + +An example of a `` element being used to load image tiles for a single +URL template. + +```html + + + + + +``` + +## Attributes + +### `units` + +Specifies the projection of the tiles and other content that is expected from the +server. If the projection value is a case-insensitive match of the `` +`projection` attribute, the extent will be disabled in the layer control, and will +not be displayed on the map, nor content fetched. + +Defined values of `units` include: + +| Projection | Description | +|-------------- |-------------------------------------------------------- | +| OSMTILE | Web Mercator, with 256px x 256px tiles recursively defined inside a square bounds at zoom = 0| +| WGS84 | Pseudo plate carrée, with 256px x 256px tiles. Zoom = 0 contains two tiles in two columns, with their origin at -180,90. False easting and northing (pcrs) values inside the projection bounds correspond to longitude and latitude, respectively. | +| CBMTILE | Lambert Conformal Conic, with 256px x 256px tiles. Zoom levels chosen by scale denominator, so tiles do not nest.| + +Author-defined values of `units` are possible, using the [Custom projections API](../../api/mapml-viewer-api/#definecustomprojectionoptions) + +The `units` attribute is required and can't be changed. + +--- + +### `label` + +Specifies a label for an extent which is displayed in the layer control. When a `label` value is not provided, the `label` value defaults to 'Sub-Layer' in the layer control. + +--- + +### `checked` + +The `checked` attribute and property is boolean. When present, the checked property value is taken to be 'true'; when not present, the property value is 'false'. The map-extent content will be fetched and rendered according to the `checked` state. Beware that it is the *presence* of the attribute that makes it true, not the value of the attribute. For example, the attribute `checked="false"` actually turns out to be checked, [as described by MDN Web docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes). + +--- + +### `hidden` + +The `hidden` attribute and property is boolean. When present, the extent is hidden (not present) in the layer control. Regardless of `hidden` state, the layer is rendered or not depending on the `checked` attribute state. + +--- + +### `opacity` + +The `opacity` attribute is used to set the initial opacity of the `` element. +Valid `opacity` values range from from "0.0" to "1.0" with strictly one demical place and are reflected in the extent settings +opacity input slider control. When the `opacity` attribute is not present, the opacity is set to "1.0" by default. + +--- + +## Examples + +### Multiple Extent + +The following example shows multiple `` elements in a layer. The different elements can be selected from the three dots menu of the Basemap layer. + +```html + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### WMS Request + +The following example shows a Web Map Service Request using `` to request map images. + +```html + + + + + + + + + + + + + + + + +``` diff --git a/.github/skills/map-feature-markup/SKILL.md b/.github/skills/map-feature-markup/SKILL.md new file mode 100644 index 000000000..0993174eb --- /dev/null +++ b/.github/skills/map-feature-markup/SKILL.md @@ -0,0 +1,261 @@ +--- +name: mapml-feature-markup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page. +--- + +Map [features](https://en.wikipedia.org/wiki/Geographical_feature) are real or imaginary location objects represented in 2D according to a standard model, called the [Simple Features model](https://en.wikipedia.org/wiki/Simple_Features). There exists a wide variety of formats that allow the encoding of the Simple Features model, famously including: GeoJSON, Keyhole Markup Language (KML), and shape files (.shp), among many others. + +Map features are represented in HTML MapML using a `` element, which is rendered on the map through translation to SVG. This allows the feature to scale without distortion, as you zoom in and out. + +A `` element is a container for a feature's accessible name (``), scalar properties (``) and its geometry (``). The `` element can be modeled as inline HTML content as a child of the `` element, or in an XHTML MapML document, as a child of the `` element. + +## Attributes + +### `zoom` + +This allows you to set the zoom level the feature is rendered at. The zoom value +should fall within the range of 0 to the maximum zoom level of the map's +[projection](../meta/#attributes). + +### `min` + +The `min` (zoom) attribute gets or sets the native minimum zoom of the feature. +Map features' geometry and other properties are scale-dependent. The `min` value +is a rendering zoom value cut-off; at map zoom values less than `min`, the feature +will not be rendered. + +If `min` is not provided, a fallback value will be calculated; the fallback value +will be the minimum zoom value of the layer, or if that is not specified, of the +map viewer `projection`'s minimum value i.e. 0. + +### `max` + +The `max` (zoom) attribute gets or sets the native maximum zoom of the feature. +Map features' geometry and other properties are scale-dependent. The `max` value +is a rendering zoom value cut-off; at map zoom values greater than `max`, the +feature will not be rendered. + +If `max` is not provided, a fallback value will be calculated; the fallback value +will be the maximum zoom value of the layer, or if that is not specified, of the +map viewer `projection`'s maximum value e.g. 25 (depending on the projection). + +--- + +## Child Elements + +### `` + +This element contains the feature's accessible name, which is displayed when the feature is in focus or hovered. + +--- + +### `` + +This element contains the contents of the popup associated to a given feature. Details on the properties elements and it's syntax can be found [here](../properties/). + +--- + +### `` + +This element contains the different points, lines and polygons that will be drawn. Details on the geometry elements and it's syntax can be found [here](../geometry/). + +#### Attributes + +- `cs` + - This allows you to set the [coordinate system](../meta/#attributes) of geometries. + - Defaults to pcrs (projected coordinates), but can be set to tilematrix | pcrs | gcrs explicitly. + +--- + +## Related Elements + +Other elements may be important to provide context for feature data: + + +### `` + +Sets the native minimum and maximum [native zoom](../meta/#attributes). It also allows you to set a value, this is the default zoom of all features in the case the `` is missing a zoom attribute. + +```html + +``` + +--- + +### `` + +Sets the [projection](../meta/#attributes) of the layer. + +```html + +``` + +--- + +### `` + +Sets the default [coordinate system](../meta/#attributes) of the layer. If a feature is missing the cs attribute it will 'fall back' to the value provided by a `map-meta` element, or `pcrs` if no `map-meta` element is in scope. + +```html + +``` + +--- + +### `` + +Sets the [extent](../meta/#attributes) of the layer. + +```html + +``` + +--- + +## Examples + +```html + + + + + + + + + + +

Test

+
test + + + + 11 11 12 11 12 12 11 12 + + + + + + +

Test

+
+ + + 257421 -3567196 -271745 1221771 -3896544 242811 -3183549 -2613313 + + +
+ + + +

Test

+
+ + + 2771 3106 2946 3113 2954 3210 2815 3192 + + +
+ + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+ + +``` +### An inline HTML map-feature + +```html + + + + + + + + + + + + + + + +
code1200020
accuracy26
valdate1995
image + + + +
themeFO
type2
elevation61
altiaccu5
+
+ + + -75.705278 45.397778 + + +
+
+``` + +### A map-feature in a fetched XHTML MapML document + +```html + +``` + +### 35059.mapml: + +```html + + + The Man With Two Hats + + + + + + + + + + + + + + + + + + + + + +
code1200020
accuracy26
valdate1995
image + + + +
themeFO
type2
elevation61
altiaccu5
+
+ + + -75.705278 45.397778 + + +
+
+
+``` diff --git a/.github/skills/map-geometry-markup/SKILLS.md b/.github/skills/map-geometry-markup/SKILLS.md new file mode 100644 index 000000000..35fbc9581 --- /dev/null +++ b/.github/skills/map-geometry-markup/SKILLS.md @@ -0,0 +1,235 @@ +--- +name: mapml-geometry-markup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page. +--- + +A `` element is a child of `` and is used to describe the geometry of the feature. + +A `` element has one child element, which can be a ``, ``, ``, ``, ``, ``, or ``. + +## Attributes + +### `cs` + +Defines the Coordinate System of the geometry. When no `cs` is provided, the coordinate system of descendant `` elements is determined by a fallback to any in-scope ``. If no fallback coordinate system is specified by a `` element, the default coordinate system of the `map-layer` is used; if none is defined, `gcrs` (geographic coordinates) is used. + +| CRS | Description | +|------|-------------| +| tcrs | For each zoom level (i.e. at a pre-defined scale denominator value), locations are expressed in terms of scaled pixels, with the origin of pixel space at the upper left corner. The pixel coordinates of a location at a single zoom level are independent of the pixel coordinates of a location any other zoom level. In other words, you need to know the zoom level of a tcrs coordinate in order to locate it on a map or otherwise process it. | +| tilematrix | Each zoom level has an array of tiles, called a tilematrix. The individual tiles constitute the coordinates in this CRS, and the axes are know as `row` and `column`. The tiles are defined as squares of 256 pixels in the associated tcrs of the particular zoom level. | +| pcrs | Projected CRS (pcrs) are defined by a mathematical relationship with an underlying gcrs, using a technique called "projection". pcrs coordinates are scale- and zoom level-independent, and are designed to represent geographic coordinates on a planar surface, such as a device screen. The measurement units of pcrs coordinates is _usually_ meters (a notable exception being pcrs coordinates in the `WGS84` projection). | +| gcrs | Geographic coordinates are referenced to various ellipsoids, and are not necessarily comparable across projections. A common ellipsoid today is WGS 84, which is defined and used by the global positioning satellite (GPS) constellation. | +| map | The map CRS is dynamic, in the sense that it has its origin at the upper left of the user's viewport, with scaled pixels as units. This is used to identify image coordinates for use, typically by WMS and similar services which use a virtual image to enable query of map feature property information, without necessarily transferring the features over the network. | +| tile | Each tile in any zoom level has an implicit scaled-pixel coordinate system ranging from 0 to 255 in both horizontal and vertical directions. These coordinates are used by WMTS and similar services to identify a pixel for query of feature property values, without transferring the feature geometry over the network. | + +--- + +## Child Elements + + +### `` + +This element contains a `` element containing a single position. Axis order - x followed by y, separated by whitespace. Note that longitude and latitude (gcrs coordinates) are listed in that order, always, in all geometry types. + +--- + +### `` + +This element contains a `` element containing two or more positions. Axis order - x followed by y, separated by whitespace. + +--- + +### `` + +This element contains one or more `` elements, each containing three or more positions. Axis order - x followed by y, separated by whitespace. + +The first and last positions in every child `` element are equal / at the same position. + +The first `` element represents the outside of the polygon, and subsequent `` elements represent holes. The "winding order" of positions in child `` should depend on the axis orientation of the coordinate reference system in use, and whether the `` element represents the exterior of a polygon, or a hole. For WGS84, the exterior should be counterclockwise and holes should be clockwise. + +--- + +### `` + +This element contains a `` element, containing one or more positions. Axis order - x followed by y, separated by whitespace. + +--- + +### `` + +This element contains one or more `` elements, each containing two or more positions. Axis order - x followed by y, separated by whitespace. + +--- + +### `` + +This element contains the contents one or more `` elements. Axis order - x followed by y, separated by whitespace. + +For each member polygon, the same non-schema constraints apply to multipolygon descendant `` elements, as for polygon `` descendant elements. + +--- + +### `` + +This element contains one or more ``, ``, ``, ``, ``, `` elements. + +For each member geometry, the same non-schema constraints apply as to the unique geometry type above. + +--- + + +## Examples + +### Point +```html + + + + + + Point + + + -75.6916809 45.4186964 + + +

This is a Point

+
+
+
+``` + +### LineString + +```html + + + + + + Line + + + -75.6168365 45.471929 -75.6855011 45.458445 -75.7016373 45.4391764 -75.7030106 45.4259255 -75.7236099 45.4208652 -75.7565689 45.4117074 -75.7833481 45.384225 -75.8197403 45.3714435 -75.8516693 45.377714 + + +

This is a Line

+
+
+
+``` + +### Polygon + +```html + + + + + + Polygon + + + -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 -75.7249832 45.4083331 -75.7792282 45.3772317 -75.7534790 45.3294614 -75.5831909 45.3815724 -75.6024170 45.4273712 -75.5673981 45.4639834 -75.5859375 45.4656690 + -75.6596588 45.4211062 -75.6338958 45.4254436 -75.6277127 45.4066458 -75.6572542 45.4097792 -75.6596588 45.4211062 + + +

This is a Polygon

+
+
+
+``` + +### MultiPoint + +```html + + + + + + MultiPoint + + + -75.7016373 45.4391764 -75.7236099 45.4208652 -75.7833481 45.384225 + + +

This is a multipoint

+
+
+
+``` + +### MultiLineString + +```html + + + + + + MultiLineString + + + -75.6168365 45.471929 -75.6855011 45.458445 -75.7016373 45.4391764 -75.7030106 45.4259255 + -75.7565689 45.4117074 -75.7833481 45.384225 -75.8197403 45.3714435 -75.8516693 45.377714 + + +

This is a MultiLineString

+
+
+
+``` + +### MultiPolygon + +```html + + + + + + MultiPolygon + + + + -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 -75.7249832 45.4083331 -75.7792282 45.3772317 -75.7534790 45.3294614 -75.5831909 45.3815724 -75.6024170 45.4273712 -75.5673981 45.4639834 -75.5859375 45.4656690 + + + -75.6744295 45.4728920 -75.7053451 45.4439942 -75.7063756 45.4249616 -75.7489704 45.4177324 -75.7788555 45.4003785 -75.7943133 45.4321899 -75.6744295 45.4728920 + + + +

This is a MultiPolygon

+
+
+
+``` + +### GeometryCollection + +```html + + + + + + Geometry Collection + + + + -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 -75.7249832 45.4083331 -75.7792282 45.3772317 -75.7534790 45.3294614 -75.5831909 45.3815724 -75.6024170 45.4273712 -75.5673981 45.4639834 -75.5859375 45.4656690 + + + -75.6168365 45.471929 -75.6855011 45.458445 -75.7016373 45.4391764 -75.7030106 45.4259255 -75.7236099 45.4208652 -75.7565689 45.4117074 -75.7833481 45.384225 -75.8197403 45.3714435 -75.8516693 45.377714 + + + -75.6916809 45.4186964 + + + +

This is a Geometry Collection

+
+
+
+``` diff --git a/.github/skills/map-input-markup/SKILL.md b/.github/skills/map-input-markup/SKILL.md new file mode 100644 index 000000000..9032b424d --- /dev/null +++ b/.github/skills/map-input-markup/SKILL.md @@ -0,0 +1,205 @@ +--- +name: mapml-input-markup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page. +--- + + +The `` element is an extended HTML `input` by `type` for use in Web +map `` elements. The attributes that apply to the input depend on +the `type` attribute. There are several types of `` that can be used. + +The `` declares a variable that will be set and used by the polyfill +according to its attributes, as the map changes viewport extent in response to +user gestures. + +## Attributes + +### `name` +Sets the name of the variable created by the input. The variable can then be +referenced by the URL template `` [tref attribute](../link#tref), +using the `{name}` variable reference notation. + +--- +### `type` + Sets the **type** of the input. + +| Type | Description | +|-------------- |-------------------------------------------------------- | +| zoom | Integer value that ranges from 0 to some fixed number of values, depending on the projection, less than or equal to 25.| +| location | A location input captures **one** `axis` value of a two-dimensional point ( represented by a coordinate pair) from the map extent, e.g. `top-right`, or, for server queries, the location in the map where the user clicks or touches. | +| width | A width input captures the width of the map viewport's extent in standardized pixels | +| height | A height input captures the height of the map viewport's extent in standardized pixels | +| hidden | Establishes a variable that may be used to pass a fixed value to the server when requesting map resources. | +--- + +### `value` +In general, the `value` is set and used by the polyfill when generating URLs from +[URL templates](../link#tref), for fetching server resources such as tiles, images +and map documents. + +In particular, `value` is used by authors to specify values for inputs of +`type="zoom"`. In the case of `type="zoom"`, `value` defines the zoom level of +_associated_ **sibling** `` elements' `min` and `max` +attribute values. Allows authors to establish native / server resource bounds, +on a per-`` basis. + +Inputs are _associated_ by their variables being referenced by a +`` value. The `{z}`, +`{row}`, and `{col}` variable references in the above example associate the +variables (``'s) named **z**, **row** and **col**. + +--- +### `axis` +This attribute applies only to inputs of `type="location"`. It establishes the axis +of the coordinate to be serialized as [a named variable](#). This value also +identifies the axis that the `min` and `max` attributes apply to, so that the +polyfill will not make requests for spatial resources (tiles etc) outside the +native axis bounds. Possible values of `axis` are: + +| Axis name | CRS | Description | +|-----------|-----|------------------------------------------------------ | +|`row` | tilematrix |Vertical axis, positive down the screen. The origin is at the upper left. Units are tiles. Each zoom level is a distinct tilematrix crs, so coordinate values apply only to that zoom level. | +|`column` | tilematrix |Horizontal axis, positive to the right. The origin is at the upper left. Units are tiles. Each zoom level is a distinct tilematrix crs, so coordinate values apply only to that zoom level.| +|`easting` | pcrs |Horizontal axis, positive to the right. The origin is defined a geographic location. Units are ususally meters, although for some projections (specifically WGS84), the transformation from geographic CRS, i.e. from longitude to easting is the [identity transformation](https://www.thefreedictionary.com/Identity+transformation), in which case the easting values could be _said_ to be in decimal degrees. **pcrs** stands for "projected coordinate reference system". Note that because pcrs is an 'infinite canvas', there exist locations for which a transformation from pcrs coordinates to gcrs coordinates is undefined. | +|`northing` | pcrs |Vertical axis, positive to the right. The origin is defined a geographic location. Units are ususally meters, although for some projections (specifically WGS84), the transformation from geographic CRS, i.e. from latitude to northing is the [identity transformation](https://www.thefreedictionary.com/Identity+transformation), in which case the northing values could be _said_ to be decimal degrees. Note that because pcrs is an 'infinite canvas', there exist locations for which a transformation from pcrs coordinates to gcrs coordinates is undefined.| +|`latitude` | gcrs | The latitude of a point on an ellipsoid is the angle between a line perpendicular to the surface of the ellipsoid at the given point and the plane of the equator. **gcrs** stands for "geographic coordinate reference system".| +|`longitude`| gcrs | The longitude of a point specifies its east–west position on the reference body's (Earth's) surface.| +|`x` | tcrs | Horizontal axis, positive to the right. The origin is at the upper left. Units are defined-size pixels. Each zoom level is a distinct tcrs, so coordinate values apply only to that zoom level. **tcrs** stands for "tiled coordinate reference system". The tiles of each **tilematrix** crs are defined as aggregations of pixels in the corresponding zoom level **tcrs**.| +|`y` | tcrs | Vertical axis, positive down the screen. The origin is at the upper left. Units are defined-size pixels. The origin is at the upper left. Units are scaled pixels. Each zoom level is a distinct tcrs, so coordinate values apply only to that zoom level.| +|`i` | tile, map | Horizontal axis, positive to the right. Each tile, and the map viewport, has a defined-size pixel-based crs that has its origin at the upper left (of each tile in the case of the **tile** crs, and of the map viewport, in the case of the **map** crs). | +|`j` | tile, map | Vertical axis, positive down the screen. Each individual tile, and the map viewport has a defined-size pixel-based crs that has its origin at the upper left (of each tile in the case of the **tile** crs, and of the map viewport, in the case of the **map** crs).| + +When requesting a the coordinate axis of whole tile in the `OSMTILE` projection, +a location input might use a `` +input to establish a variable named **y**, referenced by `{y}` in an associated URL template, +which would serialize the **tilematrix** crs `row` axis values. A series of +location events is genereated by the polyfill as required by the map to cover +the viewport in tiles. + +--- +### `units` + +Identifies the associated specific coordinate reference system that a location +input event is referred to. The term "projection" in MapML is synonymous with the set +of CRS that are related together by the projection name. In all cases for any projection, +there exist a set of CRS that are related mathematically. These CRS are known +and identified within the namespace of the projection name by the following +table of keyword values: + + +| CRS | Description | +|------|-------------| +| tcrs | For each zoom level (i.e. at a pre-defined scale denominator value), locations are expressed in terms of scaled pixels, with the origin of pixel space at the upper left corner. The pixel coordinates of a location at a single zoom level are independent of the pixel coordinates of a location any other zoom level. In other words, you need to know the zoom level of a tcrs coordinate in order to locate it on a map or otherwise process it. | +| tilematrix | Each zoom level has an array of tiles, called a tilematrix. The individual tiles constitute the coordinates in this CRS, and the axes are know as `row` and `column`. The tiles are defined as squares of 256 pixels in the associated tcrs of the particular zoom level. | +| pcrs | Projected CRS (pcrs) are defined by a mathematical relationship with an underlying gcrs, using a technique called "projection". pcrs coordinates are scale- and zoom level-independent, and are designed to represent geographic coordinates on a planar surface, such as a device screen. The measurement units of pcrs coordinates is _usually_ meters (a notable exception being pcrs coordinates in the `WGS84` projection). | +| gcrs | Geographic coordinates are referenced to various ellipsoids, and are not necessarily comparable across projections. A common ellipsoid today is WGS 84, which is defined and used by the global positioning satellite (GPS) constellation. | +| map | The map CRS is dynamic, in the sense that it has its origin at the upper left of the user's viewport, with scaled pixels as units. This is used to identify image coordinates for use, typically by WMS and similar services which use a virtual image to enable query of map feature property information, without necessarily transferring the features over the network. | +| tile | Each tile in any zoom level has an implicit scaled-pixel coordinate system ranging from 0 to 255 in both horizontal and vertical directions. These coordinates are used by WMTS and similar services to identify a pixel for query of feature property values, without transferring the feature geometry over the network. | + +Sometimes, location inputs are used to generate "brownie-cutter" (square) requests +for tiles from WMS and similar un-tiled services. In such a case, it is possible +for the `units` to be specified as `tilematrix`, implying that the location event +expected is that of a tile, and the `position` keyword is then used to describe +the corner of the tile for which the coordinate should be serialized. In such a +case, the `axis` value may be specified as `easting` or other axis name +associated with the projection, to obtain coordinate of the corner of the tile +that is being processed by the polyfill: + +`` + +Internally, the crs of the requested coordinate is deduced from the axis +name, instead of requiring the author to explicitly specify the axis' crs as an +additional attribute of the ``. + +--- + +### `position` + +Allows the author to specify a predefined corner of the viewport or tile to be used +as the location value to be serialized. If `position` is not present, the input +location coordinates will be generated by user click or touch on the map, which +is used to generate interactive server queries. + +| position keyword | keyword description| +|------------------|--------------------| +| top-left | Identifies the location at the top left corner of the tile or viewport | +| top-right | Identifies the location at the top right corner of the tile or viewport | +| bottom-left | Identifies the location at the bottom left corner of the tile or viewport | +| bottom-right | Identifies the location at the bottom right corner of the tile or viewport | + +Other values of `position` are possible, but are not implemented yet. + +--- +### `rel` + +Specifies the entity to which the `position` applies. Possible values are `tile` and `image`. +The default value, if unspecified, is `image`. It is used to help identify what crs the +coordinate serialized by the input exists in. + +--- +### `min` +Establishes the minimum of the axis on the server . Requests for coordinates less than +this value will not be created by the polyfill. + +--- +### `max` +Establishes the maximum of the axis on the server. Requests for coordinates greater than +this value will not be created by the polyfill. + +--- +### `step` +Sets the zoom interval according to which resources will be requested within the +zoom range. The step is always calculated from a base value of 0. At zoom values +that fall within a step interval, resources will be requested as required, and +scaled to the current zoom level. For example, with a min=0 and a max=7 for +a given zoom input with a step=4, tiles will be requested at only zoom=0 and scaled +to zoom values of 1, 2 and 3 as the map is rendered at those levels. Use of this +attribute can conserve user bandwidth while having little visual effect, depending +on the nature of the content. + +--- +## Examples + +### Input step + +```html + + + + + + + + + + +``` + +### Input rel=tile to generate WMS requests for tiles + +WMS behaviour can seem slow, even when it is fast. Painting the map using tiles +generated by individual WMS GetMap requests can improve users' impression of your +map, although it is not advisable when the layer being requested contains text +labels, which may be duplicated on adjacent tiles many times over. + +```html + + + + + + + + + + + + + + + + +``` diff --git a/.github/skills/map-layer-markup/SKILL.md b/.github/skills/map-layer-markup/SKILL.md new file mode 100644 index 000000000..e9873b5d6 --- /dev/null +++ b/.github/skills/map-layer-markup/SKILL.md @@ -0,0 +1,97 @@ +--- +name: mapml-map-layermarkup +description: Tells you how to correctly create and edit the markup for a element. Use it when generating MapML output markup in an HTML page. +--- + +Maps have one or more layers. Map layers are implemented by the `` custom element. +All `` content is contained by one or more `` elements, either inline or by remote fetch representing the layer's content. + +```html + + ...layer content goes here... + +``` + +`` is not a 'void' element - it must be closed with a `` tag. + +Map content can either be inline, as shown above - between the beginning `` and ending `` tags - +or fetched, from the `` source attribute URL: + +```html + +``` + + +This documentation uses the convention of inline content mostly. Fetched map content +follows similar semantics, except it is parsed with the browser's XML parser and +so must follow both MapML document conventions as well as +[XML syntax rules](https://developer.mozilla.org/en-US/docs/Web/XML/XML_introduction). + +## Attributes + +### `src` + +Contains the path to a MapML document. + +--- + +### `checked` + +The `` attribute and property is boolean. When present, +the checked property value is taken to be 'true'; when not present, the property +value is 'false'. Beware that it is the _presence_ of the attribute that makes it +true, not the value of the attribute. For example, the attribute `checked="false"` +actually turns out to be checked, +[as described by MDN Web docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes#boolean_attributes). + +--- + +### `hidden` + +The `
+ - - - - - - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
+ + + + + + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
- - - - - - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
+ + + + + - - - - - - -

Arizona

-
- - - -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 - -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 - 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 - -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 - 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 - -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 - 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 - -109.042503 37.000263 - - -
-
- - + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
+ + + + + + + + +

Arizona

+
+ + + -109.042503 37.000263 -109.04798 31.331629 -111.074448 31.331629 -112.246513 31.704061 + -114.815198 32.492741 -114.72209 32.717295 -114.524921 32.755634 -114.470151 32.843265 -114.524921 + 33.029481 -114.661844 33.034958 -114.727567 33.40739 -114.524921 33.54979 -114.497536 33.697668 + -114.535874 33.933176 -114.415382 34.108438 -114.256551 34.174162 -114.136058 34.305608 -114.333228 + 34.448009 -114.470151 34.710902 -114.634459 34.87521 -114.634459 35.00118 -114.574213 35.138103 + -114.596121 35.324319 -114.678275 35.516012 -114.738521 36.102045 -114.371566 36.140383 -114.251074 + 36.01989 -114.152489 36.025367 -114.048427 36.195153 -114.048427 37.000263 -110.499369 37.00574 + -109.042503 37.000263 + + +
+
+ + \ No newline at end of file diff --git a/test/e2e/core/axisInferring.test.js b/test/e2e/core/axisInferring.test.js index 662817caf..47c5109a7 100644 --- a/test/e2e/core/axisInferring.test.js +++ b/test/e2e/core/axisInferring.test.js @@ -1,53 +1,107 @@ import { test, expect, chromium } from '@playwright/test'; -test.describe("UI Drag&Drop Test", () => { +test.describe('UI Drag&Drop Test', () => { let page; let context; test.beforeAll(async () => { context = await chromium.launchPersistentContext(''); - page = context.pages().find((page) => page.url() === 'about:blank') || await context.newPage(); - await page.goto("axisInferring.html"); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('axisInferring.html'); }); test.afterAll(async function () { await context.close(); }); - test("TileMatrix inferring", async () => { + /** + * + * The point of "axisInferring.test.js" is to validate that the extent's cs + * is correctly inferred from the axis names in the content attribute. + * + * Semantics: the extent of the layers should include the extent of the map-meta + * -specified extent PLUS the contents of the layer (a map-feature in all cases). + * + * The test expectations were actually measured from the loaded html. + */ + test('TileMatrix inferring', async () => { + await page.waitForTimeout(1000); const layerExtent = await page.$eval( - "body > map > layer-:nth-child(1)", + 'body > map > map-layer:nth-child(1)', (layer) => layer.extent ); - expect(layerExtent.topLeft.tilematrix[0]).toEqual({ horizontal: 0, vertical: 1 }); - expect(layerExtent.bottomRight.tilematrix[0]).toEqual({ horizontal: 4, vertical: 5 }); + expect(layerExtent.topLeft.tilematrix[0]).toEqual({ + horizontal: 0, + vertical: 1 + }); + expect(layerExtent.bottomRight.tilematrix[0]).toEqual({ + horizontal: 4, + vertical: 5 + }); }); - test("TCRS inferring", async () => { + test('TCRS inferring', async () => { const layerExtent = await page.$eval( - "body > map > layer-:nth-child(2)", + 'body > map > map-layer:nth-child(2)', (layer) => layer.extent ); - expect(layerExtent.topLeft.tcrs[0]).toEqual({ horizontal: 0, vertical: 256 }); - expect(layerExtent.bottomRight.tcrs[0]).toEqual({ horizontal: 256, vertical: 512 }); + expect(layerExtent.topLeft.tcrs[0]).toEqual({ + horizontal: 0, + vertical: 256 + }); + expect(layerExtent.bottomRight.tcrs[0]).toEqual({ + horizontal: 868.9331277338575, + vertical: 1069.7755295390994 + }); }); - test("PCRS inferring", async () => { + test('PCRS inferring', async () => { const layerExtent = await page.$eval( - "body > map > layer-:nth-child(3)", + 'body > map > map-layer:nth-child(3)', (layer) => layer.extent ); - expect(layerExtent.topLeft.pcrs).toEqual({ horizontal: 100, vertical: 600 }); - expect(layerExtent.bottomRight.pcrs).toEqual({ horizontal: 500, vertical: 150 }); + // the top left corner is that of the map-meta[name=extent] + expect(layerExtent.topLeft.pcrs).toEqual({ + horizontal: -6601973, + vertical: 1569758 + }); + // the bottom right corner is that of the map-feature + expect(layerExtent.bottomRight.pcrs).toEqual({ + horizontal: -1319475.9373123178, + vertical: -1731574.5341126453 + }); }); - test("GCRS inferring", async () => { + test('GCRS inferring', async () => { const layerExtent = await page.$eval( - "body > map > layer-:nth-child(4)", + 'body > map > map-layer:nth-child(4)', (layer) => layer.extent ); - expect(layerExtent.topLeft.gcrs).toEqual({ horizontal: -92, vertical: 52.999999999993484 }); - expect(layerExtent.bottomRight.gcrs).toEqual({ horizontal: -62, vertical: 33.99999999999964 }); + let expectedTopLeftLongitude = -114.815198; + let expectedTopLeftLatitude = 53; + let expectedBottomRightLongitude = -62; + let expectedBottomRightLatitude = 31.331629; + + expect(layerExtent.topLeft.gcrs.horizontal).toBeCloseTo( + expectedTopLeftLongitude, + 6 + ); + expect(layerExtent.topLeft.gcrs.vertical).toBeCloseTo( + expectedTopLeftLatitude, + 6 + ); + + expect(layerExtent.bottomRight.gcrs.horizontal).toBeCloseTo( + expectedBottomRightLongitude, + 6 + ); + expect(layerExtent.bottomRight.gcrs.vertical).toBeCloseTo( + expectedBottomRightLatitude, + 6 + ); }); }); diff --git a/test/e2e/core/debugMode.html b/test/e2e/core/debugMode.html index aef015a17..51f6eab6c 100644 --- a/test/e2e/core/debugMode.html +++ b/test/e2e/core/debugMode.html @@ -4,7 +4,7 @@ Debug mode tests - + + + + + + + + + + + + + + + .second { + stroke: aqua; + } + + + .third { + stroke: blue + } + + + + feature, role='button' + +

Test

+ test +
+ + + 11 11 12 11 12 12 11 12 + + +
+ + + feature with table properties + + + + + + + + + + + + + + + + + +
Head_1Head_2Head_3
row 1, column 1row 1, column 2row 1, column 3
row 2, column 1row 2, column 2row 2, column 3
+
+ + + 10.5 11 10.75 11 10.75 11.25 10.5 11.25 10.5 11 + + +
+ + + feature, role='link' + + + + + 2771 3106 2946 3113 2954 3210 2815 3192 + + + + 2745 3218 + + + + + + + + + Point, zoom = 2 + + + + + + 2745 3218 + + + + + + + + + Point, with no zoom attribute + + + + + + 1658335.1282702535 -3261543.4764202833 + + + + + + + + + topLeft point, zoom = 2 + + + + + -35001.59173651785 -1568206.756413512 + + + + + + + + bottomRight, zoom = maxZoom + + + + + 1658343.59495385 -3261551.9431038797 + + + + + + + + topLeft, zoom = maxZoom + + + + + 1658326.6615866497 -3261535.0097366795 + + + + + + + + +

Event handler test

+
+ + + + 9 10 10 10 10 11 9 11 9 10 + + + +
+
+ +
+ + + + diff --git a/test/e2e/core/mapFeature.test.js b/test/e2e/core/mapFeature.test.js new file mode 100644 index 000000000..234cc8b2a --- /dev/null +++ b/test/e2e/core/mapFeature.test.js @@ -0,0 +1,355 @@ +import { test, expect, chromium } from '@playwright/test'; +const data = require('./mapFeature.data.js'); + +test.describe('Playwright MapFeature Custom Element Tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('mapFeature.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('Shadowroot tests of with src attribute', async () => { + let shadowAttached = await page.$eval( + 'body > map', + (map) => map.layers[1].shadowRoot !== null + ); + expect(shadowAttached).toEqual(true); + + // remove and then re-add element + shadowAttached = await page.$eval('body > map', (map) => { + let layer = map.layers[1]; + map.removeChild(layer); + map.appendChild(layer); + return layer.shadowRoot !== null; + }); + expect(shadowAttached).toEqual(true); + }); + + test('MapFeature interactivity tests', async () => { + await page.waitForTimeout(1000); + const buttonFeature = page.locator('map-feature.button'); + await expect(buttonFeature).toHaveCount(1); + const buttonFeatureRendering = page.getByLabel("feature, role='button'"); + await expect(buttonFeatureRendering).toHaveCount(1); + + // change attributes + await page.$eval('body > map', async (map) => { + let layer = map.querySelector('map-layer'), + mapFeature = layer.querySelector('map-feature'); + mapFeature.setAttribute('zoom', '4'); + mapFeature.zoomTo(); + }); + await page.waitForTimeout(200); + let mapZoom = await page.$eval('body > map', (map) => + map.getAttribute('zoom') + ); + // expect the associated el to re-render and re-attach to the map + expect(mapZoom).toEqual('4'); + + // change + await page.reload(); + await page.waitForTimeout(500); + let prevExtentBR = await page.$eval('body > map', (map) => { + let layer = map.querySelector('map-layer'), + mapFeature = layer.querySelector('map-feature'); + return mapFeature.extent.bottomRight.pcrs; + }); + + let newExtentBR = await page.$eval('body > map', (map) => { + let layer = map.querySelector('map-layer'), + mapFeature = layer.querySelector('map-feature'), + mapCoord = mapFeature.querySelector('map-coordinates'); + mapCoord.innerHTML = '12 11 12 11 12 12 11 13'; + return mapFeature.extent.bottomRight.pcrs; + }); + // expect the associated el to re-render and re-attach to the map + expect(newExtentBR === prevExtentBR).toBe(false); + + // remove + await page.$eval('body > map', (map) => { + let layer = map.querySelector('map-layer'), + mapFeature = layer.querySelector('map-feature'); + mapFeature.querySelector('map-properties').remove(); + }); + await page.$eval('body > map', (map) => { + let layer = map.querySelector('map-layer'), + mapFeature = layer.querySelector('map-feature'); + return mapFeature.click(); + }); + const popupCount = await page.$eval( + 'body > map > div > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', + (popupPane) => popupPane.childElementCount + ); + // expect no popup is binded + expect(popupCount).toEqual(0); + }); + + test('Get extent of with zoom attribute = 2', async () => { + await page.reload(); + await page.waitForTimeout(500); + const extent = await page.$eval( + 'body > map', + (map) => map.querySelector('.point_1').extent + ); + expect(extent).toEqual(data.pointExtentwithZoom); + }); + + test('Zoom to with zoom attribute = 2', async () => { + const mapZoom = await page.$eval('body > map', (map) => { + map.querySelector('.point_1').zoomTo(); + return +map.zoom; + }); + expect(mapZoom).toEqual(2); + }); + + test('Get extent of with no zoom attribute', async () => { + const extent = await page.$eval( + 'body > map', + (map) => map.querySelector('.point_2').extent + ); + expect(extent).toEqual(data.pointExtentNoZoom); + }); + + test('Zoom to with no zoom attribute', async () => { + const mapZoom = await page.$eval('body > map', (map) => { + map.querySelector('.point_2').zoomTo(); + return +map.zoom; + }); + expect(mapZoom).toEqual(3); + }); + + test('Get geojson representation of with single geometry', async () => { + await page.reload(); + await page.waitForTimeout(200); + // options = {propertyFunction: null, transform: true} (default) + let geojson = await page.$eval('body > map', (map) => + map.querySelector('map-feature').mapml2geojson() + ); + expect(geojson).toEqual(data.geojsonData.withDefOptions); + + // options = {propertyFunction: null, transform: false} + geojson = await page.$eval('body > map', (map) => + map.querySelector('map-feature').mapml2geojson({ transform: false }) + ); + expect(geojson).toEqual(data.geojsonData.withNoTransform); + + // options = {propertyFunction: function (properties) {...}, transform: true} + geojson = await page.$eval('body > map', (map) => { + return map.querySelector('.table').mapml2geojson({ + propertyFunction: function (properties) { + let obj = {}, + count = 0; + properties.querySelectorAll('th').forEach((th) => { + obj[th.innerHTML] = []; + properties.querySelectorAll('tr').forEach((tr) => { + let data = tr.querySelectorAll('td')[count]?.innerHTML; + if (data) { + obj[th.innerHTML].push(data); + } + }); + count++; + }); + return obj; + } + }); + }); + expect(geojson).toEqual(data.geojsonData.withPropertyFunc); + }); + + test('Get geojson representation of with multiple geometries', async () => { + // multiple geometries () + let geojson = await page.$eval('body > map', (map) => + map.querySelector('.link').mapml2geojson() + ); + expect(geojson).toEqual(data.geojsonData.withMultiGeo); + }); + + test('Default click method test', async () => { + // click method test + // with role="button" should have popup opened after click + const popup = await page.$eval('body > map', (map) => { + let feature = map.querySelector('.button'); + feature.click(); + return feature._geometry.isPopupOpen(); + }); + expect(popup).toEqual(true); + + // with role="link" should add a new layer / jump to another page after click + const layerCount = await page.$eval('body > map', (map) => { + map.querySelector('.link').click(); + return map.layers.length; + }); + expect(layerCount).toEqual(4); + + // the element should be marked as "visited" after click + let status = await page.$eval( + 'body > map > div > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.mapml-vector-container > svg > g > g:nth-child(3)', + (g) => { + for (let path of g.querySelectorAll('path')) { + if (!path.classList.contains('map-a-visited')) { + return false; + } + } + return true; + } + ); + expect(status).toEqual(true); + }); + + test('Custom click method test', async () => { + const isClicked = await page.$eval('body > map', (map) => { + let mapFeature = map.querySelector('map-feature'); + // define custom click method + mapFeature.onclick = function (e) { + document.querySelector('map-feature').classList.add('customClick'); + }; + mapFeature.click(); + return mapFeature.classList.contains('customClick'); + }); + expect(isClicked).toEqual(true); + }); + + test('Default focus method test', async () => { + await page.reload(); + await page.waitForTimeout(500); + // focus method test + let focus = await page.$eval( + 'body > map > div > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.mapml-vector-container > svg > g > g:nth-child(1)', + (g) => { + let layer = document.querySelector('map').querySelector('map-layer'), + mapFeature = layer.querySelector('map-feature'); + mapFeature.focus(); + return document.activeElement.shadowRoot?.activeElement === g; + } + ); + expect(focus).toEqual(true); + + // focus state will be removed when users change focus to the other elements + await page.$eval('body > map', (map) => + map.querySelectorAll('map-feature')[1].focus() + ); + focus = await page.$eval( + 'body > map > div > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.mapml-vector-container > svg > g > g:nth-child(1)', + (g) => document.activeElement.shadowRoot?.activeElement === g + ); + expect(focus).toEqual(false); + }); + + test('Default blur method test', async () => { + const loseFocus = await page.$eval('body > map', (map) => { + let feature = map.querySelector('map-feature'); + feature.focus(); + feature.blur(); + return ( + document.activeElement.shadowRoot?.activeElement === map._map._container + ); + }); + expect(loseFocus).toEqual(true); + }); + + test('Custom focus method test', async () => { + const isFocused = await page.$eval('body > map', (map) => { + let mapFeature = map.querySelector('map-feature'); + // define custom focus method + mapFeature.onfocus = function (e) { + map.querySelector('map-feature').classList.add('customFocus'); + }; + mapFeature.focus(); + return mapFeature.classList.contains('customFocus'); + }); + expect(isFocused).toEqual(true); + }); + + test('Add event handler via HTML', async () => { + const test = await page.$eval('body > map', (map) => { + let mapFeature = map.querySelector('.event'); + mapFeature.setAttribute('onfocus', 'test_1()'); + mapFeature._groupEl.focus(); + return mapFeature.classList.contains('test_1'); + }); + expect(test).toEqual(true); + }); + + test('Add event handler via Script', async () => { + const test = await page.$eval('body > map', (map) => { + let mapFeature = map.querySelector('.event'); + mapFeature._groupEl.blur(); + return ( + mapFeature.classList.contains('blur_property_test') && + mapFeature.classList.contains('blur_addEvtLsn_test') + ); + }); + expect(test).toEqual(true); + }); +}); + +test.describe('MapFeature Events', () => { + let page, context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('mapFeature1.html'); + }); + test.afterAll(async function () { + await context.close(); + }); + + test('Custom Click event - stopPropagation', async () => { + await page.waitForTimeout(1000); + // Click on polygon + await page + .locator( + 'mapml-viewer[role="application"]:has-text("Polygon -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 -75")' + ) + .click(); + const popupCount = await page.$eval( + 'body > mapml-viewer > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', + (popupPane) => popupPane.childElementCount + ); + // expect no popup is binded + expect(popupCount).toEqual(0); + + await page.waitForTimeout(500); + + // custom click property displaying on div + const propertyDiv = await page.$eval( + 'body > div#property', + (div) => div.firstElementChild.innerText + ); + // check custom event is displaying properties + expect(propertyDiv).toEqual('This is a Polygon'); + }); + + test('click() method - stopPropagation', async () => { + // click() method on line feature + await page.$eval( + 'body > mapml-viewer > map-layer > map-feature#line', + (line) => line.click() + ); + + const popupCount = await page.$eval( + 'body > mapml-viewer > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane', + (popupPane) => popupPane.childElementCount + ); + // expect no popup is binded + expect(popupCount).toEqual(0); + + // custom click property displaying on div + const propertyDiv = await page.$eval( + 'body > div#property', + (div) => div.firstElementChild.innerText + ); + // check custom event is displaying properties + expect(propertyDiv).toEqual('This is a Line'); + }); +}); diff --git a/test/e2e/core/mapFeature1.html b/test/e2e/core/mapFeature1.html new file mode 100644 index 000000000..14e836272 --- /dev/null +++ b/test/e2e/core/mapFeature1.html @@ -0,0 +1,71 @@ + + + + + map-feature Event tests + + + + + + + + + + Polygon + + + -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 + -75.7249832 45.4083331 -75.7792282 45.3772317 -75.7534790 45.3294614 -75.5831909 45.3815724 + -75.6024170 45.4273712 -75.5673981 45.4639834 -75.5859375 45.4656690 + + + +

This is a Polygon

+
+
+ + + Line + + + -75.6168365 45.471929 -75.6855011 45.458445 -75.7016373 45.4391764 -75.7030106 + 45.4259255 -75.7236099 45.4208652 -75.7565689 45.4117074 -75.7833481 45.384225 -75.8197403 + 45.3714435 -75.8516693 45.377714 + + + +

This is a Line

+
+
+ + + Point + + + -75.6916809 45.4186964 + + + +

This is a Point

+
+
+
+
+ +
+ + + + + diff --git a/test/e2e/core/mapSpan.css b/test/e2e/core/mapSpan.css index e4ac03688..ed0464328 100644 --- a/test/e2e/core/mapSpan.css +++ b/test/e2e/core/mapSpan.css @@ -36,7 +36,7 @@ mapml-viewer:defined, vertical-align: middle; } -/* Pre-style to avoid FOUC of inline layer- and fallback content. */ +/* Pre-style to avoid FOUC of inline map-layer and fallback content. */ mapml-viewer:not(:defined) > * { display: none; } @@ -48,6 +48,6 @@ mapml-viewer:not(:defined) > * { supported, or if javascript is disabled. This needs to be defined separately from the above, because the `:not(:defined)` selector invalidates the entire declaration in browsers that do not support it. */ -layer- { +map-layer { display: none; } \ No newline at end of file diff --git a/test/e2e/core/mapSpan.html b/test/e2e/core/mapSpan.html index 3ed808e8e..8436e1624 100644 --- a/test/e2e/core/mapSpan.html +++ b/test/e2e/core/mapSpan.html @@ -4,7 +4,7 @@ MapML Vector Tiles - + - - - + + + + + + + `); await page.waitForLoadState('networkidle'); - await page.hover('div > div.leaflet-control-container > div.leaflet-top.leaflet-right'); - const cbmtileLayer = await page.$eval("body > map > layer-:nth-child(1)", - (controller) => controller.hasAttribute('disabled')); - const osmtileLayer = await page.$eval("#checkMe", - (controller) => controller.hasAttribute('disabled')); + await page.hover( + 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right' + ); + const cbmtileLayer = await page.$eval( + 'body > map > map-layer:nth-child(1)', + (controller) => controller.hasAttribute('disabled') + ); + const osmtileLayer = await page.$eval('#checkMe', (controller) => + controller.hasAttribute('disabled') + ); expect(cbmtileLayer).toEqual(false); expect(osmtileLayer).toEqual(true); }); - test("OSMTILE Map with CBMTILE layer", async () => { + test('OSMTILE Map with CBMTILE layer', async () => { await page.setContent(` index-map.html - + - - - + + + + + + + `); await page.waitForLoadState('networkidle'); - await page.hover('div > div.leaflet-control-container > div.leaflet-top.leaflet-right'); - const cbmtileLayer = await page.$eval("#checkMe", - (controller) => controller.hasAttribute('disabled')); - const osmtileLayer = await page.$eval("body > mapml-viewer > layer-:nth-child(2)", - (controller) => controller.hasAttribute('disabled')); + await page.hover( + 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right' + ); + const cbmtileLayer = await page.$eval('#checkMe', (controller) => + controller.hasAttribute('disabled') + ); + const osmtileLayer = await page.$eval( + 'body > mapml-viewer > map-layer:nth-child(2)', + (controller) => controller.hasAttribute('disabled') + ); - await page.hover("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div"); - await page.click("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(1) > div:nth-child(1) > label > span", - { button: "right", force: true }); + await page.hover( + 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div' + ); + await page.click( + 'div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(1) > div:nth-child(1) > label > span', + { button: 'right', force: true } + ); - const aHandle = await page.evaluateHandle(() => document.querySelector("mapml-viewer")); - const nextHandle = await page.evaluateHandle(doc => doc.shadowRoot, aHandle); - const resultHandle = await page.evaluateHandle(root => root.querySelector(".mapml-contextmenu.mapml-layer-menu"), nextHandle); + const aHandle = await page.evaluateHandle(() => + document.querySelector('mapml-viewer') + ); + const nextHandle = await page.evaluateHandle( + (doc) => doc.shadowRoot, + aHandle + ); + const resultHandle = await page.evaluateHandle( + (root) => root.querySelector('.mapml-contextmenu.mapml-layer-menu'), + nextHandle + ); - const menuDisplay = await (await page.evaluateHandle(elem => elem.style.display, resultHandle)).jsonValue(); + const menuDisplay = await ( + await page.evaluateHandle((elem) => elem.style.display, resultHandle) + ).jsonValue(); - expect(menuDisplay).toEqual(""); + expect(menuDisplay).toEqual(''); expect(cbmtileLayer).toEqual(true); expect(osmtileLayer).toEqual(false); }); -}); \ No newline at end of file +}); diff --git a/test/e2e/core/missingMetaParameters.html b/test/e2e/core/missingMetaParameters.html index 2fb856b04..850382856 100644 --- a/test/e2e/core/missingMetaParameters.html +++ b/test/e2e/core/missingMetaParameters.html @@ -5,7 +5,7 @@ Missing Meta Parameters Test - + + + + + + + + + + + + diff --git a/test/e2e/core/searchDisabled.test.js b/test/e2e/core/searchDisabled.test.js new file mode 100644 index 000000000..42c820979 --- /dev/null +++ b/test/e2e/core/searchDisabled.test.js @@ -0,0 +1,179 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('Search button disabled state tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('searchDisabled.html', { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('search button disabled when no layer has map-link rel=search', async () => { + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('true'); + }); + + test.describe('Local layer — dynamic add/remove', () => { + test('search button enabled after adding map-link rel=search to local layer', async () => { + await page.evaluate(() => { + let layer = document.querySelector('[data-testid=local-layer]'); + let link = document.createElement('map-link'); + link.setAttribute('rel', 'search'); + link.dataset.testid = 'local-search-link'; + layer.appendChild(link); + }); + await page.waitForTimeout(100); + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('false'); + }); + + test('search button disabled after removing map-link rel=search from local layer', async () => { + await page.evaluate(() => { + let link = document.querySelector('[data-testid=local-search-link]'); + link.remove(); + }); + await page.waitForTimeout(100); + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('true'); + }); + }); + + test.describe('Layer checked/unchecked', () => { + test('search button disabled when layer with search link is unchecked', async () => { + // Add search link first + await page.evaluate(() => { + let layer = document.querySelector('[data-testid=local-layer]'); + let link = document.createElement('map-link'); + link.setAttribute('rel', 'search'); + link.dataset.testid = 'local-search-link'; + layer.appendChild(link); + }); + await page.waitForTimeout(100); + // Verify enabled + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('false'); + + // Uncheck the layer + await page.evaluate(() => { + document + .querySelector('[data-testid=local-layer]') + .removeAttribute('checked'); + }); + await page.waitForTimeout(100); + ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('true'); + }); + + test('search button re-enabled when layer with search link is re-checked', async () => { + await page.evaluate(() => { + document + .querySelector('[data-testid=local-layer]') + .setAttribute('checked', ''); + }); + await page.waitForTimeout(100); + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('false'); + + // Clean up: remove the search link + await page.evaluate(() => { + let link = document.querySelector('[data-testid=local-search-link]'); + if (link) link.remove(); + }); + await page.waitForTimeout(100); + }); + }); + + test.describe('Remote (src) layer', () => { + test('search button enabled when remote layer has map-link rel=search', async () => { + await page.evaluate(async () => { + let layer = document.querySelector('[data-testid=remote-layer]'); + layer.src = 'search-layer.mapml'; + await layer.whenReady(); + }); + await page.waitForTimeout(200); + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('false'); + }); + + test('search button disabled when remote layer without search link', async () => { + await page.evaluate(async () => { + let layer = document.querySelector('[data-testid=remote-layer]'); + layer.src = 'no-search-layer.mapml'; + await layer.whenReady(); + }); + await page.waitForTimeout(200); + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('true'); + }); + }); + + test.describe('Multiple layers', () => { + test('search button stays enabled if one of two layers has search link', async () => { + // Add search link to local layer + await page.evaluate(() => { + let layer = document.querySelector('[data-testid=local-layer]'); + let link = document.createElement('map-link'); + link.setAttribute('rel', 'search'); + link.dataset.testid = 'local-search-link'; + layer.appendChild(link); + }); + await page.waitForTimeout(100); + // Remote layer has no search link (no-search-layer.mapml) + // But local layer does — button should be enabled + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('false'); + }); + + test('search button disabled only when all search-capable layers are unchecked', async () => { + // Uncheck local layer (the only one with search link) + await page.evaluate(() => { + document + .querySelector('[data-testid=local-layer]') + .removeAttribute('checked'); + }); + await page.waitForTimeout(100); + let ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('true'); + + // Re-check it — should be enabled again + await page.evaluate(() => { + document + .querySelector('[data-testid=local-layer]') + .setAttribute('checked', ''); + }); + await page.waitForTimeout(100); + ariaDisabled = await page.$eval('.mapml-search-button', (btn) => + btn.getAttribute('aria-disabled') + ); + expect(ariaDisabled).toEqual('false'); + }); + }); +}); diff --git a/test/e2e/core/searchI18n.html b/test/e2e/core/searchI18n.html new file mode 100644 index 000000000..09dbd2dd8 --- /dev/null +++ b/test/e2e/core/searchI18n.html @@ -0,0 +1,28 @@ + + + + + + Search i18n Tests + + + + + + + + diff --git a/test/e2e/core/searchI18n.test.js b/test/e2e/core/searchI18n.test.js new file mode 100644 index 000000000..f22f9c9e4 --- /dev/null +++ b/test/e2e/core/searchI18n.test.js @@ -0,0 +1,75 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('Search i18n tests (Japanese)', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('searchI18n.html', { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + }); + test.afterAll(async () => { + await context.close(); + }); + + test('Japanese query in suggestions displays CJK results', async () => { + await page.click('.mapml-search-button'); + await page.waitForTimeout(400); + // Type Japanese characters directly (simulates committed IME input) + await page.fill('.mapml-search-input', '東京'); + // Wait for debounce + fetch + await page.waitForSelector('.mapml-search-result', { timeout: 5000 }); + let texts = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.textContent) + ); + expect(texts[0]).toBe('東京都, 日本'); + expect(texts[1]).toBe('東京タワー, 東京都, 日本'); + expect(texts[2]).toBe('東京駅, 東京都, 日本'); + }); + + test('Japanese query via Enter performs search', async () => { + await page.fill('.mapml-search-input', '東京'); + await page.press('.mapml-search-input', 'Enter'); + await page.waitForSelector('.mapml-search-result', { timeout: 5000 }); + await page.waitForTimeout(300); + let texts = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.textContent) + ); + expect(texts).toEqual(['東京都, 日本']); + }); + + test('Clicking Japanese result moves map to correct location', async () => { + await page.click('.mapml-search-result:first-child'); + await page.waitForTimeout(500); + let center = await page.$eval('[data-testid=viewer]', (viewer) => { + let map = viewer._map; + return { lat: map.getCenter().lat, lng: map.getCenter().lng }; + }); + // Tokyo bbox center is roughly (35.7, 139.4) + expect(center.lat).toBeCloseTo(35.7, 0); + expect(center.lng).toBeCloseTo(139.4, 0); + // Close panel to restore clean state for subsequent tests + await page.press('.mapml-search-input', 'Escape'); // clear input + await page.press('.mapml-search-input', 'Escape'); // close panel + await page.waitForTimeout(400); + }); + + test('Search query is properly URL-encoded for non-Latin characters', async () => { + // Intercept the network request to verify encoding + let requestUrl = ''; + page.on('request', (request) => { + if (request.url().includes('/search/ja/suggestions')) { + requestUrl = request.url(); + } + }); + await page.click('.mapml-search-button'); + await page.waitForTimeout(400); + await page.fill('.mapml-search-input', '東京'); + await page.waitForSelector('.mapml-search-result', { timeout: 5000 }); + // The query should be percent-encoded + expect(requestUrl).toContain(encodeURIComponent('東京')); + }); +}); diff --git a/test/e2e/core/searchMultiLayer.html b/test/e2e/core/searchMultiLayer.html new file mode 100644 index 000000000..f05725b86 --- /dev/null +++ b/test/e2e/core/searchMultiLayer.html @@ -0,0 +1,107 @@ + + + + + + Search Multi-Layer Tests + + + + + + + + + + diff --git a/test/e2e/core/searchMultiLayer.test.js b/test/e2e/core/searchMultiLayer.test.js new file mode 100644 index 000000000..72470eb28 --- /dev/null +++ b/test/e2e/core/searchMultiLayer.test.js @@ -0,0 +1,150 @@ +import { test, expect, chromium } from '@playwright/test'; + +test.describe('Search multi-layer tests', () => { + let page; + let context; + test.beforeAll(async () => { + context = await chromium.launchPersistentContext(''); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('searchMultiLayer.html', { waitUntil: 'networkidle' }); + await page.waitForTimeout(1000); + }); + test.afterAll(async () => { + await context.close(); + }); + + test('Suggestions aggregate results from both layers', async () => { + // Open the panel + await page.click('.mapml-search-button'); + await page.waitForTimeout(400); + // Type a query + await page.fill('.mapml-search-input', 'test'); + // Wait for results from both layers (5 total) + // Use waitForSelector (pierces shadow DOM) then poll count via $$eval + await page.waitForSelector('.mapml-search-result', { timeout: 5000 }); + await page.waitForTimeout(500); + let texts = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.textContent) + ); + // Layer 1 returns 3 items (Ottawa, Arctic Ocean, Atlantic Ocean) + // Layer 2 returns 2 items (Toronto, Montreal) + expect(texts).toEqual([ + 'Ottawa', + 'Arctic Ocean', + 'Atlantic Ocean', + 'Toronto', + 'Montreal' + ]); + }); + + test('Suggestion results carry correct layer attribution', async () => { + let layers = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.dataset.layer) + ); + // First 3 from layer 1, last 2 from layer 2 + expect(layers[0]).toBe('Geonames Layer 1'); + expect(layers[1]).toBe('Geonames Layer 1'); + expect(layers[2]).toBe('Geonames Layer 1'); + expect(layers[3]).toBe('Geonames Layer 2'); + expect(layers[4]).toBe('Geonames Layer 2'); + }); + + test('Event detail responses array has entries from each layer', async () => { + // Close panel from previous test + await page.evaluate(() => { + const viewer = document.querySelector('[data-testid=viewer]'); + const panel = viewer.shadowRoot.querySelector('.mapml-search-panel'); + if (panel) { + panel.classList.remove('mapml-search-panel-open'); + panel.setAttribute('hidden', ''); + } + }); + await page.waitForTimeout(400); + + await page.evaluate(() => { + const viewer = document.querySelector('[data-testid=viewer]'); + viewer.addEventListener( + 'mapsuggestions', + (e) => { + viewer._testResponseCount = e.detail.responses.length; + viewer._testLayerLabels = e.detail.responses.map((r) => + r.layer.getAttribute('label') + ); + }, + { once: true } + ); + }); + + await page.click('.mapml-search-button'); + await page.waitForTimeout(400); + await page.fill('.mapml-search-input', 'multi'); + await page.waitForFunction( + () => + document.querySelector('[data-testid=viewer]')._testResponseCount !== + undefined, + { timeout: 5000 } + ); + + let responseCount = await page.$eval( + '[data-testid=viewer]', + (v) => v._testResponseCount + ); + let layerLabels = await page.$eval( + '[data-testid=viewer]', + (v) => v._testLayerLabels + ); + + expect(responseCount).toBe(2); + expect(layerLabels).toEqual(['Geonames Layer 1', 'Geonames Layer 2']); + }); + + test('Search (Enter) aggregates results from both layers', async () => { + // Clear input and search + await page.fill('.mapml-search-input', 'search-multi'); + await page.press('.mapml-search-input', 'Enter'); + // Wait for search results + await page.waitForSelector('.mapml-search-result', { timeout: 5000 }); + await page.waitForTimeout(500); + let texts = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.textContent) + ); + expect(texts).toEqual(['Ottawa', 'Toronto']); + }); + + test('Search results carry correct layer attribution', async () => { + let layers = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.dataset.layer) + ); + expect(layers[0]).toBe('Geonames Layer 1'); + expect(layers[1]).toBe('Geonames Layer 2'); + }); + + test('Unchecking a layer excludes it from results', async () => { + // Uncheck layer 2 + await page.evaluate(() => { + document.querySelector('[data-testid=layer2]').removeAttribute('checked'); + }); + await page.waitForTimeout(500); + + // Clear and re-type to trigger new suggestions + await page.fill('.mapml-search-input', ''); + await page.fill('.mapml-search-input', 'filtered'); + await page.waitForSelector('.mapml-search-result', { timeout: 5000 }); + await page.waitForTimeout(500); + + let texts = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.textContent) + ); + // Only layer 1 results (Ottawa, Arctic Ocean, Atlantic Ocean) + expect(texts).toEqual(['Ottawa', 'Arctic Ocean', 'Atlantic Ocean']); + + let layers = await page.$$eval('.mapml-search-result', (btns) => + btns.map((b) => b.dataset.layer) + ); + for (let label of layers) { + expect(label).toBe('Geonames Layer 1'); + } + }); +}); diff --git a/test/e2e/core/styleParsing.html b/test/e2e/core/styleParsing.html index 2eba13617..7922c9dce 100644 --- a/test/e2e/core/styleParsing.html +++ b/test/e2e/core/styleParsing.html @@ -5,7 +5,7 @@ Style Parsing Test - + @@ -11,9 +11,9 @@ - + - + - +
\ No newline at end of file diff --git a/test/e2e/core/tms.test.js b/test/e2e/core/tms.test.js index 9657a523b..b12addb5b 100644 --- a/test/e2e/core/tms.test.js +++ b/test/e2e/core/tms.test.js @@ -1,26 +1,38 @@ import { test, expect, chromium } from '@playwright/test'; -test.describe("Playwright Map Element Tests", () => { +test.describe('Playwright Map Element Tests', () => { let page; let context; test.beforeAll(async () => { - context = await chromium.launchPersistentContext(''); - page = context.pages().find((page) => page.url() === 'about:blank') || await context.newPage(); - await page.goto("tms.html"); + context = await chromium.launchPersistentContext('', { slowMo: 250 }); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('tms.html'); + await page.waitForTimeout(250); }); test.afterAll(async function () { await context.close(); }); - test("Painting tiles are in proper order", async () => { - let tileOrder = ["1/0/1", "1/0/0", "1/1/1", "1/1/0"] - for (let i = 0; i < 4; i++) { - const feature = await page.$eval( - `xpath=//html/body/mapml-viewer >> css=div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-overlay-pane > div > div.leaflet-layer.mapml-templatedlayer-container > div > div > div:nth-child(${i + 1}) > img`, - (tile) => tile.getAttribute("src") - ); - expect(feature).toEqual(`https://maps4html.org/TiledArt-Rousseau/TheBanksOfTheBièvreNearBicêtre/${tileOrder[i]}.png`); - } + test('Painting tiles are in proper order', async () => { + let tileOrder = ['1/0/1', '1/0/0', '1/1/1', '1/1/0']; + const firstTile = await page + .locator('mapml-viewer .mapml-tile-group:nth-child(1) > img') + .evaluate((img) => img.src); + const secondTile = await page + .locator('mapml-viewer .mapml-tile-group:nth-child(2) > img') + .evaluate((img) => img.src); + const thirdTile = await page + .locator('mapml-viewer .mapml-tile-group:nth-child(3) > img') + .evaluate((img) => img.src); + const fourthTile = await page + .locator('mapml-viewer .mapml-tile-group:nth-child(4) > img') + .evaluate((img) => img.src); + expect(firstTile).toContain(tileOrder[0]); + expect(secondTile).toContain(tileOrder[1]); + expect(thirdTile).toContain(tileOrder[2]); + expect(fourthTile).toContain(tileOrder[3]); }); -}); \ No newline at end of file +}); diff --git a/test/e2e/core/touchDevice.test.js b/test/e2e/core/touchDevice.test.js new file mode 100644 index 000000000..ef65bdab6 --- /dev/null +++ b/test/e2e/core/touchDevice.test.js @@ -0,0 +1,70 @@ +import { test, expect, chromium, devices } from '@playwright/test'; +const device = devices['Pixel 5']; + +test.describe('Playwright touch device tests', () => { + let page; + let context; + test.beforeAll(async () => { + // the test must be run in headless mode + // to successfully emulate a touch device with mouse disabled + context = await chromium.launchPersistentContext('', { + hasTouch: true + }); + page = await context.newPage({ + ...device + }); + await page.goto('layerContextMenu.html'); + }); + + test.afterAll(async function () { + await context.close(); + }); + + test('Tap/Long press to show layer control', async () => { + // possibly useful for debugging CI problems + // const isCI = process.env.CI === 'true'; // GitHub Actions sets CI=true + const layerControl = await page.locator('.leaflet-control-layers'); + await layerControl.tap(); + // does not pass on linux + // await expect(layerControl).toHaveClass(/leaflet-control-layers-expanded/); + await expect(layerControl).toHaveJSProperty('_isExpanded', true); + + // expect the opacity setting not open after the click + let opacity = await page.$eval( + '.leaflet-control-layers-overlays > fieldset:nth-child(1) > div.mapml-layer-item-properties > div > button:nth-child(2)', + (btn) => btn.getAttribute('aria-expanded') + ); + expect(opacity).toEqual('false'); + + const viewer = await page.locator('mapml-viewer'); + // tap on the map to dismiss the layer control + await viewer.tap({ position: { x: 150, y: 150 } }); + // tap on the lc to expand it + await layerControl.tap(); + // long press on layercontrol does not dismiss it + await layerControl.tap({ delay: 800 }); + // does not pass on linux + //await expect(layerControl).toHaveClass(/leaflet-control-layers-expanded/); + await expect(layerControl).toHaveJSProperty('_isExpanded', true); + + // expect the layer context menu to NOT show after the long press + const aHandle = await page.evaluateHandle(() => + document.querySelector('mapml-viewer') + ); + const nextHandle = await page.evaluateHandle( + (doc) => doc.shadowRoot, + aHandle + ); + const resultHandle = await page.evaluateHandle( + (root) => root.querySelector('.mapml-contextmenu.mapml-layer-menu'), + nextHandle + ); + const menuDisplay = await ( + await page.evaluateHandle( + (elem) => window.getComputedStyle(elem).getPropertyValue('display'), + resultHandle + ) + ).jsonValue(); + expect(menuDisplay).toEqual('none'); + }); +}); diff --git a/test/e2e/core/zoomChangeProjection.html b/test/e2e/core/zoomChangeProjection.html index d9c70f430..676b44cc7 100644 --- a/test/e2e/core/zoomChangeProjection.html +++ b/test/e2e/core/zoomChangeProjection.html @@ -3,14 +3,14 @@ Zoom Projection Change Test - + - + \ No newline at end of file diff --git a/test/e2e/core/zoomChangeProjection.test.js b/test/e2e/core/zoomChangeProjection.test.js index c3f0e7dc5..a8051bd6e 100644 --- a/test/e2e/core/zoomChangeProjection.test.js +++ b/test/e2e/core/zoomChangeProjection.test.js @@ -1,45 +1,66 @@ import { test, expect, chromium } from '@playwright/test'; -test.describe("Playwright zoomin zoomout Projection Change Tests", () => { +/* + * Skip this use case until we figure out how to use media queries to select + * links based on map scale, projection, zoom, extent etc. + * + * In the current configuration, the map is constrained to allow zooming only + * within the zoom bounds of its layers. So, if the layer only allows zoom + * levels 0-8 and it should give way to a different linked layer resource at + * zoom = 9, and THEN change projections due to it being the only layer on the + * map, there is a conflict between to maxZoom of the map (based on the initial + * layer's min/max Zoom) and the zoom at which the layer should change source. + * + * That mechanism should be re-designed, eliminating the need for the zoomin and + * zoomout link relations, by using links that have media queries on them, + * probably with a simple rel=alternate value. + */ +test.describe.skip('Playwright zoomin zoomout Projection Change Tests', () => { let page; let context; test.beforeAll(async () => { context = await chromium.launchPersistentContext(''); - page = context.pages().find((page) => page.url() === 'about:blank') || await context.newPage(); - await page.goto("zoomChangeProjection.html"); + page = + context.pages().find((page) => page.url() === 'about:blank') || + (await context.newPage()); + await page.goto('zoomChangeProjection.html'); }); test.afterAll(async function () { await context.close(); }); - test("zoomin link changes projections", async () => { - await page.click("div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-control-zoom.leaflet-bar.leaflet-control > a.leaflet-control-zoom-in"); + test('zoomin link changes projections', async () => { + await page.click( + 'div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-control-zoom.leaflet-bar.leaflet-control > a.leaflet-control-zoom-in' + ); await page.waitForTimeout(1000); const newProjection = await page.$eval( 'body > map', (map) => map.projection ); const layerValid = await page.$eval( - 'body > map > layer-', + 'body > map > map-layer', (layer) => !layer.hasAttribute('disabled') - ) - expect(newProjection).toEqual("OSMTILE"); + ); + expect(newProjection).toEqual('OSMTILE'); expect(layerValid).toEqual(true); }); - test("zoomout link changes projections", async () => { - await page.click("div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-control-zoom.leaflet-bar.leaflet-control > a.leaflet-control-zoom-out"); + test('zoomout link changes projections', async () => { + await page.click( + 'div > div.leaflet-control-container > div.leaflet-top.leaflet-left > div.leaflet-control-zoom.leaflet-bar.leaflet-control > a.leaflet-control-zoom-out' + ); await page.waitForTimeout(1000); const newProjection = await page.$eval( 'body > map', (map) => map.projection ); const layerValid = await page.$eval( - 'body > map > layer-', + 'body > map > map-layer', (layer) => !layer.hasAttribute('disabled') - ) - expect(newProjection).toEqual("CBMTILE"); + ); + expect(newProjection).toEqual('CBMTILE'); expect(layerValid).toEqual(true); }); -}); \ No newline at end of file +}); diff --git a/test/e2e/data/cbmt.mapml b/test/e2e/data/cbmt.mapml index 0f5164974..7f5b28d4a 100644 --- a/test/e2e/data/cbmt.mapml +++ b/test/e2e/data/cbmt.mapml @@ -1,4 +1,4 @@ - + Canada Base Map - Transportation (CBMT) @@ -6,7 +6,7 @@ - +