diff --git a/.dockerignore b/.dockerignore index fc409a8..8687cf3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,3 +7,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +examples/ diff --git a/.gitignore b/.gitignore index 6635cf5..08bef08 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +examples/assets/ diff --git a/.prettierignore b/.prettierignore index d715da5..14109c7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,5 @@ static pnpm-lock.yaml package-lock.json yarn.lock + +examples/assets/ diff --git a/README.md b/README.md index 15364ca..97fa2a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # MapColonies Playground + Code playground for mapping clients. The code examples are saved in S3 bucket and fetched and cached by the server. @@ -21,62 +22,63 @@ The code examples are saved in S3 bucket and fetched and cached by the server. The codes examples shown are defined by a json index file with all the data needed. All the files listed in the index are loaded from the same bucket as the index itself. ### example index + ```json { - "ol": { - "basic-ol": { - "displayName": "basic openlayers", - "files": ["openlayers_basic.js", "openlayers.css", "openlayers.html"], - "links": [ - { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, - { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } - ] - }, - "geojson-style-ol": { - "displayName": "geojson openlayers", - "files": ["openlayers_geojson.js", "openlayers.css", "openlayers.html"], - "links": [ - { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, - { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } - ] - } - }, - "cesium": { - "basic-cesium": { - "displayName": "basic cesium", - "files": ["cesium_basic.js", "cesium.html"], - "links": [ - { - "name": "cesium.js", - "url": "/libs/Cesium/Cesium.js", - "type": "js" - }, - { - "name": "widgets.css", - "url": "/libs/Cesium/Widgets/widgets.css", - "type": "css" - } - ] - } - }, - "leaflet": { - "basic-leaflet": { - "displayName": "basic leaflet", - "files": ["leaflet_basic.js", "leaflet.css", "leaflet.html"], - "links": [ - { - "name": "leaflet.js", - "url": "/libs/leaflet/1.9.4/leaflet.js", - "type": "js" - }, - { - "name": "leaflet.css", - "url": "/libs/leaflet/1.9.4/leaflet.css", - "type": "css" - } - ] - } - } + "ol": { + "basic-ol": { + "displayName": "basic openlayers", + "files": ["openlayers_basic.js", "openlayers.css", "openlayers.html"], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "geojson-style-ol": { + "displayName": "geojson openlayers", + "files": ["openlayers_geojson.js", "openlayers.css", "openlayers.html"], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + } + }, + "cesium": { + "basic-cesium": { + "displayName": "basic cesium", + "files": ["cesium_basic.js", "cesium.html"], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + } + }, + "leaflet": { + "basic-leaflet": { + "displayName": "basic leaflet", + "files": ["leaflet_basic.js", "leaflet.css", "leaflet.html"], + "links": [ + { + "name": "leaflet.js", + "url": "/libs/leaflet/1.9.4/leaflet.js", + "type": "js" + }, + { + "name": "leaflet.css", + "url": "/libs/leaflet/1.9.4/leaflet.css", + "type": "css" + } + ] + } + } } ``` diff --git a/examples/cesium-3d/3d-model.js b/examples/cesium-3d/3d-model.js new file mode 100644 index 0000000..9698ebd --- /dev/null +++ b/examples/cesium-3d/3d-model.js @@ -0,0 +1,75 @@ +import { TOKEN } from './config/common-config.js'; +import { + PRODUCT_ID as RASTER_PRODUCT_ID, + PRODUCT_TYPE as RASTER_PRODUCT_TYPE, + LAYER_IMAGE_FORMAT +} from './config/raster-config.js'; +import { + PRODUCT_ID as DEM_PRODUCT_ID, + PRODUCT_TYPE as DEM_PRODUCT_TYPE, + DEM_SCHEME +} from './config/dem-config.js'; +import { + PRODUCT_ID as MODEL_3D_PRODUCT_ID, + PRODUCT_TYPE as MODEL_3D_PRODUCT_TYPE, + MODEL_3D_SCHEME +} from './config/3d-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +Promise.all([ + fetchWmtsTileTemplate(RASTER_PRODUCT_ID, RASTER_PRODUCT_TYPE, LAYER_IMAGE_FORMAT), + fetchServiceLink('dem', DEM_PRODUCT_ID, DEM_PRODUCT_TYPE, DEM_SCHEME), + fetchServiceLink('3d', MODEL_3D_PRODUCT_ID, MODEL_3D_PRODUCT_TYPE, MODEL_3D_SCHEME) +]).then(([raster, dem, model]) => { + const viewer = new Cesium.Viewer('cesiumContainer', { + baseLayer: new Cesium.ImageryLayer( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: raster.template, + queryParameters: { + token: TOKEN + } + }), + layer: raster.name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'WorldCRS84', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ), + terrainProvider: new Cesium.CesiumTerrainProvider({ + url: new Cesium.Resource({ + url: dem.url, + queryParameters: { + token: TOKEN + } + }) + }) + }); + + viewer.scene.primitives.add( + new Cesium.Cesium3DTileset({ + url: new Cesium.Resource({ + url: model.url, + queryParameters: { + token: TOKEN + } + }), + maximumScreenSpaceError: 5, + cullRequestsWhileMovingMultiplier: 120, + preloadFlightDestination: true, + preferLeaves: true, + skipLevelOfDetail: true + }) + ); + + viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(35.201436, 33.265378, 300), + orientation: { + heading: Cesium.Math.toRadians(25.0), + pitch: Cesium.Math.toRadians(-10.0), + roll: 0.0 + } + }); +}); diff --git a/examples/cesium-3d/cesium.html b/examples/cesium-3d/cesium.html new file mode 100644 index 0000000..95872da --- /dev/null +++ b/examples/cesium-3d/cesium.html @@ -0,0 +1 @@ +
diff --git a/examples/cesium-geocoding/cesium.html b/examples/cesium-geocoding/cesium.html new file mode 100644 index 0000000..95872da --- /dev/null +++ b/examples/cesium-geocoding/cesium.html @@ -0,0 +1 @@ +
diff --git a/examples/cesium-geocoding/cesium.js b/examples/cesium-geocoding/cesium.js new file mode 100644 index 0000000..14417e2 --- /dev/null +++ b/examples/cesium-geocoding/cesium.js @@ -0,0 +1,64 @@ +'use strict'; +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, LAYER_IMAGE_FORMAT } from './config/raster-config.js'; +import { GEOCODING_URL } from './config/vector-config.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +function OpenStreetMapNominatimGeocoder() {} + +OpenStreetMapNominatimGeocoder.prototype.geocode = function (input) { + const resource = new Cesium.Resource({ + url: GEOCODING_URL, + queryParameters: { + format: 'json', + q: input, + token: TOKEN + }, + headers: { + 'accept-language': 'he' + } + }); + + return resource.fetchJson().then(function (results) { + let bboxDegrees; + return results.map(function (resultObject) { + bboxDegrees = resultObject.boundingbox; + return { + displayName: resultObject.display_name, + destination: Cesium.Rectangle.fromDegrees( + bboxDegrees[2], + bboxDegrees[0], + bboxDegrees[3], + bboxDegrees[1] + ) + }; + }); + }); +}; + +fetchWmtsTileTemplate(PRODUCT_ID, PRODUCT_TYPE, LAYER_IMAGE_FORMAT).then(({ template, name }) => { + new Cesium.Viewer('cesiumContainer', { + vrButton: false, + homeButton: false, + infoBox: false, + timeline: false, + navigationHelpButton: false, + shouldAnimate: false, + baseLayer: new Cesium.ImageryLayer( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: template, + queryParameters: { + token: TOKEN + } + }), + layer: name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'WorldCRS84', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ), + geocoder: new OpenStreetMapNominatimGeocoder() + }); +}); diff --git a/examples/cesium-layers-split/split-layers.css b/examples/cesium-layers-split/split-layers.css new file mode 100644 index 0000000..a5b0d59 --- /dev/null +++ b/examples/cesium-layers-split/split-layers.css @@ -0,0 +1,13 @@ +#slider { + position: absolute; + left: 50%; + top: 0px; + background-color: #d3d3d3; + width: 5px; + height: 100%; + z-index: 9999; +} + +#slider:hover { + cursor: ew-resize; +} diff --git a/examples/cesium-layers-split/split-layers.html b/examples/cesium-layers-split/split-layers.html new file mode 100644 index 0000000..61c6b90 --- /dev/null +++ b/examples/cesium-layers-split/split-layers.html @@ -0,0 +1,4 @@ +
+
+
+
diff --git a/examples/cesium-layers-split/split-layers.js b/examples/cesium-layers-split/split-layers.js new file mode 100644 index 0000000..ca2b78e --- /dev/null +++ b/examples/cesium-layers-split/split-layers.js @@ -0,0 +1,83 @@ +'use strict'; +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, LAYER_IMAGE_FORMAT } from './config/raster-config.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +Promise.all([ + fetchWmtsTileTemplate(PRODUCT_ID, PRODUCT_TYPE, LAYER_IMAGE_FORMAT), + fetchWmtsTileTemplate('OSM', 'RasterVectorBest', LAYER_IMAGE_FORMAT) +]).then(([main, second]) => { + const viewer = new Cesium.Viewer('cesiumContainer', { + baseLayer: new Cesium.ImageryLayer( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: main.template, + queryParameters: { + token: TOKEN + } + }), + layer: main.name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'WorldCRS84', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ), + baseLayerPicker: false, + infoBox: false + }); + + const layers = viewer.imageryLayers; + const secondLayer = layers.addImageryProvider( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: second.template, + queryParameters: { + token: TOKEN + } + }), + layer: second.name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'WorldCRS84', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ); + secondLayer.splitDirection = Cesium.SplitDirection.LEFT; // Only show to the left of the slider. + + // Sync the position of the slider with the split position + const slider = document.getElementById('slider'); + viewer.scene.splitPosition = slider.offsetLeft / slider.parentElement.offsetWidth; + + const handler = new Cesium.ScreenSpaceEventHandler(slider); + + let moveActive = false; + + function move(movement) { + if (!moveActive) { + return; + } + + const relativeOffset = movement.endPosition.x; + const splitPosition = (slider.offsetLeft + relativeOffset) / slider.parentElement.offsetWidth; + slider.style.left = `${100.0 * splitPosition}%`; + viewer.scene.splitPosition = splitPosition; + } + + handler.setInputAction(function () { + moveActive = true; + }, Cesium.ScreenSpaceEventType.LEFT_DOWN); + handler.setInputAction(function () { + moveActive = true; + }, Cesium.ScreenSpaceEventType.PINCH_START); + + handler.setInputAction(move, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + handler.setInputAction(move, Cesium.ScreenSpaceEventType.PINCH_MOVE); + + handler.setInputAction(function () { + moveActive = false; + }, Cesium.ScreenSpaceEventType.LEFT_UP); + handler.setInputAction(function () { + moveActive = false; + }, Cesium.ScreenSpaceEventType.PINCH_END); +}); diff --git a/examples/cesium-terrain/cesium.html b/examples/cesium-terrain/cesium.html new file mode 100644 index 0000000..95872da --- /dev/null +++ b/examples/cesium-terrain/cesium.html @@ -0,0 +1 @@ +
diff --git a/examples/cesium-terrain/cesium.js b/examples/cesium-terrain/cesium.js new file mode 100644 index 0000000..1424df5 --- /dev/null +++ b/examples/cesium-terrain/cesium.js @@ -0,0 +1,73 @@ +import { TOKEN } from './config/common-config.js'; +import { + PRODUCT_ID as RASTER_PRODUCT_ID, + PRODUCT_TYPE as RASTER_PRODUCT_TYPE, + LAYER_IMAGE_FORMAT +} from './config/raster-config.js'; +import { + PRODUCT_ID as DEM_PRODUCT_ID, + PRODUCT_TYPE as DEM_PRODUCT_TYPE, + DEM_SCHEME +} from './config/dem-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +Promise.all([ + fetchWmtsTileTemplate(RASTER_PRODUCT_ID, RASTER_PRODUCT_TYPE, LAYER_IMAGE_FORMAT), + fetchServiceLink('dem', DEM_PRODUCT_ID, DEM_PRODUCT_TYPE, DEM_SCHEME) +]).then(([raster, dem]) => { + const viewer = new Cesium.Viewer('cesiumContainer', { + baseLayer: new Cesium.ImageryLayer( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: raster.template, + queryParameters: { + token: TOKEN + } + }), + layer: raster.name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'newGrids', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ), + terrainProvider: new Cesium.CesiumTerrainProvider({ + url: new Cesium.Resource({ + url: dem.url, + queryParameters: { + token: TOKEN + } + }) + }) + }); + + viewer.camera.flyTo({ + destination: Cesium.Cartesian3.fromDegrees(35.567306, 33.210784, 6000), + orientation: { + heading: Cesium.Math.toRadians(25.0), + pitch: Cesium.Math.toRadians(-10.0), + roll: 0.0 + } + }); + + fetchWmtsTileTemplate('WORLD_MAP_BASE_THIN', 'RasterVectorBest', 'image/png').then( + ({ template, name }) => { + viewer.imageryLayers.addImageryProvider( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: template, + queryParameters: { + token: TOKEN + } + }), + layer: name, + style: 'default', + format: 'image/png', + tileMatrixSetID: 'newGrids', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ); + } + ); +}); diff --git a/examples/cesium-viewshed/viewshed.css b/examples/cesium-viewshed/viewshed.css new file mode 100644 index 0000000..e69de29 diff --git a/examples/cesium-viewshed/viewshed.html b/examples/cesium-viewshed/viewshed.html new file mode 100644 index 0000000..95872da --- /dev/null +++ b/examples/cesium-viewshed/viewshed.html @@ -0,0 +1 @@ +
diff --git a/examples/cesium-viewshed/viewshed.js b/examples/cesium-viewshed/viewshed.js new file mode 100644 index 0000000..874cfeb --- /dev/null +++ b/examples/cesium-viewshed/viewshed.js @@ -0,0 +1,713 @@ +import { TOKEN } from './config/common-config.js'; +import { + PRODUCT_ID as RASTER_PRODUCT_ID, + PRODUCT_TYPE as RASTER_PRODUCT_TYPE, + LAYER_IMAGE_FORMAT +} from './config/raster-config.js'; +import { + PRODUCT_ID as DEM_PRODUCT_ID, + PRODUCT_TYPE as DEM_PRODUCT_TYPE, + DEM_SCHEME +} from './config/dem-config.js'; +import { + PRODUCT_ID as MODEL_3D_PRODUCT_ID, + PRODUCT_TYPE as MODEL_3D_PRODUCT_TYPE, + MODEL_3D_SCHEME +} from './config/3d-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +const fsShaderText = ` +#define USE_NORMAL_SHADING +uniform float view_distance; // Maximum distance for shadow effect +uniform vec3 viewArea_color; // Color for visible areas +uniform vec3 shadowArea_color; // Color for invisible areas +uniform float percentShade; // Mix number for color blending +uniform sampler2D colorTexture; // Texture for color +uniform sampler2D shadowMap; // Shadow map texture +uniform sampler2D depthTexture; // Depth texture +uniform mat4 shadowMap_matrix; // Shadow map matrix +uniform vec3 viewPosition_WC; // Uniform for view position +uniform vec3 cameraPosition_WC; // Uniform for camera position +uniform vec4 shadowMap_camera_positionEC; // Light position in eye coordinates +uniform vec4 shadowMap_camera_directionEC; // Light direction in eye coordinates +uniform vec3 ellipsoidInverseRadii; +uniform vec3 shadowMap_camera_up; // Light up direction +uniform vec3 shadowMap_camera_dir; // Light direction +uniform vec3 shadowMap_camera_right; // Light right direction +uniform vec4 shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness; // Shadow map parameters +uniform vec4 shadowMap_texelSizeDepthBiasAndNormalShadingSmooth; // Shadow map parameters +uniform vec4 _shadowMap_cascadeSplits[2]; +uniform mat4 _shadowMap_cascadeMatrices[4]; +uniform vec4 _shadowMap_cascadeDistances; +uniform bool exclude_terrain; + +in vec2 v_textureCoordinates; +out vec4 FragColor; + +vec4 toEye(in vec2 uv, in float depth){ + float x = uv.x * 2.0 - 1.0; + float y = uv.y * 2.0 - 1.0; + vec4 camPosition = czm_inverseProjection * vec4(x, y, depth, 1.0); + float reciprocalW = 1.0 / camPosition.w; + camPosition *= reciprocalW; + return camPosition; +} + +// This function gets the depth from a depth texture. +float getDepth(in vec4 depth){ + // Unpack the depth value from the depth texture + float z_window = czm_unpackDepth(depth); + // Reverse the logarithmic depth value to get the linear depth + z_window = czm_reverseLogDepth(z_window); + // Get the near and far values of the depth range + float n_range = czm_depthRange.near; + float f_range = czm_depthRange.far; + // Convert the depth value from window coordinates to normalized device coordinates + return (2.0 * z_window - n_range - f_range) / (f_range - n_range); +} + +/** + * Projects a point onto a plane. + * + * @param planeNormal - A vector representing the normal of the plane. + * @param planeOrigin - A point on the plane. + * @param point - The point to be projected onto the plane. + * @return The projection of the point on the plane. + */ +vec3 pointProjectOnPlane(in vec3 planeNormal, in vec3 planeOrigin, in vec3 point){ + // Calculate the vector from the plane origin to the point + vec3 v01 = point - planeOrigin; + + // Calculate the perpendicular distance from the point to the plane + float d = dot(planeNormal, v01); + + // Subtract the product of the plane normal and d from the point + // to get the projection of the point on the plane + return (point - planeNormal * d); +} + +/** + * Calculates the magnitude (length) of a vector. + * + * @param pt - The input vector. + * @return The magnitude of the vector. + */ +float point2mag(vec3 point){ + // Square each component of the vector, add them together, + // and take the square root of the result + return sqrt(point.x*point.x + point.y*point.y + point.z*point.z); +} + +/** + * Main function for the fragment shader. + */ +void main() +{ + // Get the color and depth at the current texture coordinates + vec4 color = texture(colorTexture, v_textureCoordinates); + vec4 cDepth = texture(depthTexture, v_textureCoordinates); + + // Get the depth and position in eye coordinates + float depth = getDepth(cDepth); + vec4 positionEC = toEye(v_textureCoordinates, depth); + + // If the depth is at its maximum value, set the fragment color to the texture color and return + if(cDepth.r >= 1.0){ + FragColor = color; + return; + } + + //check to see if we are within distance of the view target + float cameraDistance = length(cameraPosition_WC.xyz - viewPosition_WC.xyz); + + // Get the fragment position in world coordinates + vec4 fragPosition_WC = vec4(v_textureCoordinates, 0.0, 1.0); + + if ( + cDepth.r >= 1.0 || + (exclude_terrain && czm_ellipsoidContainsPoint(ellipsoidInverseRadii, positionEC.xyz)) + ){ + FragColor = color; + return; + } + + // Initialize shadow parameters + czm_shadowParameters shadowParameters; + shadowParameters.texelStepSize = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.xy; + shadowParameters.depthBias = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.z; + shadowParameters.normalShadingSmooth = shadowMap_texelSizeDepthBiasAndNormalShadingSmooth.w; + shadowParameters.darkness = shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness.w; + + // Adjust the depth bias + shadowParameters.depthBias *= max(depth * 0.01, 1.0); + + // Calculate the direction in eye coordinates + vec3 directionEC = normalize(positionEC.xyz - shadowMap_camera_positionEC.xyz); + + // Calculate the dot product of the normal and the negative direction + float nDotL = clamp(dot(vec3(1.0), -directionEC), 0.0, 1.0); + + // Calculate the shadow position + vec4 shadowPosition = shadowMap_matrix * positionEC; + shadowPosition /= shadowPosition.w; + + // If the shadow position is outside the [0, 1] range in any dimension, set the fragment color to the texture color and return + if (any(lessThan(shadowPosition.xyz, vec3(0.0))) || any(greaterThan(shadowPosition.xyz, vec3(1.0)))) + { + FragColor = color; + return; + } + + // If the distance between the coordinates and the viewpoint is greater than the maximum distance, the shadow effect is discarded + vec4 lw = czm_inverseView* vec4(shadowMap_camera_positionEC.xyz, 1.0); + vec4 vw = czm_inverseView* vec4(positionEC.xyz, 1.0); + + if(distance(lw.xyz,vw.xyz)>view_distance){ + FragColor = color; + return; + } + + // Set the shadow parameters + shadowParameters.texCoords = shadowPosition.xy; + shadowParameters.depth = shadowPosition.z; + shadowParameters.nDotL = nDotL; + + // Calculate the shadow visibility + float visibility = czm_shadowVisibility(shadowMap, shadowParameters); + + // If the visibility is 1.0, mix the color with the visible color + if(visibility==1.0){ + FragColor = mix(texture(colorTexture, v_textureCoordinates),vec4(viewArea_color,1.0),percentShade); + }else{ + if(abs(shadowPosition.z-0.0)<0.01){ + FragColor = color; + return; + } + FragColor = mix(texture(colorTexture, v_textureCoordinates),vec4(shadowArea_color,1.0),percentShade); + } +}`; +const fsShader = fsShaderText.replace('`;', ''); +//https://github.com/DigitalArsenal/SensorShadow +const { + ShadowMap, + PerspectiveFrustum, + Camera, + Color, + defaultValue, + PositionProperty, + ConstantPositionProperty, + Cartesian2, + Cartesian3, + Cartesian4, + EllipsoidTerrainProvider, + PostProcessStage, + Math: CesiumMath +} = Cesium; + +const defaultValues = { + cameraPosition: new ConstantPositionProperty(), + viewPosition: new ConstantPositionProperty(), + viewAreaColor: new Color(0, 1, 0), + shadowAreaColor: new Color(1, 0, 0), + alpha: 0.5, + frustum: true, + size: 4096, + depthBias: 2e-12 +}; + +/** + * SensorShadow Class. + * This class handles the creation, update and management of sensor shadow entities. + * + * @property {Object} viewer - A reference to the Cesium viewer instance. + * @property {ConstantPositionProperty|PositionProperty|Cartesian3} cameraPosition - The camera position. + * @property {ConstantPositionProperty|PositionProperty|Cartesian3} viewPosition - The view position. + * @property {Color} viewAreaColor - The color of the visible area of the sensor shadow. + * @property {Color} shadowAreaColor - The color of the hidden area of the sensor shadow. + * @property {number} alpha - The alpha value for the sensor shadow. + * @property {boolean} frustum - Whether the frustum is enabled. + * @property {number} size - The size of the sensor shadow. + * @property {function|null} preUpdateListener - A pre-update listener function. + */ +class SensorShadow { + /** + * Constructs a new SensorShadow instance. + * + * @param {Object} viewer - A reference to the Cesium viewer instance. + * @param {Object} options - An optional configuration object. + * + * @example + * let sensorShadow = new SensorShadow(viewer, { + * cameraPosition: new Cartesian3(0, 0, 0), + * viewPosition: new Cartesian3(1, 1, 1), + * viewAreaColor: new Color(0, 1, 0), + * shadowAreaColor: new Color(1, 0, 0), + * alpha: 0.5, + * frustum: true, + * size: 512 + * }); + */ + constructor(viewer, options = {}) { + this.viewer = viewer; + + this.cameraPosition = + typeof options.cameraPosition.getValue === 'function' + ? options.cameraPosition + : new ConstantPositionProperty(options.cameraPosition); + + this.viewPosition = + typeof options.viewPosition.getValue === 'function' + ? options.viewPosition + : new ConstantPositionProperty(options.viewPosition); + + this.viewAreaColor = defaultValue(options.viewAreaColor, defaultValues.viewAreaColor); + + this.shadowAreaColor = defaultValue(options.shadowAreaColor, defaultValues.shadowAreaColor); + + this.alpha = defaultValue(options.alpha, defaultValues.alpha); + this.size = defaultValue(options.size, defaultValues.size); + this.frustum = defaultValue(options.frustum, defaultValues.frustum); + this.depthBias = defaultValue(options.depthBias, defaultValues.depthBias); + + this.preUpdateListener = null; + + if (this.cameraPosition && this.viewPosition) { + this._addToScene(); + } + } + + /** + * Get the actual position of the camera. + * This method calculates the position vector based on the current time. + * + * @private + * @returns {Cartesian3} The calculated camera position vector. + */ + get _getVectors() { + let positionVector = this.cameraPosition.getValue(this.viewer.clock.currentTime); + let viewVector = this.viewPosition.getValue(this.viewer.clock.currentTime); + let distanceBetweenVectors = Number(Cartesian3.distance(viewVector, positionVector).toFixed(1)); + + if (distanceBetweenVectors > 10000) { + let multiple = 1 - 10000 / distanceBetweenVectors; + positionVector = Cartesian3.lerp(positionVector, viewVector, multiple, new Cartesian3()); + } + + return { positionVector, viewVector }; + } + + /** + * Adds the SensorShadow to the scene. + * + * @private + */ + _addToScene() { + this._createShadowMap(); + this._addPostProcess(); + this.viewer.scene.primitives.add(this); + } + + /** + * Creates the shadow map. + * + * @private + */ + _createShadowMap(updateOnly) { + let { positionVector, viewVector } = this._getVectors; + + const distance = Number(Cartesian3.distance(viewVector, positionVector).toFixed(1)); + + if (distance > 10000) { + const multiple = 1 - 10000 / distance; + positionVector = Cartesian3.lerp(positionVector, viewVector, multiple, new Cartesian3()); + } + + const scene = this.viewer.scene; + + const camera = new Camera(scene); + + camera.position = positionVector; + + camera.direction = Cartesian3.subtract(viewVector, positionVector, new Cartesian3(0, 0, 0)); + + camera.up = Cartesian3.normalize(positionVector, new Cartesian3(0, 0, 0)); + + camera.frustum = new PerspectiveFrustum({ + fov: CesiumMath.toRadians(120), + aspectRatio: scene.canvas.clientWidth / scene.canvas.clientHeight, + near: 0.1, + far: distance + }); + + if (!updateOnly) { + this.viewShadowMap = new ShadowMap({ + lightCamera: camera, + enable: true, + isPointLight: false, + isSpotLight: true, + cascadesEnabled: false, + context: scene.context, + size: this.size, + pointLightRadius: distance, + fromLightSource: false, + maximumDistance: distance + }); + } else { + this.viewShadowMap._lightCamera.position = positionVector; + } + + this.viewShadowMap.normalOffset = true; + this.viewShadowMap._terrainBias.depthBias = 0.0; + } + + /** + * Adds post processing to the SensorShadow. + * + * @private + */ + _addPostProcess() { + const SensorShadow = this; + + const viewShadowMap = this.viewShadowMap; + const primitiveBias = viewShadowMap._isPointLight + ? viewShadowMap._pointBias + : viewShadowMap._primitiveBias; + this.postProcess = this.viewer.scene.postProcessStages.add( + new PostProcessStage({ + fragmentShader: fsShader, + uniforms: { + view_distance: function () { + return SensorShadow.distance; + }, + viewArea_color: function () { + return SensorShadow.viewAreaColor; + }, + shadowArea_color: function () { + return SensorShadow.shadowAreaColor; + }, + percentShade: function () { + return SensorShadow.alpha; + }, + shadowMap: function () { + return viewShadowMap._shadowMapTexture; + }, + _shadowMap_cascadeSplits: function () { + return viewShadowMap._cascadeSplits; + }, + _shadowMap_cascadeMatrices: function () { + return viewShadowMap._cascadeMatrices; + }, + _shadowMap_cascadeDistances: function () { + return viewShadowMap._cascadeDistances; + }, + shadowMap_matrix: function () { + return viewShadowMap._shadowMapMatrix; + }, + shadowMap_camera_positionEC: function () { + return viewShadowMap._lightPositionEC; + }, + shadowMap_camera_directionEC: function () { + return viewShadowMap._lightDirectionEC; + }, + cameraPosition_WC: function () { + return SensorShadow.viewer.camera.positionWC; + }, + viewPosition_WC: function () { + return SensorShadow.viewPosition.getValue(SensorShadow.viewer.clock.currentTime); + }, + shadowMap_camera_up: function () { + return viewShadowMap._lightCamera.up; + }, + shadowMap_camera_dir: function () { + return viewShadowMap._lightCamera.direction; + }, + shadowMap_camera_right: function () { + return viewShadowMap._lightCamera.right; + }, + ellipsoidInverseRadii: function () { + let radii = SensorShadow.viewer.scene.globe.ellipsoid.radii; + return new Cartesian3(1 / radii.x, 1 / radii.y, 1 / radii.z); + }, + shadowMap_texelSizeDepthBiasAndNormalShadingSmooth: function () { + var viewShed2D = new Cartesian2(); + viewShed2D.x = 1 / viewShadowMap._textureSize.x; + viewShed2D.y = 1 / viewShadowMap._textureSize.y; + + return Cartesian4.fromElements( + viewShed2D.x, + viewShed2D.y, + this.depthBias, + primitiveBias.normalShadingSmooth, + this.combinedUniforms1 + ); + }, + shadowMap_normalOffsetScaleDistanceMaxDistanceAndDarkness: function () { + return Cartesian4.fromElements( + primitiveBias.normalOffsetScale, + viewShadowMap._distance, + viewShadowMap.maximumDistance, + viewShadowMap._darkness, + this.combinedUniforms2 + ); + }, + exclude_terrain: function () { + return SensorShadow.viewer.terrainProvider instanceof EllipsoidTerrainProvider; + } + } + }) + ); + + // If a previous listener was added, remove it + if (this.preUpdateListener) { + viewer.scene.preUpdate.removeEventListener(this.preUpdateListener); + } + + // Add a new listener + this.preUpdateListener = () => { + if (!this.viewShadowMap._shadowMapTexture) { + this.postProcess.enabled = false; + } else { + this.postProcess.enabled = true; + } + }; + + viewer.scene.preUpdate.addEventListener(this.preUpdateListener); + } + + update(frameState) { + this._createShadowMap(true); + frameState.shadowMaps.push(this.viewShadowMap); + } + + destroy() { + if (this.preUpdateListener) { + viewer.scene.preUpdate.removeEventListener(this.preUpdateListener); + } + this.viewer.scene.postProcessStages.remove(this.postProcess); + for (let property in this) { + if (this.hasOwnProperty(property)) { + delete this[property]; + } + } + } + + get size() { + return this._size; + } + + set size(v) { + this._size = v; + } + + get depthBias() { + return this._depthBias; + } + + set depthBias(v) { + this._depthBias = v; + } + + get cameraPosition() { + return this._cameraPosition; + } + + set cameraPosition(v) { + this._cameraPosition = v; + } + + get viewPosition() { + return this._viewPosition; + } + + set viewPosition(v) { + this._viewPosition = v; + } + + get frustum() { + return this._frustum; + } + + set frustum(v) { + this._frustum = v; + } + + get distance() { + return this._distance; + } + + set distance(v) { + this._distance = v; + } + + get viewAreaColor() { + return this._viewAreaColor; + } + + set viewAreaColor(v) { + this._viewAreaColor = v; + } + + get shadowAreaColor() { + return this._shadowAreaColor; + } + + set shadowAreaColor(v) { + this._shadowAreaColor = v; + } + + get alpha() { + return this._alpha; + } + + set alpha(v) { + this._alpha = v; + } +} + +let pointA = Cesium.Cartesian3.fromDegrees(35.198213, 33.264289, 250); // Central Park + +let pointB = Cesium.Cartesian3.fromDegrees(35.200014, 33.268811, 40); // Empire State Building + +let viewer; +Promise.all([ + fetchWmtsTileTemplate(RASTER_PRODUCT_ID, RASTER_PRODUCT_TYPE, LAYER_IMAGE_FORMAT), + fetchServiceLink('dem', DEM_PRODUCT_ID, DEM_PRODUCT_TYPE, DEM_SCHEME), + fetchServiceLink('3d', MODEL_3D_PRODUCT_ID, MODEL_3D_PRODUCT_TYPE, MODEL_3D_SCHEME) +]).then(([raster, dem, model]) => { + viewer = new Cesium.Viewer('cesiumContainer', { + baseLayer: new Cesium.ImageryLayer( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: raster.template, + queryParameters: { + token: TOKEN + } + }), + layer: raster.name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'WorldCRS84', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ), + terrainProvider: new Cesium.CesiumTerrainProvider({ + url: new Cesium.Resource({ + url: dem.url, + queryParameters: { + token: TOKEN + } + }) + }) + }); + + viewer.scene.primitives.add( + new Cesium.Cesium3DTileset({ + url: new Cesium.Resource({ + url: model.url, + queryParameters: { + token: TOKEN + } + }), + maximumScreenSpaceError: 5, + cullRequestsWhileMovingMultiplier: 120, + preloadFlightDestination: true, + preferLeaves: true, + skipLevelOfDetail: true + }) + ); + + viewer.camera.flyTo({ + destination: pointA, + orientation: { + heading: Cesium.Math.toRadians(25.0), + pitch: Cesium.Math.toRadians(-10.0), + roll: 0.0 + } + }); + viewer.camera.moveEnd.addEventListener(function () { + const { camera } = viewer; + if (camera.position?.clone) { + const cameraState = { + position: camera.position.clone(), + direction: camera.direction.clone(), + up: camera.up.clone() + }; + } + }); + + viewer.clock.shouldAnimate = false; + //return; + const redBall = viewer.entities.add({ + position: pointA, + point: { + pixelSize: 10, + color: Cesium.Color.RED + } + }); + + //return; + const blueBall = viewer.entities.add({ + position: pointB, + point: { + pixelSize: 10, + color: Cesium.Color.BLUE + } + }); + + //@ts-ignore + var sensorShadowInstance = new SensorShadow(viewer, { + cameraPosition: redBall.position, + viewPosition: pointB + }); + + let handler; + let pickedEntity; + let cartesian; + handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas); + handler.setInputAction((click) => { + cartesian = viewer.camera.pickEllipsoid(click.position, viewer.scene.globe.ellipsoid); + if (cartesian) { + var pickedObject = viewer.scene.pick(click.position); + if (Cesium.defined(pickedObject) && pickedObject.id === redBall) { + pickedEntity = pickedObject.id; + viewer.scene.screenSpaceCameraController.enableInputs = false; + } + } + }, Cesium.ScreenSpaceEventType.LEFT_DOWN); + + handler.setInputAction((movement) => { + if (pickedEntity) { + let newCartesian = viewer.camera.pickEllipsoid( + movement.endPosition, + viewer.scene.globe.ellipsoid + ); + if (newCartesian) { + // Convert the picked Cartesian3 to Cartographic + let newCartographic = Cesium.Cartographic.fromCartesian(newCartesian); + + // Get the original height + let originalCartographic = Cesium.Cartographic.fromCartesian( + pickedEntity.position.getValue(Cesium.JulianDate.now()) + ); + + // Update the height to the original one + newCartographic.height = originalCartographic.height; + + // Convert the updated Cartographic back to Cartesian3 + let updatedCartesian = Cesium.Cartographic.toCartesian(newCartographic); + + // Set the new position + pickedEntity.position = new Cesium.ConstantPositionProperty(updatedCartesian); + sensorShadowInstance.cameraPosition = pickedEntity.position; + } + } + }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + + handler.setInputAction(() => { + if (pickedEntity) { + pickedEntity = undefined; + viewer.scene.screenSpaceCameraController.enableInputs = true; + } + }, Cesium.ScreenSpaceEventType.LEFT_UP); +}); diff --git a/examples/cesium-wmts/cesium.html b/examples/cesium-wmts/cesium.html new file mode 100644 index 0000000..95872da --- /dev/null +++ b/examples/cesium-wmts/cesium.html @@ -0,0 +1 @@ +
diff --git a/examples/cesium-wmts/cesium.js b/examples/cesium-wmts/cesium.js new file mode 100644 index 0000000..af72d81 --- /dev/null +++ b/examples/cesium-wmts/cesium.js @@ -0,0 +1,23 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, LAYER_IMAGE_FORMAT } from './config/raster-config.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +fetchWmtsTileTemplate(PRODUCT_ID, PRODUCT_TYPE, LAYER_IMAGE_FORMAT).then(({ template, name }) => { + new Cesium.Viewer('cesiumContainer', { + baseLayer: new Cesium.ImageryLayer( + new Cesium.WebMapTileServiceImageryProvider({ + url: new Cesium.Resource({ + url: template, + queryParameters: { + token: TOKEN + } + }), + layer: name, + style: 'default', + format: LAYER_IMAGE_FORMAT, + tileMatrixSetID: 'WorldCRS84', + tilingScheme: new Cesium.GeographicTilingScheme() + }) + ) + }); +}); diff --git a/examples/config/3d-config.js b/examples/config/3d-config.js new file mode 100644 index 0000000..a37e778 --- /dev/null +++ b/examples/config/3d-config.js @@ -0,0 +1,6 @@ +import { MAPCOLONIES_TILES_URL } from './config/common-config.js'; + +export var MODEL_3D_SCHEME = '3DTiles'; +export var PRODUCT_ID = 'd03ee59f-1676-4059-84cb-a0f68f15aefe'; +export var PRODUCT_TYPE = '3DPhotoRealistic'; +export var MODEL_3D_URL = `${MAPCOLONIES_TILES_URL}/api/3d/v1/b3dm/${PRODUCT_ID}/tileset.json`; diff --git a/examples/config/common-config.js b/examples/config/common-config.js new file mode 100644 index 0000000..3f60bf9 --- /dev/null +++ b/examples/config/common-config.js @@ -0,0 +1,5 @@ +export var MAPCOLONIES_TILES_URL = 'TILES_URL'; +export var MAPCOLONIES_QUERY_URL = 'QUERY_URL'; +export var MAPCOLONIES_GEOCODING_URL = 'GEOCODING_URL'; +export var MAPCOLONIES_CATALOG_URL = 'CATALOG_URL'; +export var TOKEN = 'TOKEN'; diff --git a/examples/config/dem-config.js b/examples/config/dem-config.js new file mode 100644 index 0000000..a8159de --- /dev/null +++ b/examples/config/dem-config.js @@ -0,0 +1,6 @@ +import { MAPCOLONIES_TILES_URL } from './config/common-config.js'; + +export var DEM_SCHEME = 'WCS'; +export var PRODUCT_ID = 'srtm_100_30-aoi'; +export var PRODUCT_TYPE = 'DTM'; +export var DEM_URL = `${MAPCOLONIES_TILES_URL}/api/dem/v1/terrains/${PRODUCT_ID}`; diff --git a/examples/config/raster-config.js b/examples/config/raster-config.js new file mode 100644 index 0000000..ed42d8c --- /dev/null +++ b/examples/config/raster-config.js @@ -0,0 +1,13 @@ +import { MAPCOLONIES_TILES_URL, TOKEN } from './config/common-config.js'; + +var WMTS_BASE_URL = `${MAPCOLONIES_TILES_URL}/api/raster/v1/wmts`; + +export var RASTER_SCHEME = 'WMTS'; +export var PRODUCT_ID = 'blueMarble'; +export var PRODUCT_TYPE = 'Orthophoto'; +export var LAYER_NAME = `${PRODUCT_ID}-${PRODUCT_TYPE}`; +export var ADDITIONAL_LAYER_NAME = `${PRODUCT_ID}-${PRODUCT_TYPE}`; +export var LAYER_IMAGE_FORMAT = 'image/png'; +export var RASTER_SERVICE_URL = `${MAPCOLONIES_TILES_URL}/api/raster/v1/service`; +export var WMTS_CAPABILITIES_URL = `${WMTS_BASE_URL}/1.0.0/WMTSCapabilities.xml?token=${TOKEN}`; +export var WMTS_URL = `${WMTS_BASE_URL}/${LAYER_NAME}/{TileMatrixSet}/{TileMatrix}/{TileCol}/{TileRow}.jpeg`; diff --git a/examples/config/vector-config.js b/examples/config/vector-config.js new file mode 100644 index 0000000..6ec23a7 --- /dev/null +++ b/examples/config/vector-config.js @@ -0,0 +1,4 @@ +import { MAPCOLONIES_QUERY_URL, MAPCOLONIES_GEOCODING_URL } from './config/common-config.js'; + +export var VECTOR_WFS_URL = `${MAPCOLONIES_QUERY_URL}/api/vector/v1/core/wfs`; +export var GEOCODING_URL = `${MAPCOLONIES_GEOCODING_URL}/api/osm/v1/search`; diff --git a/examples/index.json b/examples/index.json new file mode 100644 index 0000000..40b1734 --- /dev/null +++ b/examples/index.json @@ -0,0 +1,384 @@ +{ + "OpenLayers": { + "WMTS": { + "displayName": "WMTS", + "description": "Loads a MapColonies raster layer into OpenLayers via the WMTS capabilities document. Shows how to authenticate with a token, parse capabilities, and build a tile source.\n\nUse this as the starting point for any 2D map backed by MapColonies tiles — it covers the auth + capabilities handshake you would otherwise have to wire up yourself.", + "files": [ + "openlayers-wmts/wmts.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "openlayers-wmts/openlayers.css", + "openlayers-wmts/openlayers.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "OverviewMap": { + "displayName": "OverviewMap(MiniMap)", + "description": "Adds an OpenLayers OverviewMap control — a minimap in the corner that mirrors the main view.\n\nGives users spatial context when zoomed in deeply, without you having to build a synced second map by hand.", + "files": [ + "openlayers-overview-map/overview-map.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "openlayers-overview-map/overview-map.css", + "openlayers-overview-map/overview-map.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "Permalink": { + "displayName": "Permalink", + "description": "Keeps the map center, zoom and rotation in sync with the URL hash, and restores them on reload.\n\nLets users share a deep-link to any view and bookmark map state — no backend persistence needed.", + "files": [ + "openlayers-permalink/permalink.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "openlayers-permalink/permalink.css", + "openlayers-permalink/permalink.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "QueryService": { + "displayName": "QueryService", + "description": "Queries the MapColonies vector WFS endpoint and renders matching features on top of the raster basemap.\n\nShows the request shape and response handling so you can filter and display server-side data without writing the WFS plumbing yourself.", + "files": [ + "openlayers-query-service/query-service.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "config/vector-config.js", + "openlayers-query-service/openlayers.css", + "openlayers-query-service/openlayers.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "InteractiveFeatureSelect": { + "displayName": "Interactive Feature Select", + "description": "Drag-box selection over vector features, with hit testing and styled selection state.\n\nDrop-in pattern for bulk operations (export, edit, delete) on map features — saves you from reinventing the box-drag + selection bookkeeping.", + "files": [ + "openlayers-box-selection/box-selection.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "config/vector-config.js", + "openlayers-box-selection/openlayers.css", + "openlayers-box-selection/box-selection.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "InteractiveSensitiveBuildings": { + "displayName": "Interactive Sensitive Buldings Select", + "description": "Loads sensitive building features, highlights them on hover, and exposes attribute data on click.\n\nReference for domain-specific POI workflows where styling, hit detection and attribute panels need to work together.", + "files": [ + "openlayers-sensitive/interactive-sensitive.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "config/vector-config.js", + "openlayers-sensitive/openlayers.css", + "openlayers-sensitive/openlayers.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "Debug Layer": { + "displayName": "DebugLayer", + "description": "Overlays the WMTS tile grid with tile coordinates and matrix IDs.\n\nUse during integration to verify that your tile matrix set, origin and resolutions actually line up — turns 'why is my map blank' into a visible diagnosis.", + "files": [ + "openlayers-debug-layer/debug-layer.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "openlayers-debug-layer/openlayers.css", + "openlayers-debug-layer/openlayers.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "DrawAndModify": { + "displayName": "Draw and Modify Geodesic", + "description": "Draw and edit points, lines and polygons with geodesic geometry — distances and areas are computed on the ellipsoid, not the projected plane.\n\nIf you need measurement-correct sketching (AOIs, range rings, routes), this saves you from the classic 'planar distance looks wrong at high latitudes' bug.", + "files": [ + "openlayers-draw-and-modify/draw-and-modify.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "config/vector-config.js", + "openlayers-draw-and-modify/draw-and-modify.css", + "openlayers-draw-and-modify/draw-and-modify.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "PreloadTiles": { + "displayName": "Preload Tiles", + "description": "Configures the tile layer to preload neighbouring zoom levels in the background.\n\nResult: pan and zoom feel snappier because the next tiles are already in cache. Useful when you need a smooth UX on slow networks without writing a custom tile cache.", + "files": [ + "openlayers-preload/preload.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "openlayers-preload/preload.css", + "openlayers-preload/preload.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "DrawStreetLabels": { + "displayName": "Draw Street Labels", + "description": "Renders street-name labels from a vector source with collision avoidance and rotation-aware placement.\n\nShortcut to readable labelling on top of raster basemaps — the placement/rotation logic is where naive implementations usually fall over.", + "files": [ + "openlayers-street-labels/street-labels.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "config/vector-config.js", + "openlayers-street-labels/street-labels.css", + "openlayers-street-labels/street-labels.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + }, + "ol-ext": { + "displayName": "ol-ext", + "description": "Integrates the ol-ext extension library on top of the MapColonies WMTS basemap, showcasing ready-made controls and interactions.\n\nUse it as a survey of off-the-shelf widgets (legends, layer switchers, popups) so you don't end up reimplementing common UI from scratch.", + "files": [ + "ol-ext-example/ol-ext.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/xml-utils.js", + "ol-ext-example/openlayers.css", + "ol-ext-example/openlayers.html" + ], + "links": [ + { "name": "ol.css", "url": "/libs/ol/v7.3.0/ol.css", "type": "css" }, + { "name": "ol.js", "url": "/libs/ol/v7.3.0/ol.js", "type": "js" } + ] + } + }, + "Cesium": { + "WMTS": { + "displayName": "WMTS Imagery Provider", + "description": "Wires a MapColonies WMTS raster layer into Cesium as a WebMapTileServiceImageryProvider, using the capabilities document to pick the right tile matrix set.\n\nStart here for any 3D globe view backed by MapColonies imagery — the capabilities + tilingScheme matching is the part most integrators get wrong.", + "files": [ + "cesium-wmts/cesium.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "cesium-wmts/cesium.html" + ], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + }, + "GeoCoding": { + "displayName": "GeoCoding", + "description": "Sends user-typed queries to the MapColonies geocoding service and flies the Cesium camera to matching results.\n\nDrop-in 'search this place' UX for 3D viewers — covers request shape, result ranking and camera animation in one example.", + "files": [ + "cesium-geocoding/cesium.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "config/vector-config.js", + "cesium-geocoding/cesium.html" + ], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + }, + "Terrain": { + "displayName": "Terrain Provider", + "description": "Attaches a MapColonies DEM as a Cesium terrain provider so the globe has real elevation underneath the imagery.\n\nGives you photorealistic relief and correct picking heights without having to host or tile your own DEM.", + "files": [ + "cesium-terrain/cesium.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "config/dem-config.js", + "cesium-terrain/cesium.html" + ], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + }, + "3D Model": { + "displayName": "3D Model (South Lebanon)", + "description": "Composes the full MapColonies 3D stack: WMTS imagery, DEM terrain and a photo-realistic 3D Tiles mesh, all aligned in one scene.\n\nReference for end-to-end 3D scene assembly — shows the loading order and provider config that makes imagery, terrain and meshes line up correctly.", + "files": [ + "cesium-3d/3d-model.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "config/3d-config.js", + "config/dem-config.js", + "cesium-3d/cesium.html" + ], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + }, + "LayersSplit": { + "displayName": "Layers Split", + "description": "Splits the Cesium view between two imagery layers with a draggable divider — left side shows layer A, right side shows layer B.\n\nReady-made before/after compare UX for change detection, version review or A/B imagery without writing the splitter math yourself.", + "files": [ + "cesium-layers-split/split-layers.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "cesium-layers-split/split-layers.html", + "cesium-layers-split/split-layers.css" + ], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + }, + "ViewShed": { + "displayName": "ViewShed", + "description": "Computes a viewshed (visible / not visible) from an observer point over the 3D Tiles mesh and DEM, using a custom shader.\n\nLine-of-sight analysis for planning, security and coverage use cases — the shader plus depth-buffer trick is the hard part this example does for you.", + "files": [ + "cesium-viewshed/viewshed.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "config/3d-config.js", + "config/dem-config.js", + "cesium-viewshed/viewshed.html", + "cesium-viewshed/viewshed.css" + ], + "links": [ + { + "name": "cesium.js", + "url": "/libs/Cesium/Cesium.js", + "type": "js" + }, + { + "name": "widgets.css", + "url": "/libs/Cesium/Widgets/widgets.css", + "type": "css" + } + ] + } + }, + "Leaflet": { + "WMTS": { + "displayName": "Basic WMTS Example", + "description": "Minimal Leaflet map consuming the MapColonies WMTS service via a plain L.tileLayer.\n\nGood entry point when you want a lightweight 2D map without OpenLayers — shows the URL template and token handling needed to hit the same backend.", + "files": [ + "leaflet-wmts/wmts.js", + "config/common-config.js", + "config/raster-config.js", + "utils/catalog-client.js", + "utils/wmts-utils.js", + "utils/xml-utils.js", + "leaflet-wmts/leaflet.css", + "leaflet-wmts/leaflet.html" + ], + "links": [ + { + "name": "leaflet.js", + "url": "/libs/leaflet/1.9.4/leaflet.js", + "type": "js" + }, + { + "name": "leaflet.css", + "url": "/libs/leaflet/1.9.4/leaflet.css", + "type": "css" + } + ] + } + } +} diff --git a/examples/leaflet-wmts/leaflet.css b/examples/leaflet-wmts/leaflet.css new file mode 100644 index 0000000..63c0413 --- /dev/null +++ b/examples/leaflet-wmts/leaflet.css @@ -0,0 +1,3 @@ +#map { + height: 100%; +} diff --git a/examples/leaflet-wmts/leaflet.html b/examples/leaflet-wmts/leaflet.html new file mode 100644 index 0000000..ad19e7d --- /dev/null +++ b/examples/leaflet-wmts/leaflet.html @@ -0,0 +1 @@ +
diff --git a/examples/leaflet-wmts/wmts.js b/examples/leaflet-wmts/wmts.js new file mode 100644 index 0000000..180cbe3 --- /dev/null +++ b/examples/leaflet-wmts/wmts.js @@ -0,0 +1,19 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE } from './config/raster-config.js'; +import { fetchWmtsTileTemplate } from './utils/wmts-utils.js'; + +const parser = (urlTemplate) => { + return urlTemplate + .replace('{TileMatrixSet}', 'WorldCRS84') + .replace('{TileMatrix}', '{z}') + .replace('{TileRow}', '{y}') + .replace('{TileCol}', '{x}'); +}; + +const map = L.map('map', { crs: L.CRS.EPSG4326 }).setView([0.0, 0.0], 1); + +fetchWmtsTileTemplate(PRODUCT_ID, PRODUCT_TYPE).then(({ template, name }) => { + const parsedUrl = parser(template); + const layer = L.tileLayer(parsedUrl + `?token=${TOKEN}`, { id: name }); + map.addLayer(layer); +}); diff --git a/examples/ol-ext-example/ol-ext.js b/examples/ol-ext-example/ol-ext.js new file mode 100644 index 0000000..153ddbb --- /dev/null +++ b/examples/ol-ext-example/ol-ext.js @@ -0,0 +1,32 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 0, + projection: 'EPSG:4326' + }) +}); + +const WMTSParser = new ol.format.WMTSCapabilities(); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const layer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(layer); + }); diff --git a/examples/ol-ext-example/openlayers.css b/examples/ol-ext-example/openlayers.css new file mode 100644 index 0000000..3e6b562 --- /dev/null +++ b/examples/ol-ext-example/openlayers.css @@ -0,0 +1,6 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/ol-ext-example/openlayers.html b/examples/ol-ext-example/openlayers.html new file mode 100644 index 0000000..ad19e7d --- /dev/null +++ b/examples/ol-ext-example/openlayers.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-box-selection/box-selection.html b/examples/openlayers-box-selection/box-selection.html new file mode 100644 index 0000000..e5bbe55 --- /dev/null +++ b/examples/openlayers-box-selection/box-selection.html @@ -0,0 +1,21 @@ +
+
+
+

Using a DragBox interaction to select features.

+
+

+ This example shows how to use a DragBox interaction to select features. + Selected features are added to the feature overlay of a select interaction + (ol/interaction/Select) for highlighting. +

+

Use Ctrl+Drag (Command+Drag on Mac) to draw boxes.

+
+
+ +
+

+ The selected Features are + +

+
+
diff --git a/examples/openlayers-box-selection/box-selection.js b/examples/openlayers-box-selection/box-selection.js new file mode 100644 index 0000000..10f3896 --- /dev/null +++ b/examples/openlayers-box-selection/box-selection.js @@ -0,0 +1,169 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const WMTSParser = new ol.format.WMTSCapabilities(); + +const vectorSource = new ol.source.Vector({ + format: new ol.format.GeoJSON(), + strategy: ol.loadingstrategy.tile(ol.tilegrid.createXYZ({ tileSize: 256 })), + url: function (extent) { + return ( + VECTOR_WFS_URL + + `?service=WFS&version=2.0.0&request=GetFeature&typeName=core:buildings_polygon&srsname=EPSG:4326&bbox=${extent.join( + ',' + )},EPSG:4326&token=${TOKEN}&outputFormat=application/json` + ); + } +}); + +const style = new ol.style.Style({ + fill: new ol.style.Fill({ + color: '#eeeeee' + }) +}); + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [34.465798, 31.513991], + zoom: 18, + projection: 'EPSG:4326', + constrainRotation: 16 + }) +}); + +const vectorLayer = new ol.layer.Vector({ + source: vectorSource, + style: function (feature) { + const color = feature.get('is_sensitive') === true ? 'red' : '#eeeeee'; + style.getFill().setColor(color); + return style; + } +}); + +const selectedStyle = new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(180, 2, 180, 0.3)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(180, 2, 180, 0.4)', + width: 5 + }) +}); + +// a normal select interaction to handle click +const select = new ol.interaction.Select({ + style: function (feature) { + const color = feature.get('is_sensitive') === true ? 'red' : '#eeeeee'; + selectedStyle.getFill().setColor(color); + return selectedStyle; + } +}); +map.addInteraction(select); + +const selectedFeatures = select.getFeatures(); + +// a DragBox interaction used to select features by drawing boxes +const dragBox = new ol.interaction.DragBox({ + condition: ol.events.condition.platformModifierKeyOnly +}); + +map.addInteraction(dragBox); + +dragBox.on('boxend', function () { + const boxExtent = dragBox.getGeometry().getExtent(); + + // if the extent crosses the antimeridian process each world separately + const worldExtent = map.getView().getProjection().getExtent(); + const worldWidth = ol.extent.getWidth(worldExtent); + const startWorld = Math.floor((boxExtent[0] - worldExtent[0]) / worldWidth); + const endWorld = Math.floor((boxExtent[2] - worldExtent[0]) / worldWidth); + + for (let world = startWorld; world <= endWorld; ++world) { + const left = Math.max(boxExtent[0] - world * worldWidth, worldExtent[0]); + const right = Math.min(boxExtent[2] - world * worldWidth, worldExtent[2]); + const extent = [left, boxExtent[1], right, boxExtent[3]]; + + const boxFeatures = vectorSource + .getFeaturesInExtent(extent) + .filter( + (feature) => + !selectedFeatures.getArray().includes(feature) && + feature.getGeometry().intersectsExtent(extent) + ); + + // features that intersect the box geometry are added to the + // collection of selected features + + // if the view is not obliquely rotated the box geometry and + // its extent are equalivalent so intersecting features can + // be added directly to the collection + const rotation = map.getView().getRotation(); + const oblique = rotation % (Math.PI / 2) !== 0; + + // when the view is obliquely rotated the box extent will + // exceed its geometry so both the box and the candidate + // feature geometries are rotated around a common anchor + // to confirm that, with the box geometry aligned with its + // extent, the geometries intersect + if (oblique) { + const anchor = [0, 0]; + const geometry = dragBox.getGeometry().clone(); + geometry.translate(-world * worldWidth, 0); + geometry.rotate(-rotation, anchor); + const extent = geometry.getExtent(); + boxFeatures.forEach(function (feature) { + const geometry = feature.getGeometry().clone(); + geometry.rotate(-rotation, anchor); + if (geometry.intersectsExtent(extent)) { + selectedFeatures.push(feature); + } + }); + } else { + selectedFeatures.extend(boxFeatures); + } + } +}); + +// clear selection when drawing a new box and when clicking on the map +dragBox.on('boxstart', function () { + selectedFeatures.clear(); +}); + +const infoBox = document.getElementById('info'); + +selectedFeatures.on(['add', 'remove'], function () { + const names = selectedFeatures.getArray().map((feature) => { + return { + 'סוג מבנה': feature.get('building_type'), + GFID: feature.get('entity_id'), + רגיש: feature.get('is_sensitive') + }; + }); + console.clear(); + if (names.length > 0) { + infoBox.innerHTML = JSON.stringify(names, 2, 4); + } else { + infoBox.innerHTML = 'None'; + } +}); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const rasterLayer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(rasterLayer); + map.addLayer(vectorLayer); + }); diff --git a/examples/openlayers-box-selection/openlayers.css b/examples/openlayers-box-selection/openlayers.css new file mode 100644 index 0000000..72b367f --- /dev/null +++ b/examples/openlayers-box-selection/openlayers.css @@ -0,0 +1,6 @@ +#map { + top: 0; + bottom: 0; + height: 50%; + width: 100%; +} diff --git a/examples/openlayers-debug-layer/debug-layer.js b/examples/openlayers-debug-layer/debug-layer.js new file mode 100644 index 0000000..b55af11 --- /dev/null +++ b/examples/openlayers-debug-layer/debug-layer.js @@ -0,0 +1,42 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 0, + projection: 'EPSG:4326' + }) +}); + +const WMTSParser = new ol.format.WMTSCapabilities(); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const layer = new ol.layer.WebGLTile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(layer); + + const debugLayer = new ol.layer.WebGLTile({ + source: new ol.source.TileDebug({ + template: 'z:{z} x:{x}, y:{y}', + projection: layer.getSource().getProjection(), + tileGrid: layer.getSource().getTileGrid(), + zDirection: 1 + }) + }); + map.addLayer(debugLayer); + }); diff --git a/examples/openlayers-debug-layer/openlayers.css b/examples/openlayers-debug-layer/openlayers.css new file mode 100644 index 0000000..3e6b562 --- /dev/null +++ b/examples/openlayers-debug-layer/openlayers.css @@ -0,0 +1,6 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-debug-layer/openlayers.html b/examples/openlayers-debug-layer/openlayers.html new file mode 100644 index 0000000..ad19e7d --- /dev/null +++ b/examples/openlayers-debug-layer/openlayers.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-draw-and-modify/draw-and-modify.css b/examples/openlayers-draw-and-modify/draw-and-modify.css new file mode 100644 index 0000000..110b53a --- /dev/null +++ b/examples/openlayers-draw-and-modify/draw-and-modify.css @@ -0,0 +1,6 @@ +#map { + height: 70%; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-draw-and-modify/draw-and-modify.html b/examples/openlayers-draw-and-modify/draw-and-modify.html new file mode 100644 index 0000000..cae5273 --- /dev/null +++ b/examples/openlayers-draw-and-modify/draw-and-modify.html @@ -0,0 +1,30 @@ +
+
+ + +
+ +
+

Example of using Draw and Modify interactions for geodesic circles.

+
+

+ Example of using the ol/interaction/Draw interaction with a custom geometry + function together with the ol/interaction/Modify interaction to draw and modify + geodesic circles (a ol/geom/Polygon#circular polygon representing a circle on the + surface of the Earth's sphere). The polygon is placed in a + ol/geom/GeometryCollection together with a ol/geom/Point which + allows the Modify interaction to adjust the circle center as well as the radius. Custom style + functions ensure the correct final geometry is displayed throughout. + ol/geom/Circle projected (planar) geometries can also be drawn and modified. The + difference between geodesic and projected circles can be seen when their centers are moved + between northern and southern latitudes in the Web Mercator projection. The + ol/interaction/Snap interaction can be used to create concentric circles. +

+
+
diff --git a/examples/openlayers-draw-and-modify/draw-and-modify.js b/examples/openlayers-draw-and-modify/draw-and-modify.js new file mode 100644 index 0000000..512d8cc --- /dev/null +++ b/examples/openlayers-draw-and-modify/draw-and-modify.js @@ -0,0 +1,189 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const map = new ol.Map({ + layers: [], + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 0, + projection: 'EPSG:4326' + }) +}); + +const WMTSParser = new ol.format.WMTSCapabilities(); + +const source = new ol.source.Vector(); + +const style = new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: '#33cc33', + width: 2 + }), + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: '#ffcc33' + }) + }) +}); + +const geodesicStyle = new ol.style.Style({ + geometry: function (feature) { + return feature.get('modifyGeometry') || feature.getGeometry(); + }, + fill: new ol.style.Fill({ + color: 'rgba(255, 255, 255, 0.2)' + }), + stroke: new ol.style.Stroke({ + color: '#ff3333', + width: 2 + }), + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: 'rgba(0, 0, 0, 0)' + }) + }) +}); + +const vector = new ol.layer.Vector({ + source: source, + style: function (feature) { + const geometry = feature.getGeometry(); + return geometry.getType() === 'GeometryCollection' ? geodesicStyle : style; + } +}); + +const defaultStyle = new ol.interaction.Modify({ source: source }).getOverlay().getStyleFunction(); + +const modify = new ol.interaction.Modify({ + source: source, + style: function (feature) { + feature.get('features').forEach(function (modifyFeature) { + const modifyGeometry = modifyFeature.get('modifyGeometry'); + if (modifyGeometry) { + const modifyPoint = feature.getGeometry().getCoordinates(); + const geometries = modifyFeature.getGeometry().getGeometries(); + const polygon = geometries[0].getCoordinates()[0]; + const center = geometries[1].getCoordinates(); + const projection = map.getView().getProjection(); + let first, last, radius; + if (modifyPoint[0] === center[0] && modifyPoint[1] === center[1]) { + // center is being modified + // get unchanged radius from diameter between polygon vertices + first = ol.proj.transform(polygon[0], projection, 'EPSG:4326'); + last = ol.proj.transform(polygon[(polygon.length - 1) / 2], projection, 'EPSG:4326'); + radius = ol.sphere.getDistance(first, last) / 2; + } else { + // radius is being modified + first = ol.proj.transform(center, projection, 'EPSG:4326'); + last = ol.proj.transform(modifyPoint, projection, 'EPSG:4326'); + radius = ol.sphere.getDistance(first, last); + } + // update the polygon using new center or radius + const circle = ol.geom.Circle( + ol.proj.transform(center, projection, 'EPSG:4326'), + radius, + 128 + ); + circle.transform('EPSG:4326', projection); + geometries[0].setCoordinates(circle.getCoordinates()); + // save changes to be applied at the end of the interaction + modifyGeometry.setGeometries(geometries); + } + }); + return defaultStyle(feature); + } +}); + +modify.on('modifystart', function (event) { + event.features.forEach(function (feature) { + const geometry = feature.getGeometry(); + if (geometry.getType() === 'GeometryCollection') { + feature.set('modifyGeometry', geometry.clone(), true); + } + }); +}); + +modify.on('modifyend', function (event) { + event.features.forEach(function (feature) { + const modifyGeometry = feature.get('modifyGeometry'); + if (modifyGeometry) { + feature.setGeometry(modifyGeometry); + feature.unset('modifyGeometry', true); + } + }); +}); + +map.addInteraction(modify); + +let draw, snap; // global so we can remove them later +const typeSelect = document.getElementById('type'); + +function addInteractions() { + let value = typeSelect.value; + let geometryFunction; + if (value === 'Geodesic') { + value = 'Circle'; + geometryFunction = function (coordinates, geometry, projection) { + if (!geometry) { + geometry = new ol.geom.GeometryCollection([ + new ol.geom.Polygon([]), + new ol.geom.Point(coordinates[0]) + ]); + } + const geometries = geometry.getGeometries(); + const center = ol.proj.transform(coordinates[0], projection, 'EPSG:4326'); + const last = ol.proj.transform(coordinates[1], projection, 'EPSG:4326'); + const radius = ol.sphere.getDistance(center, last); + const circle = ol.geom.Polygon.circular(center, radius, 128); + circle.transform('EPSG:4326', projection); + geometries[0].setCoordinates(circle.getCoordinates()); + geometry.setGeometries(geometries); + return geometry; + }; + } + draw = new ol.interaction.Draw({ + source: source, + type: value, + geometryFunction: geometryFunction + }); + map.addInteraction(draw); + snap = new ol.interaction.Snap({ source: source }); + map.addInteraction(snap); +} + +/** + * Handle change event. + */ +typeSelect.onchange = function () { + map.removeInteraction(draw); + map.removeInteraction(snap); + addInteractions(); +}; + +addInteractions(); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const layer = new ol.layer.WebGLTile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(layer); + map.addLayer(vector); + }); diff --git a/examples/openlayers-overview-map/overview-map.css b/examples/openlayers-overview-map/overview-map.css new file mode 100644 index 0000000..37d4727 --- /dev/null +++ b/examples/openlayers-overview-map/overview-map.css @@ -0,0 +1,39 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} + +.map .ol-custom-overviewmap, +.map .ol-custom-overviewmap.ol-uncollapsible { + bottom: auto; + left: auto; + right: 0; + top: 0; +} + +.map .ol-custom-overviewmap:not(.ol-collapsed) { + border: 1px solid black; +} + +.map .ol-custom-overviewmap .ol-overviewmap-map { + border: none; + width: 300px; +} + +.map .ol-custom-overviewmap .ol-overviewmap-box { + border: 2px solid red; +} + +.map .ol-custom-overviewmap:not(.ol-collapsed) button { + bottom: auto; + left: auto; + right: 1px; + top: 1px; +} + +.map .ol-rotate { + top: 170px; + right: 0; +} diff --git a/examples/openlayers-overview-map/overview-map.html b/examples/openlayers-overview-map/overview-map.html new file mode 100644 index 0000000..4fa1fbc --- /dev/null +++ b/examples/openlayers-overview-map/overview-map.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-overview-map/overview-map.js b/examples/openlayers-overview-map/overview-map.js new file mode 100644 index 0000000..d6a579c --- /dev/null +++ b/examples/openlayers-overview-map/overview-map.js @@ -0,0 +1,52 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const WMTSParser = new ol.format.WMTSCapabilities(); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + const optionsMiniMap = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + optionsMiniMap.urls = optionsMiniMap.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + let rasterLayer = new ol.layer.Tile({ + opacity: 1, + source: new ol.source.WMTS(options), + preload: 10 + }); + let rasterLayer2 = new ol.layer.Tile({ + opacity: 1, + source: new ol.source.WMTS(optionsMiniMap) + }); + const overviewMapControl = new ol.control.OverviewMap({ + layers: [rasterLayer2], + collapsed: false + }); + + const map = new ol.Map({ + controls: ol.control.defaults.defaults().extend([overviewMapControl]), + target: 'map', + layers: [rasterLayer], + view: new ol.View({ + center: [34.465798, 31.513991], + zoom: 18, + projection: 'EPSG:4326', + minZoom: 1 + }) + }); + }); diff --git a/examples/openlayers-permalink/permalink.css b/examples/openlayers-permalink/permalink.css new file mode 100644 index 0000000..3e6b562 --- /dev/null +++ b/examples/openlayers-permalink/permalink.css @@ -0,0 +1,6 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-permalink/permalink.html b/examples/openlayers-permalink/permalink.html new file mode 100644 index 0000000..4fa1fbc --- /dev/null +++ b/examples/openlayers-permalink/permalink.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-permalink/permalink.js b/examples/openlayers-permalink/permalink.js new file mode 100644 index 0000000..4467d41 --- /dev/null +++ b/examples/openlayers-permalink/permalink.js @@ -0,0 +1,89 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +// default zoom, center and rotation +let zoom = 2; +let center = [0, 0]; +let rotation = 0; + +if (window.location.hash !== '') { + // try to restore center, zoom-level and rotation from the URL + const hash = window.location.hash.replace('#map=', ''); + const parts = hash.split('/'); + if (parts.length === 4) { + zoom = parseFloat(parts[0]); + center = [parseFloat(parts[1]), parseFloat(parts[2])]; + rotation = parseFloat(parts[3]); + } +} + +const WMTSParser = new ol.format.WMTSCapabilities(); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const layer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: center, + zoom: zoom, + rotation: rotation, + projection: 'EPSG:4326' + }) + }); + map.addLayer(layer); + + let shouldUpdate = true; + const view = map.getView(); + const updatePermalink = function () { + if (!shouldUpdate) { + // do not update the URL when the view was changed in the 'popstate' handler + shouldUpdate = true; + return; + } + + const center = view.getCenter(); + const hash = + '#map=' + + view.getZoom().toFixed(2) + + '/' + + center[0].toFixed(2) + + '/' + + center[1].toFixed(2) + + '/' + + view.getRotation(); + const state = { + zoom: view.getZoom(), + center: view.getCenter(), + rotation: view.getRotation() + }; + window.history.pushState(state, 'map', hash); + }; + + map.on('moveend', updatePermalink); + + // restore the view state when navigating through the history, see + // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate + window.addEventListener('popstate', function (event) { + if (event.state === null) { + return; + } + map.getView().setCenter(event.state.center); + map.getView().setZoom(event.state.zoom); + map.getView().setRotation(event.state.rotation); + shouldUpdate = false; + }); + }); diff --git a/examples/openlayers-preload/preload.css b/examples/openlayers-preload/preload.css new file mode 100644 index 0000000..acb63b6 --- /dev/null +++ b/examples/openlayers-preload/preload.css @@ -0,0 +1,6 @@ +.map { + height: 50%; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-preload/preload.html b/examples/openlayers-preload/preload.html new file mode 100644 index 0000000..4aa8e68 --- /dev/null +++ b/examples/openlayers-preload/preload.html @@ -0,0 +1,2 @@ +
+
diff --git a/examples/openlayers-preload/preload.js b/examples/openlayers-preload/preload.js new file mode 100644 index 0000000..2aec33a --- /dev/null +++ b/examples/openlayers-preload/preload.js @@ -0,0 +1,44 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const WMTSParser = new ol.format.WMTSCapabilities(); + +const sharedView = new ol.View({ + center: [0, 0], + zoom: 0, + projection: 'EPSG:4326' +}); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const layerNoPreload = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + const layerPreload = new ol.layer.Tile({ + opacity: 1, + source: new ol.source.WMTS(options), + preload: 10 + }); + + const mapPreload = new ol.Map({ + target: 'map-preload', + layers: [layerPreload], + view: sharedView + }); + const mapNoPreload = new ol.Map({ + target: 'map-no-preload', + layers: [layerNoPreload], + view: sharedView + }); + }); diff --git a/examples/openlayers-query-service/openlayers.css b/examples/openlayers-query-service/openlayers.css new file mode 100644 index 0000000..3e6b562 --- /dev/null +++ b/examples/openlayers-query-service/openlayers.css @@ -0,0 +1,6 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-query-service/openlayers.html b/examples/openlayers-query-service/openlayers.html new file mode 100644 index 0000000..ad19e7d --- /dev/null +++ b/examples/openlayers-query-service/openlayers.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-query-service/query-service.js b/examples/openlayers-query-service/query-service.js new file mode 100644 index 0000000..68d603a --- /dev/null +++ b/examples/openlayers-query-service/query-service.js @@ -0,0 +1,61 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { VECTOR_WFS_URL } from './config/vector-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [34.465798, 31.513991], + zoom: 18, + projection: 'EPSG:4326' + }) +}); + +const WMTSParser = new ol.format.WMTSCapabilities(); +let rasterLayer; + +const vectorSource = new ol.source.Vector({ + format: new ol.format.GeoJSON(), + strategy: ol.loadingstrategy.tile(ol.tilegrid.createXYZ({ tileSize: 512 })), + url: function (extent) { + return ( + VECTOR_WFS_URL + + `?service=WFS&version=2.0.0&request=GetFeature&typeName=core:buildings_polygon&srsname=EPSG:4326&bbox=${extent.join( + ',' + )},EPSG:4326&token=${TOKEN}&outputFormat=application/json&maxFeatures=10000` + ); + } +}); + +const vector = new ol.layer.Vector({ + source: vectorSource, + style: new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: 'rgba(255,0,0,0.5)', + width: 3 + }), + fill: new ol.style.Fill({ color: 'rgba(255,50,0,0.5)' }) + }), + minZoom: 15, + maxZoom: 20 +}); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + rasterLayer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(rasterLayer); + map.addLayer(vector); + }); diff --git a/examples/openlayers-sensitive/interactive-sensitive.js b/examples/openlayers-sensitive/interactive-sensitive.js new file mode 100644 index 0000000..8e7a86e --- /dev/null +++ b/examples/openlayers-sensitive/interactive-sensitive.js @@ -0,0 +1,106 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { VECTOR_WFS_URL } from './config/vector-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const WMTSParser = new ol.format.WMTSCapabilities(); +let rasterLayer; + +const vectorSource = new ol.source.Vector({ + format: new ol.format.GeoJSON(), + strategy: ol.loadingstrategy.tile(ol.tilegrid.createXYZ({ tileSize: 256 })), + url: function (extent) { + return ( + VECTOR_WFS_URL + + `?service=WFS&version=2.0.0&request=GetFeature&typeName=core:buildings_polygon&srsname=EPSG:4326&bbox=${extent.join( + ',' + )},EPSG:4326&token=${TOKEN}&outputFormat=application/json` + ); + } +}); + +const style = new ol.style.Style({ + fill: new ol.style.Fill({ + color: '#eeeeee' + }) +}); + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [34.465798, 31.513991], + zoom: 18, + projection: 'EPSG:4326', + constrainRotation: 16 + }) +}); + +const vectorLayer = new ol.layer.Vector({ + source: vectorSource, + style: function (feature) { + const color = + feature.get('is_sensitive') === true ? 'rgba(255, 0, 0, 0.3)' : 'rgba(255, 255, 255, 0.7)'; + style.getFill().setColor(color); + return style; + } +}); + +const selectedStyle = new ol.style.Style({ + fill: new ol.style.Fill({ + color: 'rgba(180, 2, 180, 0.3)' + }), + stroke: new ol.style.Stroke({ + color: 'rgba(180, 2, 180, 0.4)', + width: 10 + }) +}); + +// a normal select interaction to handle click +const select = new ol.interaction.Select({ + style: function (feature) { + const color = + feature.get('is_sensitive') === true ? 'rgba(255, 0, 0, 1)' : 'rgba(255, 255, 255, 1)'; + selectedStyle.getFill().setColor(color); + return selectedStyle; + } +}); +map.addInteraction(select); + +const selectedFeatures = select.getFeatures(); + +const infoBox = document.getElementById('info'); + +selectedFeatures.on(['add', 'remove'], function () { + const names = selectedFeatures.getArray().map((feature) => { + return { + 'סוג מבנה': feature.get('building_type'), + GFID: feature.get('entity_id'), + רגיש: feature.get('is_sensitive') + }; + }); + console.clear(); + if (names.length > 0) { + infoBox.innerHTML = JSON.stringify(names, 2, 4); + } else { + infoBox.innerHTML = 'None'; + } +}); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + rasterLayer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(rasterLayer); + map.addLayer(vectorLayer); + }); diff --git a/examples/openlayers-sensitive/openlayers.css b/examples/openlayers-sensitive/openlayers.css new file mode 100644 index 0000000..72b367f --- /dev/null +++ b/examples/openlayers-sensitive/openlayers.css @@ -0,0 +1,6 @@ +#map { + top: 0; + bottom: 0; + height: 50%; + width: 100%; +} diff --git a/examples/openlayers-sensitive/openlayers.html b/examples/openlayers-sensitive/openlayers.html new file mode 100644 index 0000000..e55b80c --- /dev/null +++ b/examples/openlayers-sensitive/openlayers.html @@ -0,0 +1,7 @@ +
+
+

+ Click on a sensitive feature (colored RED) to see details: + +

+
diff --git a/examples/openlayers-street-labels/street-labels.css b/examples/openlayers-street-labels/street-labels.css new file mode 100644 index 0000000..3e6b562 --- /dev/null +++ b/examples/openlayers-street-labels/street-labels.css @@ -0,0 +1,6 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-street-labels/street-labels.html b/examples/openlayers-street-labels/street-labels.html new file mode 100644 index 0000000..ad19e7d --- /dev/null +++ b/examples/openlayers-street-labels/street-labels.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-street-labels/street-labels.js b/examples/openlayers-street-labels/street-labels.js new file mode 100644 index 0000000..f408044 --- /dev/null +++ b/examples/openlayers-street-labels/street-labels.js @@ -0,0 +1,76 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const WMTSParser = new ol.format.WMTSCapabilities(); +let rasterLayer; + +const style = new ol.style.Style({ + text: new ol.style.Text({ + font: '20px "sans-serif"', + placement: 'line', + rotationWithView: true, + stroke: new ol.style.Stroke({ + color: 'white' + }), + fill: new ol.style.Fill({ + color: 'red' + }) + }), + stroke: new ol.style.Stroke({ + color: 'red' + }) +}); + +const vectorSource = new ol.source.Vector({ + format: new ol.format.GeoJSON(), + strategy: ol.loadingstrategy.tile(ol.tilegrid.createXYZ({ tileSize: 256 })), + url: function (extent) { + return ( + VECTOR_WFS_URL + + `?service=WFS&version=2.0.0&request=GetFeature&typeName=core:roads_line&srsname=EPSG:4326&bbox=${extent.join( + ',' + )},EPSG:4326&token=${TOKEN}&outputFormat=application/json&count=10000` + ); + } +}); + +const vectorLayer = new ol.layer.Vector({ + declutter: true, + source: vectorSource, + style: function (feature) { + style.getText().setText(feature.get('name') === null ? 'לא ידוע' : feature.get('name')); + return style; + }, + minZoom: 15, + maxZoom: 20 +}); + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [34.465798, 31.513991], + zoom: 18, + projection: 'EPSG:4326', + minZoom: 12 + }) +}); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + rasterLayer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(rasterLayer); + map.addLayer(vectorLayer); + }); diff --git a/examples/openlayers-wmts/openlayers.css b/examples/openlayers-wmts/openlayers.css new file mode 100644 index 0000000..3e6b562 --- /dev/null +++ b/examples/openlayers-wmts/openlayers.css @@ -0,0 +1,6 @@ +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} diff --git a/examples/openlayers-wmts/openlayers.html b/examples/openlayers-wmts/openlayers.html new file mode 100644 index 0000000..ad19e7d --- /dev/null +++ b/examples/openlayers-wmts/openlayers.html @@ -0,0 +1 @@ +
diff --git a/examples/openlayers-wmts/wmts.js b/examples/openlayers-wmts/wmts.js new file mode 100644 index 0000000..7214905 --- /dev/null +++ b/examples/openlayers-wmts/wmts.js @@ -0,0 +1,32 @@ +import { TOKEN } from './config/common-config.js'; +import { PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; + +const WMTSParser = new ol.format.WMTSCapabilities(); + +const map = new ol.Map({ + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 0, + projection: 'EPSG:4326' + }) +}); + +fetchServiceLink('raster', PRODUCT_ID, PRODUCT_TYPE, RASTER_SCHEME) + .then(({ url, name }) => + fetch(`${url}?token=${TOKEN}`) + .then((response) => response.text()) + .then((text) => ({ text, name })) + ) + .then(({ text, name }) => { + const results = WMTSParser.read(text); + const options = ol.source.WMTS.optionsFromCapabilities(results, { + layer: name + }); + options.urls = options.urls.map((url) => { + return url.concat(`?token=${TOKEN}`); + }); + const layer = new ol.layer.Tile({ opacity: 1, source: new ol.source.WMTS(options) }); + map.addLayer(layer); + }); diff --git a/examples/utils/catalog-client.js b/examples/utils/catalog-client.js new file mode 100644 index 0000000..34c3c8e --- /dev/null +++ b/examples/utils/catalog-client.js @@ -0,0 +1,91 @@ +import { MAPCOLONIES_CATALOG_URL, TOKEN } from './config/common-config.js'; +import { parseXml } from './utils/xml-utils.js'; + +const TYPENAMES = { + raster: 'mc:MCRasterRecord', + '3d': 'mc:MC3DRecord', + dem: 'mc:MCDEMRecord' +}; + +const namespaceFor = (catalogKey) => `http://schema.mapcolonies.com/${catalogKey}`; + +/** + * Builds a CSW 2.0.2 GetRecords XML body that filters by productId and productType. + * + * @param {string} typename - CSW typeName for the catalog (e.g. 'mc:MCRasterRecord'). + * @param {string} namespace - XML namespace URI for the catalog schema. + * @param {string} productId - Product identifier to match (e.g. 'blueMarble'). + * @param {string} productType - Product type to match (e.g. 'Orthophoto'). + * @returns {string} CSW GetRecords request XML. + */ +function buildGetRecordsBody(typename, namespace, productId, productType) { + return ` + + + full + + + + + mc:productId + ${productId} + + + mc:productType + ${productType} + + + + + +`; +} + +/** + * Parses a CSW response and returns the service URL and layer name for the requested scheme. + * + * @param {string} xmlText - Raw CSW response XML. + * @param {string} namespace - Namespace URI used for `` elements in the response. + * @param {string} scheme - Scheme name to pick (e.g. 'WMTS', 'WCS', '3DTiles'). + * @returns {{ url: string, name: string } | null} Service URL and layer name from the `name` attribute, or null if no matching `` element is present. + */ +function parseLink(xmlText, namespace, scheme) { + const doc = parseXml(xmlText, 'CSW response'); + const node = Array.from(doc.getElementsByTagNameNS(namespace, 'links')).find( + (n) => n.getAttribute('scheme') === scheme + ); + if (!node) return null; + return { + url: (node.textContent || '').trim(), + name: node.getAttribute('name') || '' + }; +} + +/** + * Resolves the service URL and layer name for a specific scheme on a catalog record. + * + * @param {'raster'|'3d'|'dem'} catalogKey - Catalog the product lives in. + * @param {string} productId - Product identifier. + * @param {string} productType - Product type. + * @param {string} scheme - Scheme name to pick from the record (e.g. 'WMTS', 'WCS', '3DTiles'). + * @returns {Promise<{ url: string, name: string }>} Service URL and layer name from the matched `` element. + * @throws If the catalog key is unknown, the CSW request fails, or the scheme is not advertised. + */ +export async function fetchServiceLink(catalogKey, productId, productType, scheme) { + const typename = TYPENAMES[catalogKey]; + if (!typename) throw new Error(`Unknown catalog: ${catalogKey}`); + const namespace = namespaceFor(catalogKey); + const res = await fetch(`${MAPCOLONIES_CATALOG_URL}/api/${catalogKey}/v1/csw`, { + method: 'POST', + headers: { 'Content-Type': 'application/xml', 'x-api-key': TOKEN }, + body: buildGetRecordsBody(typename, namespace, productId, productType) + }); + if (!res.ok) { + throw new Error(`CSW ${catalogKey} ${productId} failed: ${res.status}`); + } + const link = parseLink(await res.text(), namespace, scheme); + if (!link) { + throw new Error(`No "${scheme}" link in ${catalogKey}/${productId}`); + } + return link; +} diff --git a/examples/utils/wmts-utils.js b/examples/utils/wmts-utils.js new file mode 100644 index 0000000..fa4985e --- /dev/null +++ b/examples/utils/wmts-utils.js @@ -0,0 +1,69 @@ +import { TOKEN } from './config/common-config.js'; +import { RASTER_SCHEME } from './config/raster-config.js'; +import { fetchServiceLink } from './utils/catalog-client.js'; +import { parseXml } from './utils/xml-utils.js'; + +const WMTS_NS = 'http://www.opengis.net/wmts/1.0'; +const OWS_NS = 'http://www.opengis.net/ows/1.1'; + +/** + * Extracts the RESTful tile URL template for a specific layer from a WMTS capabilities document. + * + * The returned template contains WMTS placeholders ({TileMatrix}, {TileRow}, {TileCol}, ...) + * that the caller is expected to substitute when requesting tiles. + * + * @param {string} capabilitiesXml - Raw WMTS capabilities XML. + * @param {string} layerName - `` of the layer to look up. + * @param {string} [format] - Optional MIME type to require on the ResourceURL (e.g. 'image/png'). + * @returns {string} The tile URL template. + * @throws If the layer is not present, or no matching tile ResourceURL is found. + */ +export function extractWmtsTileTemplate(capabilitiesXml, layerName, format) { + const doc = parseXml(capabilitiesXml, 'WMTS capabilities'); + // Locate the whose matches the requested layerName. + const layer = Array.from(doc.getElementsByTagNameNS(WMTS_NS, 'Layer')).find((node) => { + const identifier = node.getElementsByTagNameNS(OWS_NS, 'Identifier')[0]; + return identifier && identifier.textContent.trim() === layerName; + }); + if (!layer) { + throw new Error(`Layer "${layerName}" not found in WMTS capabilities`); + } + // Pick the tile ResourceURL, optionally constrained to the requested format. + const resourceUrl = Array.from(layer.getElementsByTagNameNS(WMTS_NS, 'ResourceURL')).find( + (node) => + node.getAttribute('resourceType') === 'tile' && + (!format || node.getAttribute('format') === format) + ); + if (!resourceUrl) { + throw new Error(`No tile ResourceURL for layer "${layerName}"`); + } + return resourceUrl.getAttribute('template'); +} + +/** + * Resolves the raster catalog entry for a product, fetches its WMTS capabilities, + * and returns the tile URL template along with the layer name from the catalog. + * + * Convenience helper that wraps the full catalog → capabilities → template chain. + * + * @param {string} productId - Raster product identifier. + * @param {string} productType - Raster product type. + * @param {string} [format] - Optional MIME type filter passed to `extractWmtsTileTemplate`. + * @returns {Promise<{ template: string, name: string }>} Tile URL template (still contains WMTS placeholders) and the layer name reported by the catalog. + * @throws If the catalog lookup, capabilities fetch, or layer extraction fails. + */ +export async function fetchWmtsTileTemplate(productId, productType, format) { + const { url: capabilitiesUrl, name } = await fetchServiceLink( + 'raster', + productId, + productType, + RASTER_SCHEME + ); + // Capabilities endpoint is token-gated; same token is later used per-tile by the caller. + const res = await fetch(`${capabilitiesUrl}?token=${TOKEN}`); + if (!res.ok) { + throw new Error(`Fetching WMTS capabilities failed: ${res.status}`); + } + const template = extractWmtsTileTemplate(await res.text(), name, format); + return { template, name }; +} diff --git a/examples/utils/xml-utils.js b/examples/utils/xml-utils.js new file mode 100644 index 0000000..159ab6c --- /dev/null +++ b/examples/utils/xml-utils.js @@ -0,0 +1,8 @@ +export function parseXml(xmlText, context) { + const doc = new DOMParser().parseFromString(xmlText, 'application/xml'); + const parseError = doc.getElementsByTagName('parsererror')[0]; + if (parseError) { + throw new Error(`${context} parse error: ${parseError.textContent}`); + } + return doc; +} diff --git a/package-lock.json b/package-lock.json index d98b531..27d5e95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,8 @@ "tslib": "^2.4.1", "typescript": "~5.4.0", "vite": "^4.3.0", - "vite-plugin-static-copy": "^0.15.0" + "vite-plugin-static-copy": "^0.15.0", + "zx": "^8.8.5" } }, "node_modules/@algolia/abtesting": { @@ -404,16 +405,16 @@ } }, "node_modules/@aws-sdk/checksums": { - "version": "3.1000.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.1.tgz", - "integrity": "sha512-DFCtlisEuWzw7rESV65jHK7De1QsJZRZgUNJ8ovpmdVaayPrxvmlsAlW8hka9E7f9B31d1T7lHG9oozZf6Bp6w==", + "version": "3.1000.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.2.tgz", + "integrity": "sha512-PIha+kauTbp6IRmOpYktPTrlfrrSqDVixvhO/EUOFOf62DPX81CaJoHJreuA1m9HYpSKyXf99BKjU1dvJPeUfw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -423,24 +424,20 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1061.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1061.0.tgz", - "integrity": "sha512-ygyRCIkktaDz4/kNzsxhbZqocLwCJV5absi/k7Xd3LThPOmVkid7Nghm/xTW2Yg+vSQIL0yq99oV7u3T+4ZbAQ==", + "version": "3.1063.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1063.0.tgz", + "integrity": "sha512-ETn+vvmZVK1MmOZwVBXmWANpmD5iTbzojIqyEIoZ86qo+8oWy35S8QyQNE/ZDI+WHgMU1dS+VSYbpRl1QkEySg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/credential-provider-node": "^3.972.50", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.20", - "@aws-sdk/middleware-expect-continue": "^3.972.16", - "@aws-sdk/middleware-flexible-checksums": "^3.974.26", - "@aws-sdk/middleware-location-constraint": "^3.972.13", - "@aws-sdk/middleware-sdk-s3": "^3.972.47", - "@aws-sdk/middleware-ssec": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.31", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/credential-provider-node": "^3.972.52", + "@aws-sdk/middleware-flexible-checksums": "^3.974.27", + "@aws-sdk/middleware-sdk-s3": "^3.972.48", + "@aws-sdk/signature-v4-multi-region": "^3.996.32", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", @@ -452,13 +449,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.17.tgz", - "integrity": "sha512-r8o4h2K7j6P9ngno+8ei0aK0U/4JwDb7A2fMMxGVoSqDN8AFlIzSDeZHME9LcVLR2codyhtr1WAAg+/nmkeeMA==", + "version": "3.974.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.18.tgz", + "integrity": "sha512-JDYCPI0j7zGrzXTDFsLB346cxss7J/AxH7+O0MzWlqppJBEyB9Qe6TQXRL6iwLUo/xZkNv9KFmBL2hqElmwW0g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.10", - "@aws-sdk/xml-builder": "^3.972.27", + "@aws-sdk/types": "^3.973.11", + "@aws-sdk/xml-builder": "^3.972.28", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.6", "@smithy/signature-v4": "^5.4.6", @@ -471,13 +468,13 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.43.tgz", - "integrity": "sha512-g0XVQKzaA/4cq1vz1IvCQwYM+1Pkv01J9yHDpCTXekVuGZRDEz0wqBQ1AuYTq7FM6uik4uBGH8Tb5d9YvgeA7g==", + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.44.tgz", + "integrity": "sha512-3hKJVrZ7bqXzDAXCQp+OaQ1ASN+vWstaNuEH418wQVl//cRZhqhfR9Bjk1qIWmgUGe8/D3gdO73PgidRj378EQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -487,13 +484,13 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.45", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.45.tgz", - "integrity": "sha512-w9PuOoKCt6+xoESvY+zlV0u3PKQ0mVL259PcsVR6a3S/uYJJHnIi4r1NxdJHEcNldUVRIciltWnFMGBR4YEm3g==", + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.46.tgz", + "integrity": "sha512-VhwC9pGAZHhiQ2xSViyOPDFqvr9aRxGCAXZtADsUhU3R65nad7y//CwynE6mQnWNR+suRlqE79W36IVayL+m1g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", @@ -505,20 +502,20 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.48", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.48.tgz", - "integrity": "sha512-+6BQ6Lrnc+EyAGElLRW6j+Sa+RirPHnIJsobvYO6nnyK+oGKmz1ne/ieclbLWyjyDKEU3/JVJWcWY3VLFPvGtQ==", + "version": "3.972.50", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.50.tgz", + "integrity": "sha512-09Xi6ovxiK42+De/qBGF71sT5F2bWgYM+1fFyDwSOpy1xpsQ5R/naIu7MVDpH6Dic36QNc8dAv4KADtMGK2JYg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/credential-provider-env": "^3.972.43", - "@aws-sdk/credential-provider-http": "^3.972.45", - "@aws-sdk/credential-provider-login": "^3.972.47", - "@aws-sdk/credential-provider-process": "^3.972.43", - "@aws-sdk/credential-provider-sso": "^3.972.47", - "@aws-sdk/credential-provider-web-identity": "^3.972.47", - "@aws-sdk/nested-clients": "^3.997.15", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/credential-provider-env": "^3.972.44", + "@aws-sdk/credential-provider-http": "^3.972.46", + "@aws-sdk/credential-provider-login": "^3.972.49", + "@aws-sdk/credential-provider-process": "^3.972.44", + "@aws-sdk/credential-provider-sso": "^3.972.49", + "@aws-sdk/credential-provider-web-identity": "^3.972.49", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", @@ -529,14 +526,14 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.47", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.47.tgz", - "integrity": "sha512-Iy2ebWVgrZBH05464uJiQYu6HSSiROnwVZptthEFXx2gWjo1ORCxEAFZB5Cr2MdfrSnZ+0QUPkZ1ZpCqpkUrLQ==", + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.49.tgz", + "integrity": "sha512-EfJF/1Fh9mI4pZyoheU2RY9xUhTcugIZNkD63+orXMkYj/QXacJNbKVDUK90Yv5hE+aX+rt9J/EZ9Qr3vKOa7g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/nested-clients": "^3.997.15", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -546,18 +543,18 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.50", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.50.tgz", - "integrity": "sha512-b05Aelq5cqAvCCDQjCYacl0XmR8QhBNSqLbsdISkQmlQBa5oPS66zYPteWcSp5LswbpoIe552EUGjluKiadBig==", + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.52.tgz", + "integrity": "sha512-7QX+PbyiWBEOVipJq8Nke/TqXT6lAPLE7fvTaopa39/IVWuLfS+Fzdy71sZJONf/mLGgmtj6aU17+REw3+aRrw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.43", - "@aws-sdk/credential-provider-http": "^3.972.45", - "@aws-sdk/credential-provider-ini": "^3.972.48", - "@aws-sdk/credential-provider-process": "^3.972.43", - "@aws-sdk/credential-provider-sso": "^3.972.47", - "@aws-sdk/credential-provider-web-identity": "^3.972.47", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/credential-provider-env": "^3.972.44", + "@aws-sdk/credential-provider-http": "^3.972.46", + "@aws-sdk/credential-provider-ini": "^3.972.50", + "@aws-sdk/credential-provider-process": "^3.972.44", + "@aws-sdk/credential-provider-sso": "^3.972.49", + "@aws-sdk/credential-provider-web-identity": "^3.972.49", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/credential-provider-imds": "^4.3.7", "@smithy/types": "^4.14.3", @@ -568,13 +565,13 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.43", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.43.tgz", - "integrity": "sha512-GPokLNyvTfCmuaHk+v3GKVs4ZT3cMu5kgS2a+NPkOMt96cq6fSIK0g+mZHpGS6Cd4QGrPKesANEaLUKgOskTzg==", + "version": "3.972.44", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.44.tgz", + "integrity": "sha512-V+UUhZpRP7QDRhi+qgBDisM9tUBnYmMje8Bk77A6MZsfeGeGdMsQXmaHP1CDYFcept0o/Rz5g2Y0TMeVlG9dzg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -584,15 +581,15 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.47", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.47.tgz", - "integrity": "sha512-0AzvLrzlvJs0DzbeWGvNj+bX3Uzd7VNS6vDqCOdZzBlCGKGd78uxctJSW9iK/Rt/nxiJqpTvrYQlVJ4guVM2Dw==", + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.49.tgz", + "integrity": "sha512-9QqOYGuh5tZ76OzaT68kwI78AH+5lS/uZGGvkfxb3fc8FzRrIz2jOufNTliEBEeSAwmgK2rWLNsK+IB3zbtNPA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/nested-clients": "^3.997.15", - "@aws-sdk/token-providers": "3.1060.0", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/token-providers": "3.1063.0", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -602,14 +599,14 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.47", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.47.tgz", - "integrity": "sha512-eksfbUErOejUAGWBAcNqaP7IX21oUOEo73d9R56k9Ua4d57qS90NEYkWJsuSGzTXMFulCu17qXJI/qGmM7hvoA==", + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.49.tgz", + "integrity": "sha512-IYx1lN38MnnPXv+NBLpuATu0cZakbZ321TAfjW+aVkw7HIJF38YnEwdeEO55MSl3pl7hIX1IvvnD6EmnAzmAJw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/nested-clients": "^3.997.15", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -618,52 +615,13 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.20.tgz", - "integrity": "sha512-D35MfedGvTTzK1oygFPjm7DViSJwj9cuPV26ElHKwZqEz2rWag1hzYeAQ7st0jlCIAAihQgOyQ0/JwmqLOOinw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.47", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.16.tgz", - "integrity": "sha512-S52iw+M9zJC+7uxRdvvKeiR0s2PDeYEmbNZQkWE6OJf8upIs+r4WQY0TER+6akVitEMeRdwS0DrBUhKkmpsyng==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.47", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.26", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.26.tgz", - "integrity": "sha512-WndRXQV8wAU/bW3GH8THumEOSV7FpS0AtoluT2M7lYaaDUyG0gOCD+DppB+IWQ4TPmzuTtFcCedh9xCzM4Zv4g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/checksums": "^3.1000.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.13.tgz", - "integrity": "sha512-Yh0MmpADMsSR7ExRM/2w85D26i/U2aDC/pC7fMwhUpmOl6sebGpmBPoRL/uJRDhqRrwX/tvXWWZrsbsPM/O9FQ==", + "version": "3.974.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.27.tgz", + "integrity": "sha512-bZqezPLdllFC4VAeV/f+EIc/hz56ab3TD/+4zNCgOgmG5ZHAE5dMHrX1gtTwdcQXbPr3KR7x3zTC3zuCTE6+ng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.47", + "@aws-sdk/checksums": "^3.1000.2", "tslib": "^2.6.2" }, "engines": { @@ -671,14 +629,14 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.47", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.47.tgz", - "integrity": "sha512-fzVBvGib8P1G6RFV3qVTPlXy9bMFAy5nxhdhA7LwyhWjRkJufNfJIPiloZq2mt36YAXSlLsEa4s3Kgcw6cv3+g==", + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.48.tgz", + "integrity": "sha512-MRTqx8wD/T3REt6LTT3/yN8rrp6+xIHrbUekkDYJTYWVch70mwtdJBovR4qKJz1jIPlbN+9R/Sn6R04BfsglzA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/signature-v4-multi-region": "^3.996.31", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/signature-v4-multi-region": "^3.996.32", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -687,30 +645,17 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.13.tgz", - "integrity": "sha512-M+dDhWp2zv9u92I4/4rgUFdiF8jSIk5PIj5ktyBdhvR/dkmKSYMo07nuh+3g8/59HnizwkcRC3glcLMX5GhyaQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.47", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.15.tgz", - "integrity": "sha512-Fpri1/PXKMKveORZ7E00VLTlWS5DkfZkW70PUE+bOnpWpAeHAQLoiDHhkzN3kNWbbSsGg64+IZYiq/EZgME3Mg==", + "version": "3.997.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.17.tgz", + "integrity": "sha512-lDRgraoTfKRawUyc176Ow93mrNrOho/x+EoK4C+lKU+vKkHWhNhzvSMVAx0WEJUJoeQxxDN5ZdKMfiGEyNejig==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/signature-v4-multi-region": "^3.996.31", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/signature-v4-multi-region": "^3.996.32", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/fetch-http-handler": "^5.4.6", "@smithy/node-http-handler": "^4.7.6", @@ -722,12 +667,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.31.tgz", - "integrity": "sha512-Kn2up9SlG1KC6wRtwf0d7waTGF6rvp9DxYqB54x6UCKdQ6kyaXCqHL4WGb5vUJga5kS8FxnjhY0LqM28aMvnNQ==", + "version": "3.996.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.32.tgz", + "integrity": "sha512-llvApLcsWtmRFhG2wT3WIp1CmDeRaIYutqty1ZZXoMzK7TiJ6MOLOimk9eXUS8PwgG4ew4pa4QAbt0lfhn++1w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/types": "^3.973.11", "@smithy/signature-v4": "^5.4.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -737,14 +682,14 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1060.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1060.0.tgz", - "integrity": "sha512-6NZaMKkFhpaNiwLpHi1sZaYjidL/lCJE6ME6NxwA8gv9vQna+Kr0j4OFwVoz6tANRWM3WbGz6jiPsGX/Vkjwow==", + "version": "3.1063.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1063.0.tgz", + "integrity": "sha512-nYDaWWdzjKiDP5xj8k4oUgcYd4WPgzfAOgdU5vJsaqH/07Dfvm7ffisHCFJ+NEl7kUC9JEIUxh0kznvenbo3NQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.17", - "@aws-sdk/nested-clients": "^3.997.15", - "@aws-sdk/types": "^3.973.10", + "@aws-sdk/core": "^3.974.18", + "@aws-sdk/nested-clients": "^3.997.17", + "@aws-sdk/types": "^3.973.11", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" @@ -754,9 +699,9 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.10.tgz", - "integrity": "sha512-992QrTO7G9qCvKD0fx1rMlqcL14plUcRAbwmqqYVsuF3GrqcvlAL9qxR+baMafarEZ+l7DUQ5lCMmt5mbMhF7g==", + "version": "3.973.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.11.tgz", + "integrity": "sha512-YjS0qFuECClRh4qhEyW8XagW0fwEPBeZ1cfsW/gU73Kh/ExFILxbzxOfPCmzF/2DwEvhvsHYt0b0qnvStwKYrg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.3", @@ -767,9 +712,9 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "version": "3.965.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.6.tgz", + "integrity": "sha512-ZfHjfwSzeXj+Lg9AK5ZNmeDkXev6V+w2tn1t4kgDdRtUaRCthepTQiFwbD06EF9oNGH4LaLg+Mb6U16Ypv5bSw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -779,9 +724,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.27.tgz", - "integrity": "sha512-hpsCXCOI436kxWpjtRuIHVvuPP81MOw8f18jzfZeg+UOiiOvlqWcmWChzEhJEu16cOC6+ku4ncBN+7rdt+DZ9g==", + "version": "3.972.28", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.28.tgz", + "integrity": "sha512-lI/l3c/vPvsxmspzV63NfS3x9q4CkMmdhJy4QiM+NThAufVkDvi/PZZQ6xETnICL0UD7jI808pY83gllf86RFg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.3", @@ -1879,9 +1824,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", + "version": "20.19.42", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.42.tgz", + "integrity": "sha512-5L7SUaFC1RyDraj2yRhyBzHTobyXHmohD100CChNtyPyleoq37Mqab5Gn8XEKI04dfN/oqPdpHk38MgcQWHbZg==", "dev": true, "license": "MIT", "dependencies": { @@ -2349,9 +2294,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.33", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", - "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "version": "2.10.34", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.34.tgz", + "integrity": "sha512-IMDedajPifLnHNY0X9n8hKxRTQ6/eTHwr5bDo04WnuqxyKw6LYtQywCuuqPZwhl3aBXMvQpJov42GLCwRRdQzw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2476,9 +2421,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", "dev": true, "funding": [ { @@ -2775,9 +2720,9 @@ "license": "ISC" }, "node_modules/electron-to-chromium": { - "version": "1.5.367", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.367.tgz", - "integrity": "sha512-4Mk/mrynCNQ+atY40D3UpmhLWB6AHMbYMlIrPhHcMF6x0L7O0b052FCAsxw1LlaR++UFuNg3D/A6XCuGDa0guQ==", + "version": "1.5.368", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.368.tgz", + "integrity": "sha512-7RckJJK4uESJF9PxvfMWd3TGqIiieUTG4HxnKaKuIpGbcr+r2ZEB3g2gAhCP3Fqm42vJSzLfgab9eva/C4/XVw==", "dev": true, "license": "ISC" }, @@ -4699,11 +4644,10 @@ } }, "node_modules/protobufjs": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.5.0.tgz", - "integrity": "sha512-df1jWDPA5VIBNRtuAHjqr09f2qN5D4Vke1wYqOQg1XJ7ZDpA7BD6L7E4tyChgGRLB5hqk2m79Zsy0WHwV9a84A==", + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.1.tgz", + "integrity": "sha512-s4qQPr4pU0W95iYnUInh95skjIg+3aM2sakYsw60QYanU+qWRDY2zQxOAQV6zU7ROJpSNDG9B+VSmk4dqdWWSA==", "dev": true, - "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { "long": "^5.3.2" @@ -5841,6 +5785,19 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zx": { + "version": "8.8.5", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.8.5.tgz", + "integrity": "sha512-SNgDF5L0gfN7FwVOdEFguY3orU5AkfFZm9B5YSHog/UDHv+lvmd82ZAsOenOkQixigwH2+yyH198AwNdKhj+RA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + } } } } diff --git a/package.json b/package.json index 386af58..51b8c0a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --plugin-search-dir . --check . && eslint .", - "format": "prettier --plugin-search-dir . --write ." + "format": "prettier --plugin-search-dir . --write .", + "upload-examples": "zx scripts/upload-examples.mjs", + "upload-all": "zx scripts/upload-examples.mjs --assets" }, "devDependencies": { "@sveltejs/adapter-auto": "^2.0.0", @@ -38,7 +40,8 @@ "tslib": "^2.4.1", "typescript": "~5.4.0", "vite": "^4.3.0", - "vite-plugin-static-copy": "^0.15.0" + "vite-plugin-static-copy": "^0.15.0", + "zx": "^8.8.5" }, "type": "module", "dependencies": { diff --git a/release-please-config.json b/release-please-config.json index e1b178b..f310b21 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -1,8 +1,8 @@ { - "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", - "release-type": "node", - "include-component-in-tag": false, - "packages": { - ".": {} - } + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "node", + "include-component-in-tag": false, + "packages": { + ".": {} + } } diff --git a/scripts/upload-examples.mjs b/scripts/upload-examples.mjs new file mode 100644 index 0000000..79486a7 --- /dev/null +++ b/scripts/upload-examples.mjs @@ -0,0 +1,109 @@ +#!/usr/bin/env zx +// Upload all files under examples/ to S3 using @aws-sdk/client-s3. +// New and changed files (MD5 vs S3 ETag) are uploaded; identical files are skipped. +// Reads AWS_* env vars (same as the app). +// +// Usage: +// npx zx scripts/upload-examples.mjs [--assets|-a] [examples-dir] +// +// --assets / -a also upload the examples/assets/ folder (omit to skip it) + +import { createHash } from 'node:crypto'; +import { fileURLToPath } from 'node:url'; +import { paginateListObjectsV2, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { argv, dotenv, echo, fs, glob, path } from 'zx'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +const envFile = path.join(repoRoot, '.env'); +if (fs.existsSync(envFile)) dotenv.config(envFile); + +const includeAssets = argv.assets || argv.a || false; +const examplesDir = argv._[0] ? path.resolve(argv._[0]) : path.join(repoRoot, 'examples'); +const parallelism = Number(process.env.UPLOAD_PARALLELISM ?? 32); + +const bucket = process.env.AWS_BUCKET; +if (!bucket) { + echo('ERROR: AWS_BUCKET is required'); + process.exit(1); +} +if (!(await fs.pathExists(examplesDir))) { + echo(`ERROR: examples directory not found: ${examplesDir}`); + process.exit(1); +} + +const client = new S3Client({ + endpoint: process.env.AWS_ENDPOINT_URL, + forcePathStyle: true, + region: process.env.AWS_REGION ?? 'us-east-1' +}); + +const CONTENT_TYPES = { + '.html': 'text/html', + '.css': 'text/css', + '.js': 'text/javascript', + '.json': 'application/json', + '.txt': 'text/plain', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.gif': 'image/gif' +}; + +// Fetch all existing S3 keys + ETags. +echo('Fetching existing S3 objects...'); +const etagByKey = new Map(); +for await (const page of paginateListObjectsV2({ client }, { Bucket: bucket })) { + for (const { Key, ETag } of page.Contents ?? []) { + etagByKey.set(Key, ETag.replaceAll('"', '')); + } +} +echo(`Found ${etagByKey.size} existing objects in S3.\n`); + +// Collect local files whose MD5 differs from the S3 ETag. +// ETag equals MD5 for non-multipart uploads, which holds for everything PutObject sends. +const files = await glob('**/*', { + cwd: examplesDir, + ignore: includeAssets ? [] : ['assets/**'] +}); + +const toUpload = []; +for (const key of files.sort()) { + const body = await fs.readFile(path.join(examplesDir, key)); + const md5 = createHash('md5').update(body).digest('hex'); + if (etagByKey.get(key) !== md5) { + toUpload.push({ key, body, isNew: !etagByKey.has(key) }); + } +} +echo(`${files.length - toUpload.length} files unchanged, ${toUpload.length} to upload.\n`); + +// Promise pool: `parallelism` workers pulling from a shared iterator. +const counts = { uploaded: 0, updated: 0, failed: 0 }; +const iter = toUpload[Symbol.iterator](); +await Promise.all( + Array.from({ length: parallelism }, async () => { + for (const { key, body, isNew } of iter) { + try { + await client.send( + new PutObjectCommand({ + Bucket: bucket, + Key: key, + Body: body, + ContentType: CONTENT_TYPES[path.extname(key)] ?? 'application/octet-stream' + }) + ); + echo(`${isNew ? 'UP' : 'UPDATE'} ${key}`); + counts[isNew ? 'uploaded' : 'updated']++; + } catch (err) { + echo(`FAIL ${key} (${err.message ?? err})`); + counts.failed++; + } + } + }) +); + +echo( + `\nDone — uploaded: ${counts.uploaded} new, ${counts.updated} updated, ${ + files.length - toUpload.length + } skipped, ${counts.failed} failed` +); +if (counts.failed > 0) process.exit(1); diff --git a/src/lib/components/bottomBar.svelte b/src/lib/components/bottomBar.svelte index 0971050..3ceec36 100644 --- a/src/lib/components/bottomBar.svelte +++ b/src/lib/components/bottomBar.svelte @@ -7,6 +7,7 @@ BottomNavHeaderItem } from 'flowbite-svelte'; import { goto } from '$app/navigation'; + import { page } from '$app/stores'; import BottomHeaderItem from './bottomHeaderItem.svelte'; import classNames from 'classnames'; @@ -14,6 +15,8 @@ export let items: { name: string; displayName?: string }[]; export let activeClient: string | undefined; + $: activeItem = $page.params.name; + $: outerDiv = classNames('-translate-x-0', 'dark:bg-gray-800', $$props.outerDiv); @@ -41,7 +44,12 @@ goto('/demo/' + activeClient + '/' + item.name)} id="group-{item.name}" - btnDefault="basis-0 items-center justify-center ml-[4px] mb-1 bg-gray-100 dark:bg-gray-600 rounded-lg p-4 text-gray-900 hover:bg-gray-200 dark:text-white dark:hover:bg-gray-700 group" + btnDefault={classNames( + 'basis-0 items-center justify-center ml-[4px] mb-1 rounded-lg p-4 group', + item.name === activeItem + ? 'bg-blue-600 text-white hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600' + : 'bg-gray-100 dark:bg-gray-600 text-gray-900 hover:bg-gray-200 dark:text-white dark:hover:bg-gray-700' + )} > {item.displayName || item.name}