From a56e90703cd89d16d9e463ea7ed43289d6f67d61 Mon Sep 17 00:00:00 2001 From: Leonard Follner Date: Mon, 30 Jan 2023 13:46:53 +0100 Subject: [PATCH] FEATURE: map widget --- .gitignore | 1 - .localbeach.docker-compose.yaml | 1 + .../Neos.NeosIo/NodeTypes/Content/Map.yaml | 51 +++++++++ .../Neos.NeosIo/NodeTypes/Content/Stage.yaml | 1 + .../NodeTypes/Override/MultiColumn.yaml | 2 + .../Private/Fusion/Content/Map/Map.fusion | 39 +++++++ .../Fusion/Documents/DefaultPage.fusion | 2 +- .../Private/JavaScript/Components/Map.js | 51 +++++++++ .../Private/JavaScript/Components/index.js | 4 +- Web/index.php | 104 ++++++++++++++++++ webpack.config.ts | 10 +- 11 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 DistributionPackages/Neos.NeosIo/NodeTypes/Content/Map.yaml create mode 100644 DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Content/Map/Map.fusion create mode 100644 DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/Map.js create mode 100644 Web/index.php diff --git a/.gitignore b/.gitignore index ef74afe45..4f46188cc 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ /Data/ /Web/_Resources /Web/.htaccess -/Web/index.php /bin/ /Packages /flow diff --git a/.localbeach.docker-compose.yaml b/.localbeach.docker-compose.yaml index b1dd9a37a..e635c8dca 100644 --- a/.localbeach.docker-compose.yaml +++ b/.localbeach.docker-compose.yaml @@ -64,6 +64,7 @@ services: - CROWD_API_USERNAME=${CROWD_API_USERNAME} - CROWD_API_PASSWORD=${CROWD_API_PASSWORD} - GITHUB_API_TOKEN=${GITHUB_API_TOKEN} + - SANDSTORM_MAPS_API_KEY=${SANDSTORM_MAPS_API_KEY} redis: image: ${BEACH_REDIS_IMAGE:-flownative/redis}:${BEACH_REDIS_IMAGE_VERSION:-latest} diff --git a/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Map.yaml b/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Map.yaml new file mode 100644 index 000000000..18c9c0f10 --- /dev/null +++ b/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Map.yaml @@ -0,0 +1,51 @@ +'Neos.NeosIo:Content.Map': + superTypes: + 'Neos.Neos:Content': true + ui: + label: 'Map' + icon: icon-map + inspector: + groups: + general: + label: i18n + properties: + label: 'Properties' + tab: default + + properties: + + lat: + type: string + defaultValue: '51.0567032' + ui: + label: 'Latitude' + reloadIfChanged: true + inspector: + group: properties + + lng: + type: string + defaultValue: '13.7769583' + ui: + label: 'Longitude' + reloadIfChanged: true + inspector: + group: properties + + zoom: + type: string + defaultValue: '14' + ui: + label: 'Default zoom level' + reloadIfChanged: true + inspector: + group: properties + + popupText: + type: string + defaultValue: '' + ui: + label: 'Popup-Text' + reloadIfChanged: true + inspector: + group: properties diff --git a/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Stage.yaml b/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Stage.yaml index a27c698c2..45733917d 100644 --- a/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Stage.yaml +++ b/DistributionPackages/Neos.NeosIo/NodeTypes/Content/Stage.yaml @@ -35,6 +35,7 @@ 'Neos.NeosIo.FeatureList:FeatureList': true 'Neos.NeosIo.CaseStudies:Content.CaseList': true 'Neos.NeosIo:PostArchive': true + 'Neos.NeosIo:Content.Map': true ui: label: Stage icon: icon-tasks diff --git a/DistributionPackages/Neos.NeosIo/NodeTypes/Override/MultiColumn.yaml b/DistributionPackages/Neos.NeosIo/NodeTypes/Override/MultiColumn.yaml index 95638048f..f5fab9dca 100644 --- a/DistributionPackages/Neos.NeosIo/NodeTypes/Override/MultiColumn.yaml +++ b/DistributionPackages/Neos.NeosIo/NodeTypes/Override/MultiColumn.yaml @@ -60,6 +60,7 @@ 'Neos.NeosIo:Youtube': TRUE 'Neos.NeosIo:VideoEmbed': TRUE 'PunktDe.CodeView:Code': true + 'Neos.NeosIo:Content.Map': TRUE column1: constraints: *twoColumnConstraints @@ -116,6 +117,7 @@ 'Neos.NeosIo:Icon': TRUE 'Neos.NeosIo:Youtube': TRUE 'Neos.NeosIo:VideoEmbed': TRUE + 'Neos.NeosIo:Content.Map': TRUE column1: constraints: *threeColumnConstraints column2: diff --git a/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Content/Map/Map.fusion b/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Content/Map/Map.fusion new file mode 100644 index 000000000..e4667cf83 --- /dev/null +++ b/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Content/Map/Map.fusion @@ -0,0 +1,39 @@ +prototype(Neos.NeosIo:Content.Map) < prototype(Neos.Neos:ContentComponent) { + renderer = Neos.NeosIo:Map { + lat = ${q(node).property('lat')} + lng = ${q(node).property('lng')} + zoom = ${q(node).property('zoom')} + popupText = ${q(node).property('popupText')} + } +} + +prototype(Neos.NeosIo:Map) < prototype(Neos.Fusion:Component) { + lat = 0 + lat.@process.toFloat = ${String.toFloat(value)} + lng = 0 + lng.@process.toFloat = ${String.toFloat(value)} + zoom = 14 + zoom.@process.toFloat = ${String.toFloat(value)} + + popupText = '' + + @propTypes { + @strict = true + lat = ${PropTypes.float.isRequired} + lng = ${PropTypes.float.isRequired} + zoom = ${PropTypes.float.isRequired} + popupText = ${PropTypes.string} + } + + renderer = afx` + +
+ ` +} diff --git a/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Documents/DefaultPage.fusion b/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Documents/DefaultPage.fusion index 8c55324cd..78721b837 100644 --- a/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Documents/DefaultPage.fusion +++ b/DistributionPackages/Neos.NeosIo/Resources/Private/Fusion/Documents/DefaultPage.fusion @@ -22,7 +22,7 @@ prototype(Neos.NeosIo:DefaultPage) < prototype(Neos.Neos:Page) { } javascripts.site = afx` - ` diff --git a/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/Map.js b/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/Map.js new file mode 100644 index 000000000..b759c16d0 --- /dev/null +++ b/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/Map.js @@ -0,0 +1,51 @@ +import BaseComponent from "DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/BaseComponent"; +import inViewport from "in-viewport"; + +class Map extends BaseComponent { + constructor(el) { + super(el); + const isAlreadyVisible = inViewport(el); + + // + // Now let's check initially for the visibility in the viewport. + // + if (isAlreadyVisible) { + this.loadMap(); + } else { + inViewport( + el, + { + offset: 300 + }, + () => this.loadMap() + ); + } + } + + loadMap() { + import('/_maptiles/frontend/v1.1/map-main.js') + .then(async ({maplibregl, createMap}) => { + let map = await createMap(window.location.protocol + '//' + window.location.host + '/_maptiles', { + container: this.el, // HTML Element + center: [this.lng, this.lat], // starting position [lng, lat] + zoom: this.zoom, // starting zoom + }); + + map.addControl(new maplibregl.NavigationControl(), 'top-left'); + + new maplibregl.Marker() + .setLngLat([this.lng, this.lat]) + .setPopup(new maplibregl.Popup({offset: 25}).setText(this.popupText)) + .addTo(map); + }); + } +} + +Map.prototype.props = { + lat: '', + lng: '', + zoom: '', + popupText: '', +} + +export default Map; diff --git a/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/index.js b/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/index.js index d73ba6494..8c698c4ed 100644 --- a/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/index.js +++ b/DistributionPackages/Neos.NeosIo/Resources/Private/JavaScript/Components/index.js @@ -7,6 +7,7 @@ import ScrollTo from './ScrollTo'; import ProgressiveImage from './ProgressiveImage'; import ScrollClassToggler from './ScrollClassToggler'; import SentenceSwitcher from './SentenceSwitcher'; +import Map from './Map'; export { ClassToggler, @@ -16,5 +17,6 @@ export { SentenceSwitcher, ProgressiveImage, ScrollClassToggler, - EmptyClickHandler + EmptyClickHandler, + Map }; diff --git a/Web/index.php b/Web/index.php new file mode 100644 index 000000000..034b2e169 --- /dev/null +++ b/Web/index.php @@ -0,0 +1,104 @@ +run(); + +/** + * Proxy requests to sandstorm map server + */ +function proxyMap(string $apiKey): void +{ + $environment = new EnvironmentConfiguration('maptiles-proxy', '/tmp/maptiles/'); + $backend = new FileBackend($environment); + $cache = new StringFrontend('maptiles', $backend); + $backend->setCache($cache); + + // identify request headers + $requestHeaders = []; + foreach ($_SERVER as $key => $value) { + if (strpos($key, 'HTTP_') === 0) { + $headerName = str_replace('_', ' ', substr($key, 5)); + $headerName = str_replace(' ', '-', ucwords(strtolower($headerName))); + if (in_array($headerName, ['Accept-Encoding', 'Accept-Language', 'Accept',])) { + $requestHeaders[] = "$headerName: $value"; + } + } + } + + // identify path from url + preg_match('/^\/_maptiles\/(?P.*)$/', $_SERVER['REQUEST_URI'], $matches); + if ($matches['path']) { + $requestUrl = MAPS_BASE_URI . $matches['path'] . '?t=' . $apiKey; + } else { + header('HTTP/1.1 404'); + die(); + } + + $cacheIdentifier = md5($requestUrl . implode('', $requestHeaders)); + $response = $cache->get($cacheIdentifier); + + if (!$response) { + $curlHandle = curl_init($requestUrl); + curl_setopt($curlHandle, CURLOPT_HTTPHEADER, $requestHeaders); // (re-)send headers + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); // return response + curl_setopt($curlHandle, CURLOPT_HEADER, true); // enabled response headers + $response = curl_exec($curlHandle); + curl_close($curlHandle); + + $cache->set($cacheIdentifier, $response, [], MAPS_CACHE_LIFETIME); + } + + // split response to header and content + [$responseHeaders, $responseContent] = preg_split('/(\r\n){2}/', $response, 2); + + // (re-)send the headers + $responseHeaders = preg_split('/\r\n/', $responseHeaders); + foreach ($responseHeaders as $responseHeader) { + if (strpos($responseHeader, 'Transfer-Encoding:') !== 0) { + header($responseHeader, false); + } + } + + // finally, output the content + echo $responseContent; +} diff --git a/webpack.config.ts b/webpack.config.ts index 5e4c92e39..616a929d8 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -140,7 +140,15 @@ function config( extensions: ['*', '.js', '.jsx', '.ts', '.tsx', '.scss'], // absolute paths for JS and SCSS related files alias: alias - } + }, + target: 'es11', // required for dynamic imports + externals: { + '/_maptiles/frontend/v1.1/map-main.js': '/_maptiles/frontend/v1.1/map-main.js', + }, + externalsType: 'module', + experiments: { + outputModule: true, // required for externalsType: 'module' + }, }; }