feat(blocks-google-maps): Add google maps as a default block.

This commit is contained in:
Gervwyk 2022-06-13 09:01:31 +02:00
parent 3133feeefe
commit b0daaec9fa
7 changed files with 778 additions and 0 deletions

View File

@ -0,0 +1,372 @@
# Lowdefy Google Maps Blocks
This repository provides Lowdefy blocks for the [Google Maps API](https://developers.google.com/maps/documentation/javascript/overview) is a feature rich javascript map API that lets you customize maps with your own content and imagery for display on web pages and mobile devices.
[google-map-react](https://github.com/google-map-react/google-map-react) is a component written over a small set of the [Google Maps API](https://developers.google.com/maps/).
## Blocks
To use this block, define a [Custom Block type](https://docs.lowdefy.com/custom-blocks) in your Lowdefy app:
```yaml
name: my-app
lowdefy: 3.22.0
types:
GoogleMaps:
url: https://blocks-cdn.lowdefy.com/v3.22.0/blocks-google-maps/meta/GoogleMaps.json
# ...
```
### Block type Urls
- `GoogleMaps`: https://blocks-cdn.lowdefy.com/v3.22.0/blocks-google-maps/meta/GoogleMaps.json
### Properties
- `bootstrapURLKeys`: { key: '', language: 'en', region: 'en', libraries: ['places'], ...otherUrlParams, } If you want to include additional libraries to load with the maps api, indicate them in the libraries property of the bootstrapURLKeys object.
- `center`: Can be set to [lat, lng] or { lat: lat, lng: lng}. A position object for the center.
- `debounced`: Defaults to true. Whether map events are debounced.
- `defaultCenter`: Can be set to [lat, lng] or { lat: lat, lng: lng}. A position object for the center.
- `defaultZoom`: Map zoom level.
- `heatmap`: To use the heatmap layer, add visualization to the libraries property array on bootstrapURLKeys and provide the data & configuration for the heatmap in heatmap as props. If you have multiple maps in your project and require a heatmap layer in at least one of them, provide libraries:['visualization'] to all of them.
- `height`: The height of the map block.
- `hoverDistance`: Defaults to 30. Map hover distance.
- `layerTypes`: Examples ['TrafficLayer', 'TransitLayer']. The layer types to be included in the map.
- `mapOptions`: Custom map options.
- `margin`: Map margin.
- `markers`: A list of Markers with properties, `map` is provided by the default by the block, see [Javascript API Markers](https://developers.google.com/maps/documentation/javascript/markers) for configuration details.
- `resetBoundsOnResize`: Default: false, When true this will reset the map bounds if the parent resizes.
- `style`: Custom map css properties to apply to map block.
- `zoom`: Map zoom level.
### Events
- `onClick`: Trigger onClick actions when the map is clicked, returns `_event` object:
- `event`: event object
- `lat`: latitudinal coordinate
- `lng`: longitudinal coordinate
- `maps`: has functions removed
- `x`: position on map block
- `y`: position on map block
- `onClickMarker`: Trigger onClick actions when a marker is clicked, returns `_event` object:
- `domEvent`: event object
- `latLng`:
- `lat`: latitudinal coordinate
- `lng`: longitudinal coordinate
- `maps`: has functions removed
- `pixel`:
- `x`
- `y`
- `onDrag`: Trigger onDrag actions when a map is dragged, returns `_event` object:
- `maps`: has functions removed
- `onDragEnd`: Trigger onDragEnd actions when a map is finished being dragged, returns `_event` object:
- `maps`: has functions removed
- `onGoogleApiLoaded` Trigger onGoogleApiLoaded actions when the map api is loaded, returns `_event` object:
- `maps`: has functions removed
- `onMapTypeIdChange`: Trigger onMapTypeIdChange actions when the map type is changed, returns `_event` object:
- `maps`: has functions removed
- `type`: the map
- `onTilesLoaded`: Trigger onTilesLoaded actions when the map tiles are loaded, returns `_event` object:
- `maps`: has functions removed
- `onZoomAnimationStart`: Trigger onZoomAnimationStart actions when the map is zoomed, returns `_event` object:
- `maps`: has functions removed
- `zoom`: map zoom level
- `onZoomAnimationEnd`: Trigger onZoomAnimationEnd actions after the map is zoomed, returns `_event` object:
- `maps`: has functions removed
- `zoom`: map zoom level
### Methods
- `addMarker`: Accepts a single parameter object `marker` with marker properties.
- `removeMarker`: Accepts a single parameter object `marker` with position property.
- `fitBounds`: Accepts a two parameters, `bounds` and `mapSize`.
- `addHeatmap`: Accepts a single parameter object `heatmap` with heatmap properties.
- `toggleHeatmap`: Doesn't require any parameters.
## Examples
1. Add a list of markers, one with a tooltip:
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 14
center:
lat: -25.344
lng: 131.036
markers:
- position:
lat: -25.344
lng: 131.036
label: One
tooltip: '<div style="color:blue">Hello World!</div>'
- position:
lat: -25.348
lng: 131.038
label: Two
```
2. Add a marker:
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 14
center:
lat: -25.344
lng: 131.036
events:
onClick:
- id: add_marker
type: CallMethod
params:
blockId: google_maps
method: addMarker
args:
- position:
lat:
_event: lat
lng:
_event: lng
label: Hi
```
3. Remove a marker:
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 14
center:
lat: -25.344
lng: 131.036
events:
onClickMarker:
- id: set_click
type: SetState
params:
latLng:
_event: latLng
- id: remove_marker
type: CallMethod
params:
blockId: google_maps
method: removeMarker
args:
- position:
lat:
_state: latLng.lat
lng:
_state: latLng.lng
```
4. Fit bounds:
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 14
center:
lat: -25.344
lng: 131.036
events:
onClick:
- id: fit_bounds
type: CallMethod
params:
blockId: google_maps
method: fitBounds
args:
- ne:
lat: 50.01038826014866
lng: -118.6525866875
sw:
lat: 32.698335045970396
lng: -92.0217273125
- width: 640 # Map width in pixels
height: 380 # Map height in pixels
```
5. Add a heatmap:
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 10
center:
lat: 34.0522
lng: -118.2437
heatmap:
positions:
- lat: 34.091158
lng: -118.2795188
weight: 1
- lat: 34.0771192
lng: -118.2587199
weight: 2
- lat: 34.083527
lng: -118.370157
weight: 1
- lat: 34.0951843
lng: -118.283107
weight: 2
- lat: 34.1033401
lng: -118.2875469
weight: 4
- lat: 34.035798
lng: -118.251288
weight: 2
- lat: 34.0776068
lng: -118.2646526
weight: 3
- lat: 34.0919263
lng: -118.2820544
weight: 3
- lat: 34.0568525
lng: -118.3646369
weight: 3
- lat: 34.0285781
lng: -118.4115541
weight: 0
- lat: 34.017339
lng: -118.278469
weight: 0
- lat: 34.0764288
lng: -118.3661624
weight: 4
- lat: 33.9925942
lng: -118.4232475
weight: 4
- lat: 34.0764345
lng: -118.3730332
weight: 3
- lat: 34.093981
lng: -118.327638
weight: 0
- lat: 34.056385
lng: -118.2508724
weight: 1
- lat: 34.107701
lng: -118.2667943
weight: 4
- lat: 34.0450139
lng: -118.2388682
weight: 4
- lat: 34.1031997
lng: -118.2586152
weight: 1
- lat: 34.0828183
lng: -118.3241586
weight: 1
options:
radius: 20
opacity: 1
```
6. Add a heatmap after api has loaded:
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 10
center:
lat: 34.0522
lng: -118.2437
events:
onMountAsync:
- id: add_heatmap
type: CallMethod
params:
blockId: google_maps
method: addHeatmap
args:
- data:
- location:
lat: 34.091158
lng: -118.2795188
weight: 1
- location:
lat: 34.0771192
lng: -118.2587199
weight: 2
- location:
lat: 34.0828183
lng: -118.3241586
weight: 1
radius: 20
opacity: 1
```
7. Toggle a heatmap
```yaml
- id: google_maps
type: GoogleMaps
properties:
bootstrapURLKeys:
key: ''
libraries: ['visualization']
mapOptions:
panControl: true
zoomControl: true
fullscreenControl: true
zoom: 10
center:
lat: 34.0522
lng: -118.2437
events:
onClick:
- id: toggle_heatmap
type: CallMethod
params:
blockId: google_maps
method: toggleHeatmap
```

View File

@ -0,0 +1,22 @@
export default {
clearMocks: true,
collectCoverage: true,
collectCoverageFrom: ['src/**/*.js'],
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: [
'<rootDir>/dist/',
'<rootDir>/src/test',
'<rootDir>/src/index.js',
'<rootDir>/src/blocks.js',
'<rootDir>/src/types.js',
],
coverageReporters: [['lcov', { projectRoot: '../../../..' }], 'text', 'clover'],
errorOnDeprecated: true,
testEnvironment: 'jsdom',
testPathIgnorePatterns: ['<rootDir>/dist/', '<rootDir>/src/test'],
transform: {
'^.+\\.(t|j)sx?$': ['@swc/jest', { configFile: '../../../../.swcrc.test' }],
'\\.yaml$': '@lowdefy/jest-yaml-transform',
},
snapshotSerializers: ['@emotion/jest/serializer', 'jest-serializer-html'],
};

View File

@ -0,0 +1,70 @@
{
"name": "@lowdefy/blocks-google-maps",
"version": "4.0.0-alpha.12",
"license": "Apache-2.0",
"description": "Google Maps Blocks for Lowdefy.",
"homepage": "https://lowdefy.com",
"keywords": [
"lowdefy",
"lowdefy blocks",
"google",
"maps",
"lowdefy plugin"
],
"bugs": {
"url": "https://github.com/lowdefy/lowdefy/issues"
},
"contributors": [
{
"name": "Gerrie van Wyk",
"url": "https://github.com/Gervwyk"
}
],
"repository": {
"type": "git",
"url": "https://github.com/lowdefy/lowdefy.git"
},
"type": "module",
"exports": {
"./*": "./dist/*",
"./blocks": "./dist/blocks.js",
"./types": "./dist/types.js"
},
"files": [
"dist/*"
],
"scripts": {
"build": "yarn swc",
"clean": "rm -rf dist",
"copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.yaml\" -e \"./src/**/*.snap\"",
"prepare": "yarn build",
"swc": "swc src --out-dir dist --config-file ../../../../.swcrc --delete-dir-on-start && yarn copyfiles",
"test:watch": "jest --coverage --watch",
"test": "jest --coverage"
},
"dependencies": {
"@googlemaps/react-wrapper": "1.1.34",
"@lowdefy/block-utils": "4.0.0-alpha.12",
"react": "18.1.0",
"react-dom": "18.1.0"
},
"devDependencies": {
"@emotion/jest": "11.9.1",
"@lowdefy/block-dev": "4.0.0-alpha.12",
"@lowdefy/helpers": "4.0.0-alpha.12",
"@lowdefy/jest-yaml-transform": "4.0.0-alpha.12",
"@swc/cli": "0.1.57",
"@swc/core": "1.2.194",
"@swc/jest": "0.2.21",
"@testing-library/dom": "8.13.0",
"@testing-library/react": "13.3.0",
"@testing-library/user-event": "14.2.0",
"copyfiles": "2.4.1",
"jest": "28.1.0",
"jest-environment-jsdom": "28.1.0",
"jest-serializer-html": "7.1.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export { default as GoogleMaps } from './blocks/GoogleMaps/GoogleMaps.js';

View File

@ -0,0 +1,156 @@
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { useEffect, useState, useRef } from 'react';
import { Wrapper, Status } from '@googlemaps/react-wrapper';
import { blockDefaultProps } from '@lowdefy/block-utils';
// see which libraries to load at https://developers.google.com/maps/documentation/javascript/libraries
// properties.map{} is an interface of MapOptions: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions
// properties.heatmap{} is an interface of HeatmapLayerOptions: https://developers.google.com/maps/documentation/javascript/reference/visualization#HeatmapLayerOptions
// properties.markers[] is an interface of MarkerOptions: https://developers.google.com/maps/documentation/javascript/reference/marker#MarkerOptions
const MAP_DEFAULTS = {
zoom: 2,
center: {
lat: 0,
lng: 0,
},
};
const render = (status) => {
// TODO: not sure how to implement status for now.
// switch (status) {
// case Status.LOADING:
// return <Spinner />;
// case Status.FAILURE:
// return <ErrorComponent />;
// case Status.SUCCESS:
// return <GoogleMaps />;
return null;
};
const Marker = (options) => {
const [marker, setMarker] = React.useState();
React.useEffect(() => {
if (!marker) {
setMarker(new window.google.maps.Marker());
}
return () => {
if (marker) {
marker.setMap(null);
}
};
}, [marker]);
React.useEffect(() => {
if (marker) {
marker.setOptions(options);
}
}, [marker, options]);
return null;
};
function updateHeatmap(data) {
if (data.location) {
const latLng = new window.google.maps.LatLng(data.location);
latLng.weight = data.weight || 1;
return latLng;
}
return new window.google.maps.LatLng(data);
}
const Map = ({ blockId, properties, methods }) => {
const [map, setMap] = useState();
const [heatmap, setHeatmap] = useState();
const ref = useRef();
const triggerOnClick = (event) => {
methods.triggerEvent({
name: 'onClick',
event,
});
};
useEffect(() => {
if (ref.current) {
if (!map) {
const newMap = new window.google.maps.Map(ref.current, {
...MAP_DEFAULTS,
...properties.map,
});
setMap(newMap);
if (properties.heatmap) {
const newHeatmap = new window.google.maps.visualization.HeatmapLayer({
...properties.heatmap,
data: properties.heatmap.data?.map(updateHeatmap),
map: newMap,
});
setHeatmap(newHeatmap);
}
} else {
map.setOptions({ ...MAP_DEFAULTS, ...properties.map });
if (heatmap) {
heatmap.setOptions({
...properties.heatmap,
data: properties.heatmap.data?.map(updateHeatmap),
map,
});
}
}
}
});
useEffect(() => {
if (map) {
['click'].forEach((eventName) => window.google.maps.event.clearListeners(map, eventName));
if (triggerOnClick) {
map.addListener('click', triggerOnClick);
}
}
}, [map, triggerOnClick]);
return (
<div
className={methods.makeCssClass([{ height: 300 }, properties.style])}
id={blockId}
ref={ref}
/>
);
};
const GoogleMaps = ({ blockId, events, methods, properties, loading }) => {
const libraries = new Set(properties.libraries || []);
if (properties.heatmap) {
libraries.add('visualization');
}
return (
<Wrapper apiKey={properties.apiKey} render={render} libraries={[...libraries]}>
<Map blockId={blockId} properties={properties} methods={methods}>
{(properties.markers || []).map((markerOptions, i) => (
<Marker key={i} {...markerOptions} />
))}
<Marker />
</Map>
</Wrapper>
);
};
GoogleMaps.defaultProps = blockDefaultProps;
GoogleMaps.meta = {
category: 'display',
icons: [],
styles: [],
};
export default GoogleMaps;

View File

@ -0,0 +1,111 @@
{
"type": "object",
"properties": {
"type": "object",
"additionalProperties": false,
"properties": {
"bootstrapURLKeys": {
"type": "object",
"description": "{ key: '', language: 'en', region: 'en', libraries: ['places'], ...otherUrlParams, } If you want to include additional libraries to load with the maps api, indicate them in the libraries property of the bootstrapURLKeys object."
},
"center": {
"type": "object",
"description": "Can be set to [lat, lng] or { lat: lat, lng: lng}. A position object for the center."
},
"debounced": {
"type": "boolean",
"description": "Defaults to true. Whether map events are debounced."
},
"defaultCenter": {
"type": "object",
"description": "Can be set to [lat, lng] or { lat: lat, lng: lng}. A position object for the center."
},
"defaultZoom": {
"type": "number",
"description": "Map zoom level."
},
"heatmap": {
"type": "object",
"description": "To use the heatmap layer, add visualization to the libraries property array on bootstrapURLKeys and provide the data & configuration for the heatmap in heatmap as props. If you have multiple maps in your project and require a heatmap layer in at least one of them, provide libraries:['visualization'] to all of them."
},
"height": {
"type": "object",
"description": "The height of the map block."
},
"hoverDistance": {
"type": "number",
"description": "Defaults to 30. Map hover distance."
},
"layerTypes": {
"type": "array",
"description": "Examples ['TrafficLayer', 'TransitLayer']. The layer types to be included in the map."
},
"mapOptions": {
"type": "object",
"description": "Custom map options."
},
"margin": {
"type": "array",
"description": "Map margin."
},
"markers": {
"type": "array",
"description": "A list of Markers with defined properties."
},
"resetBoundsOnResize": {
"type": "boolean",
"description": "Default: false, When true this will reset the map bounds if the parent resizes."
},
"style": {
"type": "object",
"description": "Custom map css properties to apply to map block."
},
"zoom": {
"type": "number",
"description": "Map zoom level."
}
}
},
"events": {
"type": "object",
"additionalProperties": false,
"properties": {
"onClick": {
"type": "array",
"description": "Trigger onClick actions when the map is clicked."
},
"onClickMarker": {
"type": "array",
"description": "Trigger onClick actions when a marker is clicked."
},
"onDrag": {
"type": "array",
"description": "Trigger onDrag actions when a map is dragged."
},
"onDragEnd": {
"type": "array",
"description": "Trigger onDragEnd actions when a map is finished being dragged."
},
"onGoogleApiLoaded": {
"type": "array",
"description": "Trigger onGoogleApiLoaded actions when the map api is loaded."
},
"onMapTypeIdChange": {
"type": "array",
"description": "Trigger onMapTypeIdChange actions when the map type is changed."
},
"onTilesLoaded": {
"type": "array",
"description": "Trigger onTilesLoaded actions when the map tiles are loaded."
},
"onZoomAnimationStart": {
"type": "array",
"description": "Trigger onZoomAnimationStart actions when the map is zoomed."
},
"onZoomAnimationEnd": {
"type": "array",
"description": "Trigger onZoomAnimationEnd actions after the map is zoomed."
}
}
}
}

View File

@ -0,0 +1,30 @@
/* eslint-disable import/namespace */
/*
Copyright 2020-2022 Lowdefy, Inc
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import * as blocks from './blocks.js';
const icons = {};
const styles = {};
Object.keys(blocks).forEach((block) => {
icons[block] = blocks[block].meta.icons || [];
styles[block] = blocks[block].meta.styles || [];
});
export default {
blocks: Object.keys(blocks),
icons,
styles: { default: [], ...styles },
};